将音符写入wav文件

我感兴趣的是如何将音符(例如A,B,C#等)或和弦(同时多个音符)写入一个wav文件。

据我所知,每个音符都有一个与之相关的特定频率(对于完美的音高),例如A4(中间C以上的A)为440Hz(完成列表2/3)。

如果我的理解是正确的,那么这个音调就是在频域中,那么就需要应用逆快速傅里叶变换来产生时域等价的?

我想知道的是:

  • 和弦如何工作? 他们是球场的平均水平吗?
  • 当wav文件的内容是波形时,播放每个音符的时间长度是多less?
  • 如何将多个音符逆FFT'd的结果转换为一个字节数组,这些字节组成了wav文件中的数据?
  • 与此相关的任何其他相关信息。

谢谢你提供的所有帮助。 如果给出代码示例,我正在使用C#和我目前用来创buildwav文件的代码如下所示:

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); 

这(不知何故)创造了一个不变的声音 – 但我不完全理解代码如何与结果相关联。

你在正确的轨道上。

我们来看看你的例子:

 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之间,所以我们在一个字节的范围内。

试试看看它听起来如何。 它应该听起来很“平滑”。

人耳检测到空气压力的微小变化。 如果这些变化形成一个重复的模式,那么模式重复的频率被耳蜗中的耳蜗解释为特定的音调。 压力变化的大小被解释为体积

你的波形是六十秒钟。 变化从最小的变化1变化到最大的变化255. 峰值在哪里? 也就是说,样本在什么地方达到了255的值,还是接近?

那么,在π/ 2,5π/ 2,9π/ 2,13π/ 2上的正弦为1,依此类推。 所以,无论何时我都接近其中的一个峰值。 也就是说,在2,8,14,20,…

那些时间有多远? 每个样本是1/11025秒,所以峰值在每个峰值之间大约2π/ 11025 =大约570微秒。 每秒有多less峰? 11025 /2π= 1755Hz。 (赫兹是频率的度量;每秒有多less个峰值)。 1760赫兹是A 440以上的两个八度音阶,所以这是一个稍微平坦的A音。

和弦如何工作? 他们是球场的平均水平吗?

不,A440和八度以上的和弦,A880不等于660Hz。 你不平均球场 。 你总结 波形

想想空气压力。 如果您有一个振动源每秒钟上下压力440次,而另一个振动压力每秒钟上下880次,那么networking就不会像660次/秒的振动一样。 这等于任何时间点的压力总和。 请记住,这是一个WAV文件是: 一个气压变化的大名单

假设你想在你的样本下面做一个八度。 频率是多less? 一半。 所以我们让它发生一半:

 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之间,所以这不能溢出一个字节。

(所以有一个平均值:我们基本上是把每个音调的压力的平均值,而不是频率的平均值)。

如果你演奏,你应该同时得到两个音调,一个比另一个高八度。

最后一个expression是复杂的,难以阅读。 让我们把它分解成更容易阅读的代码。 但是,首先总结一下这个故事:

  • 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))); 

你去那里; 现在你可以产生任何你想要的audio和音量。 为了创造一个和弦,把它们加在一起,确保你不要太大声,溢出字节。

您如何知道A220,A440,A880等笔记的频率? 每个半音上乘前一个频率的第12根。所以计算2的第12根,乘以440,这就是A#。 乘以2的12根,即B的B乘以2的第12根即为C,然后是C#,依此类推。 做12次,因为它是2的第12根,你会得到880,是你开始的两倍。

当wav文件的内容是波形时,播放每个音符的时间长度是多less?

只需填写声音的示例空间即可。 假设你想玩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'd的结果转换为一个字节数组,这些字节组成了wav文件中的数据?

反向FFT只是build立正弦波并将它们加在一起,就像我们在这里做的一样。 就是这样!

有关这方面的其他相关信息?

看到我关于这个问题的文章。

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

第一至第三部分解释了为什么钢琴每八音度有十二个音符。

第四部分与你的问题有关; 这就是我们从头开始构buildWAV文件的地方。

请注意,在我的例子中,我每秒使用44100个采样,而不是11025,而我使用的是16位采样,范围从-16000到+16000,而不是8位采样,范围从0到255.但是除了这些细节之外,基本上和你的一样。

如果你要做任何复杂的波形,我build议你去更高的比特率; 对于复杂的波形来说,每秒钟11K采样的8位音质听起来很糟糕。 每个样本16位,每秒44K个样本是CD质量。

坦率地说,如果你用签名的短语而不是无符号的字节来进行math计算,那么要容易得多。

第五部分给出了一个听觉幻觉的有趣例子。

另外,请尝试使用Windows Media Player中的“范围”可视化来观看波形。 这会给你一个很好的想法。

更新:

我注意到,在将两个音符附加在一起时,由于两个波形之间的过渡太尖锐(例如,在一个音符的顶部结束并从下一个音符的底部开始),最终会出现爆音。 如何克服这个问题?

优秀的后续问题。

从本质上讲,这里发生的是从高压到低压的瞬间过渡,这被认为是“stream行”。 有几种方法可以解决这个问题。

技术1:相移

一种方法是将后续音调“相移”一些小的数量,以使后续音调的起始值与前一个音调的结束值之间的差值相移。 你可以像这样添加一个相移项:

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

如果相移为零,那显然是不变的。 2π(或π的任何偶数倍)的相移也不变,因为sin具有2π的周期。 在0到2π之间的每个值都会在音调“开始”的位置沿着波浪进一步移动。

