波を作成する
私たちは皆、音は波であり、ゼロレベルからの波の振動の周波数は音波の周波数に対応し、この波の振幅がその強さまたは単に話す音量に関与することを完全に理解していますが、機械表現では、パルスコード変調の形で記録された音はデータの配列です、各要素は特定の時点での波の位置を表します。
PCM形式の単純な音波をよりよく見てみましょう。このために、最初に正弦波のモデルとなる関数を作成します。 オフセットと周波数の2つの値を取ります。
public static double Sine(int index, double frequency) { return Math.Sin(frequency * index); }
次に、それをProgramクラスに追加し、サウンドを表す75要素のデータ配列を初期化し、作成したサイン波モデルを使用して各セルを循環するメイン関数を記述します。 特定の変位に対する関数の値を計算するには、2 * Piに等しい正弦波の周期を考慮し、この周期に必要な波の周波数を掛ける必要があります。 ただし、結果の周波数がPCM形式でどのように聞こえるかを理解するには、そのサンプリング周波数を知る必要があります。 サンプリング周波数-単位時間あたりの要素のサンプリングレートですが、簡略化すると、これは1秒あたりの配列要素の数になります。つまり、PCM形式の音声周波数は、波の周波数をサンプリング周波数で割ったものになります。 サンプリング周波数が75 Hzに等しくなることに同意しながら、周波数2 Hzの音波を生成してみましょう。
class Program { public static void Main(string[] args) { double[] data = new double[75]; // . for (int index = 1; index < 76; index++) { // . data[index-1] = Sine(index, Math.PI * 2 * 2.0 / 75); // . } Console.ReadKey(true); // . } public static double Sine(int index, double frequency) { return Math.Sin(frequency * index); } }
そして今、作業の結果を確認するために、コンソールで関数を直接視覚化できる新しい関数をProgramクラスに追加します(これが最速の方法です)ので、詳細については説明しません。
public static void Draw (double[] data) { Console.BufferHeight = 25; // . Console.CursorVisible = false; // . for (int y = 0; y < 19; y++) {// . Console.SetCursorPosition(77, y + 5);// . Console.Write(9 - y); // . } for (int x = 0; x < 75; x++) { // Console.SetCursorPosition(x, x % 3); // . Console.Write(x + 1); // . int point = (int)(data[x] * 9); // -9 9. int step = (point > 0)? -1 : 1; // 0. for (int y = point; y != step; y += step) {// Console.SetCursorPosition(x, point + 14 - y); // . Console.Write("█"); // . } } }
これで、マシンビューで2つのヘルツがどのように見えるかを確認できます。

しかし、これは1つのタイプの波にすぎませんが、他の多くのタイプの波がありますが、主なタイプの波をシミュレートし、それらがどのように見えるかを考えてみましょう。
private static double Saw(int index, double frequency) { return 2.0 * (index * frequency - Math.Floor(index * frequency )) -1.0; }
のこぎり関数の結果

private static double Triangle(int index, double frequency) { return 2.0 * Math.Abs (2.0 * (index * frequency - Math.Floor(index * frequency + 0.5))) - 1.0; }
Triangle関数の結果

private static double Flat(int index, double frequency) { if (Math.Sin(frequency * index ) > 0) return 1; else return -1; }
Flat関数の結果

Sine関数とFlat関数の周期は2 * Piであり、Saw関数とTriangle関数の周期は1であることに注意してください。
wavファイルを書く
サウンドを作成し、検討することさえできたら、そのためにそれを聞きたいと思います。.wavコンテナに録音して聴いてみましょう。 確かに、Waveコンテナがどのように配置されているかを知るまで、この厄介な間違いを修正する必要があります。 したがって、waveファイルは非常にシンプルで、3つの部分で構成されています。最初の部分はブロックヘッダー、2番目はフォーマットブロック、3番目はデータブロックです。 合わせて、次のようになります。

実際、すべてが非常に明確であり、各ポイントの詳細はここで説明されています 。 タスクは非常に単純なので、常に16ビットのビット幅と1つのトラックのみを使用します。 ここで作成する関数は、ストリーム内のWavコンテナーにPCMサウンドを保存します。これにより、作業がより柔軟になります。 これが彼女の外見です。
public static void SaveWave(Stream stream, short[] data, int sampleRate) { BinaryWriter writer = new BinaryWriter(stream); short frameSize = (short)(16 / 8); // (16 8). writer.Write(0x46464952); // "RIFF". writer.Write(36 + data.Length * frameSize); // . writer.Write(0x45564157); // "WAVE". writer.Write(0x20746D66); // "frm ". writer.Write(16); // . writer.Write((short)1); // 1 PCM. writer.Write((short)1); // . writer.Write(sampleRate); // . writer.Write(sampleRate * frameSize); // ( ). writer.Write(frameSize); // . writer.Write((short)16); // . writer.Write(0x61746164); // "DATA". writer.Write(data.Length * frameSize); // . for (int index = 0; index < data.Length; index++) { // . foreach (byte element in BitConverter.GetBytes(data[index])) { // . stream.WriteByte(element); // . } } }
あなたはそれがどれほどシンプルであるか、そして最も重要なことは、私たちが音を聞くことができるようになったということです。最初のオクターブに対して1秒の音を生成してみましょう。周波数は440 Hzです。
public static void Main(string[] args) { int sampleRate = 8000; // . short[] data = new short[sampleRate]; // 16 . double frequency = Math.PI * 2 * 440.0 / sampleRate; // . for (int index = 0; index < sampleRate; index++) { // . data[index] = (short)(Sine(index, frequency) * short.MaxValue); // 32767 -32767. } Stream file = File.Create("test.wav"); // . SaveWave(file, data, sampleRate); // . file.Close(); // . }
私たちはプログラムを開始し、見よ! test.wavをプレーヤーにダウンロードし、カタルシスが達成されるまでビープ音を聞いて進みます。 オシロスコープとスペクトログラムで四方からの波を見て、達成した結果が正確に得られたことを確認しましょう。


しかし、人生では、音は際限なく鳴りませんが、沈静化して、時間とともに音を消してしまうような修飾子を書きましょう。 彼は絶対値が必要なので、係数、現在位置、周波数、係数乗数、サンプリング周波数を彼に与え、絶対値を自分で計算します。係数は常に負でなければなりません。
public static double Length(double compressor, double frequency, double position, double length, int sampleRate){ return Math.Exp(((compressor / sampleRate) * frequency * sampleRate * (position / sampleRate)) / (length / sampleRate)); }
サウンドレベルを計算する行も変更する必要があります。
data[index] = (short)(Sine(index, frequency) * Length(-0.0015, frequency, index, 1.0, sampleRate) * short.MaxValue);
オシロスコープでは、まったく異なる画像が表示されます。

音楽を書く
4オクターブのノートを演奏することができたので、誰も別のノートを演奏することを気にしません。 音の周波数を調べる方法について疑問に思ったことはありますか? 素晴らしい式440 * 2 ^(絶対音符インデックス/ 12)があります。 ピアノのような楽器を見ると、白い鍵が7つ、黒い鍵が5つ、ブロックがオクターブ、白い鍵が主音(C、D、E、F、S、A、C)であり、黒であることに注意してくださいそれらのミッドトーン、つまり、オクターブ内でたった12音、これは均一な気質と呼ばれます。
この関数のグラフを見てみましょう。

ただし、科学表記法でメモを書くため、4オクターブ下げて公式を少し変更し、ネイティブ形式で書きます。
private static double GetNote(int key, int octave) { return 27.5 * Math.Pow(2, (key + octave * 12.0) / 12.0); }
基本的な機能を組み立ててその作業をデバッグしたので、将来のシンセサイザーのアーキテクチャについて考えてみましょう。
シンセサイザーは、サウンドを合成し、適切な場所の空のデータ配列にオーバーレイする特定の要素のセットになります。この配列と要素は、トラックオブジェクトに含まれます。 それらを記述するクラスはSynthesizer名前空間に含まれます。ElementおよびTrackクラスを記述しましょう
public class Element { int length; int start; double frequency; double compressor; public Element(double frequency, double compressor, double start, double length, int sampleRate) { this.frequency = Math.PI * 2 * frequency / sampleRate ; this.start = (int)(start * sampleRate); this.length = (int)(length * sampleRate); this.compressor = compressor / sampleRate; } public void Get(ref short[] data, int sampleRate) { double result; int position; for (int index = start; index < start + length * 2; index++) { position = index - start; result = 0.5 * Sine(position, frequency) ; result += 0.4 * Sine(position, frequency / 4); result += 0.2 * Sine(position, frequency / 2); result *= Length(compressor, frequency, position, length, sampleRate) * short.MaxValue * 0.25; result += data[index]; if (result > short.MaxValue) result = short.MaxValue; if (result < -short.MaxValue) result = -short.MaxValue; data[index] = (short)(result); } } private static double Length(double compressor, double frequency, double position, double length, int sampleRate){ return Math.Exp((compressor * frequency * sampleRate * (position / sampleRate)) / (length / sampleRate)); } private static double Sine(int index, double frequency) { return Math.Sin(frequency * index); } }
public class Track { private int sampleRate; private List<Element> elements = new List<Element>(); private short[] data; private int length; private static double GetNote(int key, int octave) { return 27.5 * Math.Pow(2, (key + octave * 12.0) / 12.0); } public Track(int sampleRate) { this.sampleRate = sampleRate; } public void Add(double frequency, double compressor, double start, double length) { if (this.length < (start+ length * 2 + 1) * sampleRate) this.length = (int)(start + length * 2 +1) * sampleRate; elements.Add(new Element(frequency, compressor, start, length, sampleRate)); } public void Synthesize() { data = new short[length]; foreach (var element in elements) { element.Get(ref data, sampleRate); } } }
これで、ノートを含む行を読み取り、メロディを生成する最後の関数に到達しました
これを行うには、ノートの名前をインデックスに関連付けるディクショナリを作成し、コントロールキー/インデックスも含めます。
関数自体は行を単語に分割し、各単語を2つの部分に分けてさらに処理します-左右、右側の部分は常に1つの文字(数字)で構成され、変数としてオクターブに数字として書き込まれ、最初の部分の長さは単語の長さです- 1(つまり、単語から右側を引いたもの)、さらに辞書のキーとして機能し、メモのインデックスを返します。単語を解析した後、何をするかを決定します。インデックスが制御している場合は、インデックスに対応する機能を実行し、そうでない場合は、 ノートインデックスがあり、必要なノートの長さと周波数の新しいサウンドをトラックに追加します。
public void Music (string melody, double temp = 60.0) { string[] words = melody.Split(' '); foreach (string word in words) { int note = notes[word.Substring(0, word.Length - 1)]; int octave = Convert.ToInt32(word.Substring(word.Length - 1, 1)); if (note > 2){ switch (note) { case 3: dtime = Math.Pow(0.5, octave + 1) * 8 * (60.0 / temp); break; case 4: length += (int)(Math.Pow(0.5, octave + 1) * 8 * (60.0 / temp)); position += Math.Pow(0.5, octave + 1) * 8 * (60.0 / temp); break; } } else { Add(GetNote(note, octave), -0.51, position, dtime); position += dtime; } } }
この時点から、メロディは
L4 B6 S4 D7 B6 F#6 S4 B6 F#6
ように記述できます。ここで、Lは音符の長さを設定するコマンドで、Sはポーズを作成し、残りの文字は音符です。 実際、ここでソフトウェアシンセサイザーの記述が終わり、「バッハジョーク」の一部を聞いて結果を確認できます。
バイナリファイル
ソースコード