你在正确的轨道上。
让我们看看你的例子:
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 之间平滑变化的数据,所以我们在一个字节的范围内。
试一试,看看听起来如何。听起来应该更“流畅”。
人耳可以检测到气压的极其微小的变化。如果这些变化形成重复模式,那么你耳朵中的耳蜗会将模式重复的频率解释为特定音调。压力变化的size被解释为volume。
你的波形是六十秒长。变化从最小变化 1 到最大变化 255。峰值在哪里?也就是说,样本在哪里达到或接近 255?
嗯,正弦在 π/2、5π/2、9π/2、13π/2 等处为 1。因此,每当我接近其中之一时,峰值就出现了。也就是说,在 2、8、14、20、...
它们在时间上相距多远?每个样本为 1/11025 秒,因此每个峰值之间的峰值约为 2π/11025 = 约 570 微秒。每秒有多少个峰值? 11025/2π = 1755 赫兹。 (赫兹是频率的量度;每秒有多少个峰值)。 1760 Hz 比 A 440 高两个八度,所以这是一个稍微平坦的 A 音。
和弦是如何工作的?他们是球场的平均值吗?
没有。一个和弦是 A440 和一个八度以上,A880 不等于 660 Hz。你没有平均 音高。你总结 波形。
想想气压。如果您有一个振动源以每秒 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# 乘以 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)));
如何将多个音符的结果进行逆 FFT 转换为字节数组,从而构成 wav 文件中的数据?
反向 FFT 只是构建正弦波并将它们相加,就像我们在这里所做的那样。就是这样!
与此相关的任何其他相关信息?
请参阅我关于该主题的文章。
http://blogs.msdn.com/b/ericlippert/archive/tags/music/
第一到第三部分解释了为什么钢琴每个八度有十二个音符。
第四部分与您的问题相关;这就是我们从头开始构建 WAV 文件的地方。
请注意,在我的示例中,我每秒使用 44100 个样本,而不是 11025,并且我使用范围从 -16000 到 +16000 的 16 位样本,而不是范围从 0 到 255 的 8 位样本。但除此之外细节,和你的基本一样。
如果您要制作任何类型的复杂波形,我建议您使用更高的比特率;每秒 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 错觉音调的文章,您会看到我使用了一些简单的微积分来确保一切都在不断变化而没有任何爆音。您可以使用类似的技术来找出使弹出消失的右移是什么。
我正在尝试研究如何生成相移值。 “ArcSin(((新笔记的第一个数据样本)-(上一个笔记的最后一个数据样本))/noteVolume)”对吗?
嗯,首先要意识到的是,可能没有一个“正确的价值”。如果结尾音符非常响亮并在一个峰值处结束,而起始音符非常安静,则新音调中可能没有与旧音调值相匹配的点。
假设有一个解决方案,它是什么?你有一个结束样本,叫它 y,你想找到相移 x 使得
y = v * sin(x + i * freq)
当 i 为零时。那就是
x = arcsin(y / v)
但是,这可能不太对!假设你有
你想追加
有两种可能的相移:
和
大胆猜测哪个听起来更好。 :-)
弄清楚您是处于波浪的“上行”还是“下行”可能有点棘手。如果您不想计算出真正的数学,您可以做一些简单的启发式方法,例如“连续数据点之间的差异符号在转换时是否发生了变化?”
技巧 2:ADSR 信封
如果您正在建模听起来像真实乐器的东西,那么您可以通过如下改变音量来获得良好的效果。
您要做的是为每个音符设置四个不同的部分,称为起音、衰减、延音和释音。在乐器上演奏的音符的音量可以这样建模:
/\
/ \__________
/ \
/ \
A D S R
音量从零开始。然后攻击发生:声音迅速上升到峰值音量。然后它会稍微衰减到维持水平。然后它保持在那个水平,可能在音符播放时缓慢下降,然后释放回零。
如果您这样做,则不会出现爆音,因为每个音符的开头和结尾都是零音量。该版本确保了这一点。
不同的乐器有不同的“信封”。例如,管风琴的起音、衰减和释放非常短。都是延音,延音是无限的。您现有的代码就像管风琴。与钢琴相比。同样,短起音、短衰减、短释音,但在延音过程中声音确实会逐渐变小。
起音、衰减和释音部分可以很短,短到听不见,但长到足以防止爆音。尝试在音符播放时改变音量,看看会发生什么。