确切地说,正确的相移是一个棘手的问题。 如果你阅读我关于产生“不断下降”的谢泼德幻觉的文章,你会发现我使用了一些简单的微积分来确保一切都不断变化,没有任何stream行。 你可以使用类似的技术来找出正确的转变是什么使stream行消失。

我正在试图找出如何生成相移值。 是“ArcSin((((新音符的第一个数据样本) – (上一个音符的最后一个数据样本))/ noteVolume)”对不对?

那么,首先要认识到的是,可能没有“正确的价值”。 如果结尾音符非常大,并在高峰结束,并且开始音符非常安静,则新音调中可能没有与旧音调的值匹配的点。

假设有一个解决scheme,它是什么? 你有一个结束样本,称之为y,并且你想find这样的相移x

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

当我是零。 所以那是

 x = arcsin(y / v) 

但是 ,这可能不太正确! 假设你有

正弦波1

你想追加

正弦波2

两种可能的相移

正弦波3

正弦波4

大胆猜测哪一个听起来更好。 🙂

弄清楚你是在“上冲”还是在“下冲”,可能有点棘手。 如果你不想计算真正的math,你可以做一些简单的启发式algorithm,比如“在转换过程中连续数据点变化的差异的迹象?

技术2:ADSR信封

如果你正在build模的东西听起来像一个真正的乐器,那么你可以通过如下改变音量来获得好的结果。

你想要做的是每个音符有四个不同的部分,称为攻击,衰减,维持和释放。 乐器上演奏的音符的音量可以这样来模拟:

  /\ / \__________ / \ / \ ADSR 

音量从零开始。 然后攻击发生:声音迅速攀升到峰值音量。 然后它稍微衰减到维持水平。 然后它保持在这个水平上,也许在音符播放的时候慢慢下降,然后释放回零。

如果你这样做,那么没有popup,因为每个音符的开始和结束都是零音量。 该版本确保。

不同的乐器有不同的“信封”。 例如,一个pipe风琴有非常短的攻击,衰减和释放; 这一切都是持续的,持续的是无限的。 你现有的代码就像一个pipe风琴。 比较一下,比如钢琴。 再次,短暂的攻击,短暂的衰退,短暂的释放,但声音在延续期间确实会逐渐平静下来。

攻击,衰减和释放部分可能很短,听不清,但足够长以防止stream行。 随着音符播放,尝试改变音量,看看会发生什么。

你在正确的轨道上。 🙂

audio信号

你不需要做一个逆FFT(你可以,但是你需要为它find一个lib或实现它,再加上生成一个信号作为input)。 直接生成我们期望从IFFT得到的结果要容易得多,IFFT是给定频率的正弦信号。

正弦波的参数取决于你要生成的音符和你生成的波形文件的采样频率 (通常等于44100Hz,在你的例子中你使用的是11025Hz)。

对于1Hz的音调,你需要有一个周期等于一秒的正弦信号。 44100赫兹,每秒44100样本,这意味着我们需要有一个周期等于44100个样本的正弦信号。 由于正弦的周期等于Tau (2 * Pi),我们得到:

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

对于440赫兹,我们得到:

 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)); 

注:我没有testing过这个来validation代码的正确性。 我会尽力做到这一点,纠正任何错误。 更新:我已经更新了一些有效的代码。 对不起,伤了你的耳朵;-)

和弦

和弦是音符的组合(例如,参见Wikipedia上的Minor chord )。 所以信号将是不同频率正弦波的组合(和)。

纯色调

那些音调和和弦听起来不自然,因为传统的乐器不会播放单一的频率音调。 相反,当您播放A4时,频率分布范围很广,集中在440赫兹左右。 参见例如Timbre 。

还没有人提到Karplus Strong弹拨algorithm。

Karplus强弦合成这是一个非常简单的方法来产生一个逼真的弹拨弦音。 我用这个写了和弦乐器/实时MIDI播放器。

你这样做:

首先,你想模拟什么频率? 假设音乐会音高A = 440Hz

假设您的采样率为44.1kHz,即每个波长44100/440 = 100.25个采样。

让我们将其四舍五入到最接近的整数100,并创build一个循环缓冲区长度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 

这是一个了不起的algorithm,因为它是如此简单,并产生超级声音。

理解发生的最好的方法是认识到时域中的随机静态是白噪声; 在频域中是随机静态的。 你可以把它想象成许多不同(随机)频率波的复合物。

接近440Hz(或2 * 440Hz,3 * 440Hz等)的频率将会对他们自身造成build设性的干扰,因为他们一次又一次地绕过环。 所以他们将被保留。 其他频率将破坏性地干扰自己。

此外,平均作为一个低通滤波器 – 想象你的序列是+1 -1 +1 -1 +1 -1,如果你平均对,然后每个平均值为0,但如果你有更慢的波如0 0.2 0.3 0.33 0.3 0.2 …然后平均仍然导致一波。 波浪越长,其能量保存得越多 – 即平均化会导致更less的阻尼。

所以平均可以被认为是非常简单的低通滤波器。

当然有复杂性,必须select一个整数缓冲长度来强制量化可能的频率,这在钢琴的顶部变得明显。 一切都是可以克服的,但变得艰难!

链接:

Delicious Max / MSP教程1:Karplus-Strong

Karplus强algorithm

JOS据我所知是世界上合成音调产生的主要权威,所有的道路都通向他的网站。 但是要注意的是,这个技巧非常复杂,需要大学水平的math。

Interesting Posts