c# - 將音符寫入wav文件




audio music (4)

我感興趣的是如何拍音符(例如A,B,C#等)或和弦(同時多個音符)並將它們寫入wav文件。

根據我的理解,每個音符都有一個與之相關的特定頻率(對於完美音高) - 例如A4(中間C以上的A)是440 Hz(完整列表本頁下方的2/3)。

如果我的理解是正確的,這個音調是在頻域,因此需要應用它的逆快速傅里葉變換來生成時域等價?

我想知道的是:

  • 和弦如何工作? 他們是球場的平均值嗎?
  • 當wav文件的內容是波形時,如何指定播放每個音符的時間長度?
  • 如何將多個音符的反FFT轉換為字節數組,這組成了wav文件中的數據?
  • 與此有關的任何其他相關信息。

謝謝你提供的所有幫助。 如果給出代碼示例,我使用的是C#,我目前用來創建wav文件的代碼如下:

int channels = 1;
int bitsPerSample = 8;
//WaveFile is custom class to create a wav file.
WaveFile file = new WaveFile(channels, bitsPerSample, 11025);

int seconds = 60;
int samples = 11025 * seconds; //Create x seconds of audio

// Sound Data Size = Number Of Channels * Bits Per Sample * Samples

byte[] data = new byte[channels * bitsPerSample/8 * samples];

//Creates a Constant Sound
for(int i = 0; i < data.Length; i++)
{
    data[i] = (byte)(256 * Math.Sin(i));
}
file.SetData(data, samples);

這會(以某種方式)創建一個恆定的聲音 - 但我不完全理解代碼如何與結果相關聯。


Answers

還沒有人提到過Karplus Strong彈撥的字符串算法。

Karplus-強字符串合成這是一種非常簡單的方法,用於生成逼真的彈撥弦樂聲。 我用這個寫了複音樂器/實時MIDI播放器。

你這樣做:

首先,您想要模擬的頻率是多少? 讓我們說音樂會音高A = 440Hz

假設您的採樣率為44.1kHz,即每波長44100/440 = 100.25個樣本。

讓我們將其舍入到最接近的整數:100,並創建一個循環緩衝區長度100。

所以它將保持一個頻率~440Hz的駐波(注意它不精確,有這種方法)。

用-1和+1之間的隨機靜態填充它,並且:

DECAY = 0.99
while( n < 99999 )
    outbuf[n++] = buf[k]

    newVal = DECAY  *  ( buf[k] + buf_prev ) / 2

    buf_prev = buf[k]
    buf[k] = newVal

    k = (k+1) % 100

這是一個了不起的算法,因為它非常簡單,並產生超級聲音。

了解正在發生的事情的最好方法是認識到時域中的隨機靜態是白噪聲; 頻域中的隨機靜態。 你可以把它想像成不同(隨機)頻率的許多波的複合。

接近440Hz(或2 * 440Hz,3 * 440Hz等)的頻率會對它們自身造成建設性干擾,因為它們一次又一次地繞過環。 所以他們將被保留下來。 其他頻率會破壞性地干擾自己。

此外,平均作為低通濾波器 - 想像你的序列是+1 -1 +1 -1 +1 -1,如果你是平均對,那麼每個平均值出現為0但是如果你有像0這樣的慢波0.2 0.3 0.33 0.3 0.2 ...然後平均值仍會產生波形。 波浪越長,其能量保留得越多 - 即平均值會導致阻尼減少。

因此,平均可以被認為是非常簡單的低通濾波器。

當然存在復雜性,必須選擇整數緩衝器長度迫使量化可能的頻率,這對於鋼琴的頂部變得明顯。 一切都是可以克服的,但它變得艱難!

鏈接:

美味的Max / MSP教程1:Karplus-Strong

Karplus-Strong算法

據我所知,JOS是合成音發生的世界領先權威,所有道路都可以回到他的網站。 但要注意,它變得非常棘手,需要大學水平的數學。


你走在正確的軌道上。

我們來看看你的例子:

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(256 * Math.Sin(i));

好的,你每秒有11025個樣本。 你有60秒的樣品價值。 每個樣本是0到255之間的數字,表示在給定時間內空間點處的氣壓的微小變化。

等一下,正弦從-1變為1,所以樣本從-256變為+256,並且大於一個字節的範圍,所以這裡有一些傻瓜。 讓我們重新編寫代碼,使樣本處於正確的範圍內。

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + 127 * Math.Sin(i));

