将音符写入wav文件

sim*_*005 48 c# audio wav

我感兴趣的是如何拍音符(例如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);
Run Code Online (Sandbox Code Playgroud)

这会(以某种方式)创建一个恒定的声音 - 但我不完全理解代码如何与结果相关联.

Eri*_*ert 116

你走在正确的轨道上.

我们来看看你的例子:

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

好的,你每秒有11025个样本.你有60秒的样品价值.每个样本是0到255之间的数字,表示在给定时间空间点处的气压的微小变化.

等一下,正弦从-1变为1,因此样本从-256变为+256,并且大于一个字节的范围,所以这里有一些傻瓜.让我们重新编写代码,使样本处于正确的范围内.

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

现在我们可以平滑地改变介于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)); 
Run Code Online (Sandbox Code Playgroud)

注意它必须是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)); 
Run Code Online (Sandbox Code Playgroud)

这可能听起来像废话.发生了什么? 我们再次溢出 ; 在许多点上总和大于256. 将两个波浪的体积减半:

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

更好."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))); 
Run Code Online (Sandbox Code Playgroud)

你去了; 现在,您可以生成任何频率和音量的音调.要制作一个和弦,将它们加在一起,确保你不要太大声并溢出字节.

你怎么知道A220,A440,A880等以外音符的频率?每个半音将上一个频率乘以2的第12个根.因此,计算2的第12个根,将其乘以440,即A#.将A#乘以2的12根,即B.B乘以2的第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))); 
Run Code Online (Sandbox Code Playgroud)

多个音符的结果如何被反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))); 
Run Code Online (Sandbox Code Playgroud)

如果相移为零,显然这没有变化.2π的相移(或π的任何偶数倍)也没有变化,因为sin具有2π的周期.0到2π之间的每个值都会在音调"开始"沿着波形进一步移动的地方移动.

准确地确定正确的相移是有点棘手的.如果你阅读我关于产生"连续下降"Shepard幻觉音调的文章,你会发现我使用了一些简单的微积分来确保一切都在不断变化而没有任何爆炸声.你可以使用类似的技巧来弄清楚正确的转变是什么让流行音乐消失.

我试图找出如何生成phasehift值.是"ArcSin(((新笔记的第一个数据样本) - (前一个笔记的最后一个数据样本))/ noteVolume)"对吗?

那么,要实现的第一件事是,有可能不会一个"正确的价值".如果结尾音符非常响亮并且在峰值结束,并且起始音符非常安静,则新音调中可能没有与旧音调的值匹配的点.

假设有一个解决方案,它是什么?你有一个结束样本,称之为y,你想要找到相移x

y = v * sin(x + i * freq)
Run Code Online (Sandbox Code Playgroud)

当我是零.所以那是

x = arcsin(y / v)
Run Code Online (Sandbox Code Playgroud)

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

正弦波1

而且你想追加

正弦波2

两种可能的相移:

正弦波3

正弦波4

猜测哪个听起来更好听.:-)

弄清楚你是处于波浪的"上冲程"还是"下冲程"可能有点棘手.如果你不想计算出真正的数学,你可以做一些简单的启发式算法,比如"转换时连续数据点之间差异的符号是否会改变?"

技术2:ADSR包络

如果您正在建模听起来像真实乐器的东西,那么您可以通过如下更改音量来获得良好的效果.

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

     /\
    /  \__________
   /              \
  /                \
   A  D   S       R
Run Code Online (Sandbox Code Playgroud)

音量从零开始.然后发生攻击:声音快速上升到其峰值音量.然后它略微衰减到其维持水平.然后它保持在那个水平,可能在音符播放时缓慢下降,然后它又回落到零.

如果你这样做那么没有弹出,因为每个音符的开头和结尾都是零音量.该版本确保了这一点.

不同的乐器有不同的"信封".例如,管风琴具有令人难以置信的短暂攻击,腐烂和释放; 这一切都是持续的,而持续是无限的.您现有的代码就像一个管风琴.比如说钢琴.再一次,短暂的攻击,短暂的衰退,短暂的释放,但声音在维持期间逐渐变得更安静.

攻击,腐烂和释放部分可能非常短,太短而无法听到,但足够长以防止弹出.尝试在音符播放时改变音量,看看会发生什么.

  • @Joan:我已经知道从我的大学时代转换到信号到频域的数学.很多年前,当我收到一架带有挑剔的鲍德温直立动作的旧钢琴时,我对钢琴调音和调节感兴趣.我从未练习过足以擅长钢琴调音,最终我厌倦了将钢琴分开修理它,所以我摆脱了它,让自己成为一个廉价的全新中国钢琴.在了解数学和实际学习如何调整钢琴之间,我已经学会了足够的理论来回答这个问题. (5认同)
  • @Eric:你碰巧有音乐背景吗?或者这是你的硕士论文?:o (4认同)
  • +1哇,很棒的解释!还可以考虑使用[Audacity](http://audacity.sourceforge.net/)查看创建的wav文件.您可以在Audacity中进行FFT以确保频率正确并且您没有任何谐波(即,来自削波). (3认同)
  • 非常明确,深入解释.谢谢你所有的时间!下次有机会我会试试看:-) (3认同)

Mar*_*son 5

你走在正确的轨道上.:)

音频信号

你不需要进行逆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
Run Code Online (Sandbox Code Playgroud)

对于440 Hz,我们得到:

sin(44100*f) = sin(440*tau)
44100*f = 440*tau
f = 440 * tau / 44100 = 440 * 2 * pi / 44100
Run Code Online (Sandbox Code Playgroud)

在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));
Run Code Online (Sandbox Code Playgroud)

注意:我没有对此进行测试以验证代码的正确性.我会尽力做到并纠正任何错误. 更新:我已将代码更新为有效的代码.抱歉伤害了你的耳朵;-)

和弦

和弦是音符的组合(参见维基百科上的小和弦).因此,信号将是具有不同频率的正弦的组合(总和).

纯净的色调

这些音调和和弦听起来并不自然,因为传统乐器不会播放单频音.相反,当您演奏A4时,频率分布很广,浓度约为440 Hz.参见例如Timbre.