現在我們可以平滑地改變1到255之間的數據,因此我們處於一個字節的範圍內。

嘗試一下,看看它聽起來如何。 它應該聽起來很“平滑”。

人耳檢測到氣壓變化非常微小。 如果這些變化形成重複模式,那麼模式重複的頻率將被耳朵中的耳蝸解釋為特定音調。 壓力變化的大小被解釋為體積

你的波形長達60秒。 變化從最小的變化1變為最大的變化255. 峰值在哪裡? 也就是說,樣本在何處達到255或接近它?

那麼,正弦在π/2,5π/2,9π/2,13π/ 2處是1,依此類推。 所以每當我接近其中一個峰值時,峰值就會出現。 也就是說,在2,8,14,20 ......

那些時間相隔多遠? 每個樣品是1/11025秒,因此每個峰之間的峰值約為2π/ 11025 =約570微秒。 每秒有多少個峰值? 11025 /2π= 1755 Hz。 (赫茲是頻率的度量;每秒多少個峰值)。 1760赫茲是A 440以上的兩個八度音階,所以這是一個略微扁平的A音。

和弦如何工作? 他們是球場的平均值嗎?

不是。一個A440和八度音階的和弦,A880不等於660赫茲。 你沒有平均 音高 。 你總結 波形

想想空氣壓力。 如果你有一個振動源每秒抽吸壓力440次,另一個振動壓力每秒上下壓力880次,那麼網絡與每秒660次的振動不同。 它等於任何給定時間點的壓力總和。 請記住,這就是所有的WAV文件: 氣壓變化的大清單

假設您想在樣本下方製作一個八度音程。 頻率是多少? 一半多。 所以,讓它經常發生一半:

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + 127 * Math.Sin(i/2.0)); 

注意它必須是2.0,而不是2.我們不希望整數舍入! 2.0告訴編譯器你希望結果是浮點數,而不是整數。

如果你這樣做,你將獲得一半的峰值:在i = 4,16,28 ......因此音調將是一個完整的八度音階。 (每個八度音程都會使頻率減半 ;每個八度音程都會增加一倍 。)

嘗試一下,看看你是如何得到相同的音調,低一個八度。

現在將它們加在一起。

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + 127 * Math.Sin(i)) + 
            (byte)(128 + 127 * Math.Sin(i/2.0)); 

這可能聽起來像廢話。 發生了什麼? 我們再次溢出 ; 在許多點上總和大於256。 將兩個波浪的體積減半

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + (63 * Math.Sin(i/2.0) + 63 * Math.Sin(i))); 

更好。 “63 sin x + 63 sin y”介於-126和+126之間,因此不能溢出一個字節。

(所以有一個平均值:我們基本上取每個音調壓力的平均值,而不是頻率的平均值。)

如果你演奏它,你應該同時獲得兩個音調,一個八度音高於另一個。

最後一個表達很複雜,難以閱讀。 讓我們將其分解為更易於閱讀的代碼。 但首先,總結一下這個故事:

  • 128是低壓(0)和高壓(255)之間的中間值。
  • 音調的音量是波浪所達到的最大壓力
  • 音調是給定頻率的正弦波
  • 以Hz為單位的頻率是採樣頻率(11025)除以2π

所以我們把它放在一起:

double sampleFrequency = 11025.0;
double multiplier = 2.0 * Math.PI / sampleFrequency;
int volume = 20;

// initialize the data to "flat", no change in pressure, in the middle:
for(int i = 0; i < data.Length; i++)
  data[i] = 128;

// Add on a change in pressure equal to A440:
for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(data[i] + volume * Math.Sin(i * multiplier * 440.0))); 

// Add on a change in pressure equal to A880:

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(data[i] + volume * Math.Sin(i * multiplier * 880.0))); 

你去了; 現在你可以生成任何頻率和音量的音調。 要製作一個和弦,將它們加在一起,確保你不要太大聲並溢出字節。

你怎麼知道A220,A440,A880等以外音符的頻率? 每個半音將上一個頻率乘以2的第12個根。因此,計算2的第12個根,將其乘以440,即A#。 將A#乘以12的2,即B.B乘以12的第12個根是C,然後是C#,依此類推。 這樣做12次,因為它是2的第12根,你將得到880,是你開始的兩倍。

當wav文件的內容是波形時,如何指定播放每個音符的時間長度?

只需填寫音調發聲的樣本空間即可。 假設你想玩A440 30秒,然後A880玩30秒:

// initialize the data to "flat", no change in pressure, in the middle:
for(int i = 0; i < data.Length; i++)
  data[i] = 128;

// Add on a change in pressure equal to A440 for 30 seconds:
for(int i = 0; i < data.Length / 2; i++)
  data[i] = (data[i] + volume * Math.Sin(i * multiplier * 440.0))); 

// Add on a change in pressure equal to A880 for the other 30 seconds:

for(int i = data.Length / 2; i < data.Length; i++)
  data[i] = (byte)(data[i] + volume * Math.Sin(i * multiplier * 880.0))); 

如何將多個音符的反FFT轉換為字節數組,這組成了wav文件中的數據?

反向FFT只是建立正弦波並將它們加在一起,就像我們在這裡做的那樣。 這就是全部!

與此相關的任何其他相關信息?

請參閱我關於此主題的文章。

http://blogs.msdn.com/b/ericlippert/archive/tags/music/

第一至第三部分解釋了為什麼鋼琴每個八度音程有十二個音符。

第四部分與您的問題相關; 這就是我們從頭開始構建WAV文件的地方。

請注意,在我的示例中,我使用的是每秒44100個樣本,而不是11025,我使用的是16位樣本,範圍從-16000到+16000,而不是8位樣本,範圍從0到255.但除了這些細節之外,它是與你的基本相同。

如果您打算做任何復雜的波形,我建議您使用更高的比特率; 對於復雜波形,每秒11K採樣的8位聲音聽起來很糟糕。 每個樣本16位,每秒44K樣本是CD質量。

坦率地說,如果你用簽名的短褲而不是無符號字節來做正確的數學就好了。

第五部分給出了一個有趣的聽覺幻覺的例子。

另外,嘗試使用Windows Media Player中的“範圍”可視化來觀察波形。 這將使您了解實際情況。

更新:

我注意到,當將兩個音符疊加在一起時,由於兩個波形之間的過渡過於尖銳(例如,在一個波形的頂部結束並從下一個波形的底部開始),最終可能會出現爆音。 如何克服這個問題?

優秀的後續問題。

基本上這裡發生的是從高壓到低壓的瞬間過渡,這被稱為“流行”。 有幾種方法可以解決這個問題。

技術1:相移

一種方法是將後續音調“相移”一些小量,使得後續音調的起始值與前一音調的結束值之間的差異。 您可以添加這樣的相移術語:

  data[i] = (data[i] + volume * Math.Sin(phaseshift + i * multiplier * 440.0))); 

如果相移為零,顯然這是沒有變化的。 2π的相移(或π的任何偶數倍)也沒有變化,因為sin具有2π的周期。 0到2π之間的每個值都會在音調“開始”沿著波形進一步移動的地方移動。

確切地確定正確的相移可能有點棘手。 如果你閱讀我關於產生“連續下降”Shepard錯覺音調的文章,你會發現我使用了一些簡單的微積分來確保一切都在不斷變化而沒有任何爆炸聲。 你可以使用類似的技巧來弄清楚正確的轉變是什麼讓流行音樂消失。

我試圖找出如何生成phasehift值。 是“ArcSin(((新筆記的第一個數據樣本) - (前一個筆記的最後一個數據樣本))/ noteVolume”“對嗎?

那麼,首先要意識到的是,可能沒有“正確的價值”。 如果結尾音符非常響亮並且在峰值處結束,並且起始音符非常安靜,則新音調中可能沒有與舊音調的值匹配的點。

假設有一個解決方案,它是什麼? 你有一個結束樣本,稱之為y,你想要找到相移x

y = v * sin(x + i * freq)

當我是零。 所以那是

x = arcsin(y / v)

但是 ,這可能不太對! 假設你有

而且你想追加

兩種可能的相移

猜測哪個聽起來更好聽。 :-)

弄清楚你是處於波浪的“上沖程”還是“下衝程”可能有點棘手。 如果你不想計算出真正的數學,你可以做一些簡單的啟發式算法,比如“連續數據點之間的差異符號在轉換時是否會發生變化?”

技術2:ADSR包絡

如果您正在建模聽起來像真實樂器的東西,那麼您可以通過如下更改音量來獲得良好的效果。

你想要做的是每個音符有四個不同的部分,稱為攻擊,衰減,延音和釋放。 在樂器上演奏的音符音量可以這樣建模:

     /\
    /  \__________
   /              \
  /                \
   A  D   S       R

音量從零開始。 然後發生攻擊:聲音快速上升到其峰值音量。 然後它略微衰減到其維持水平。 然後它保持在那個水平,可能在音符播放時緩慢下降,然後它再次回落到零。

如果你這樣做那麼沒有彈出,因為每個音符的開頭和結尾都是零音量。 該版本確保了這一點。

不同的樂器有不同的“信封”。 例如,管風琴具有令人難以置信的短暫攻擊,腐爛和釋放; 這一切都是持續的,而持續是無限的。 您現有的代碼就像一個管風琴。 比如說鋼琴。 再次,短攻擊,短暫衰減,短暫釋放,但聲音在維持期間逐漸變得更安靜。

攻擊,腐爛和釋放部分可能非常短,太短而無法聽到,但足夠長以防止彈出。 嘗試在音符播放時改變音量,看看會發生什麼。


你走在正確的軌道上。 :)

音頻信號

你不需要進行逆FFT(你可以,但你需要為它找到一個lib或實現它,再加上生成一個信號作為輸入)。 直接生成我們期望的IFFT結果要容易得多,IFFT是具有給定頻率的正弦信號。

正弦的參數取決於您想要生成的音符和您生成的波形文件的採樣頻率 (通常等於44100Hz,在您的示例中使用的是11025Hz)。

對於1 Hz音調,您需要一個正弦信號,其周期等於一秒。 對於44100 Hz,每秒有44100個樣本,這意味著我們需要一個正弦信號,其中一個週期等於44100個樣本。 由於正弦週期等於Tau (2 * Pi),我們得到:

sin(44100*f) = sin(tau)
44100*f = tau
f = tau / 44100 = 2*pi / 44100

對於440 Hz,我們得到:

sin(44100*f) = sin(440*tau)
44100*f = 440*tau
f = 440 * tau / 44100 = 440 * 2 * pi / 44100

在C#中,這將是這樣的:

double toneFreq = 440d;
double f = toneFreq * 2d * Math.PI / 44100d;
for (int i = 0; i<data.Length; i++)
    data[i] = (byte)(128 + 127*Math.Sin(f*i));

注意:我沒有對此進行測試以驗證代碼的正確性。 我會盡力做到並糾正任何錯誤。 更新:我已將代碼更新為有效的代碼。 抱歉傷害了你的耳朵;-)

和弦

和弦是音符的組合(參見維基百科上的小和弦 )。 因此,信號將是具有不同頻率的正弦的組合(和)。

純淨的色調

然而,這些音調和和弦聽起來並不自然,因為傳統樂器不會播放單頻音。 相反,當您演奏A4時,頻率分佈很廣,濃度約為440 Hz。 參見例如Timbre


public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    return (Field.Body as MemberExpression ?? ((UnaryExpression)Field.Body).Operand as MemberExpression).Member.Name;
}

這處理成員和一元表達式。 不同之處在於,如果表達式表示值類型,將得到UnaryExpression ,而如果表達式表示引用類型,則將獲得MemberExpression 。 一切都可以轉換為對象,但值類型必須裝箱。 這就是UnaryExpression存在的原因。 Reference.

為了保持可讀性(@Jowen),下面是一個擴展的等價物:

public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    if (object.Equals(Field, null))
    {
        throw new NullReferenceException("Field is required");
    }

    MemberExpression expr = null;

    if (Field.Body is MemberExpression)
    {
        expr = (MemberExpression)Field.Body;
    }
    else if (Field.Body is UnaryExpression)
    {
        expr = (MemberExpression)((UnaryExpression)Field.Body).Operand;
    }
    else
    {
        const string Format = "Expression '{0}' not supported.";
        string message = string.Format(Format, Field);

        throw new ArgumentException(message, "Field");
    }

    return expr.Member.Name;
}




c# audio music wav