JavaScript 中的鼓合成

2021-07-29 21:47:36

很长一段时间以来,编写小型鼓合成器的想法一直是“如果……也许会很酷”的任务之一,这些任务漂浮在我的 Creative Intent 插件原型的外围或我的生成音乐小实验中。通常我会推迟这个想法,认为这会花费更多的时间而不是值得。但是,现在我可以使用 Elementary,我发现像这样的小实验或目标可以是半天的项目,有些甚至比这更短。因此,我最近着手为 Elementary 构建一个小型免费鼓合成器库,以最终解决这个问题,在本文中,我想分享一个简短的教程,介绍我如何进行电子鼓、拍手和嗨帽声音的合成.如果您想听听示例,只需克隆存储库、npm install 和 npm start。最后,在我们深入探讨之前,我想说明一下,我在这里为每种声音采取的方法非常简单。我确信有更复杂、也许更准确的方法来合成这些声音,但我采用的方法使我得到了一组我对自己的音乐实验感到满意的声音。电子底鼓由一些非常简单的元素组成。首先,我们将低音调调谐到所需频率,并赋予底鼓真正的“肉感”。然后我们在声音的开头有咔嗒声,它松散地模拟了当木槌敲击原声底鼓膜时产生的接触声音。这通常是在底鼓合成中使用非常快的音高包络将亚音从较高频率扫到较低频率来实现的。这些组件单独构成了许多电子底鼓的完整构建块,但在我的中,我也想要一点垃圾,考虑扭曲的 808 风格底鼓敲击声。为了将这些组件具体转化为我们的合成方法,我们从超低音开始,为此我使用了用户指定频率音高的简单正弦音,通常在 30-120hz 范围内调谐。然后我们可以使用幅度包络应用轮廓,该幅度包络非常快速地上升到最大值(例如,5ms – 400ms),然后在此后不久(5ms – 4s)衰减回 0。函数踢(音高,攻击,衰减,门){让 env = el.adsr(攻击,衰减,0.0,0.1,门); return el.mul(env, el.cycle(pitch));} 在这里,我们的底鼓函数将被使用,这样传入的门参数是一个脉冲串,它在某个用户定义的值 0 和 1 之间交替速度。在上升沿(从 0 到 1 的过渡),我们的包络将接合,触发底鼓声音。现在为了给我们的底鼓一个很好的类似咔嗒声的瞬态,我们应用另一个包络,这次是在亚音的音高上,将亚音的频率从更高的值扫下来。这里的调整是为了品味,我在短时间内从用户给定值的 5 倍运行音调到自身的音调。

function kick(pitch, click,attack,decay,gate) { // 首先我们有我们的放大器包络 let env = el.adsr(attack,decay, 0.0, 0.1, gate); // 然后我们有一个音高包络,在 [5ms, 4s] 中具有 0 起音和衰减​​。 // `el.adsr` 节点使用指数段,这对我们的目的非常有用 // 在这里,但您也可以通过平方 // 或取 pitchenv 信号的平方根来或多或少地对曲线进行加权。让 pitchenv = el.adsr(0.005, click, 0.0, 0.1, gate); // 然后我们的合成:在我们的基本音高处的正弦音,其频率很快 // 由 pitchenv 调制以在 `click` 秒内从 5*pitch 扫到 1*pitch。 // 产生的声音直接通过我们的放大器包络相乘。 return el.mul( env, el.cycle( el.mul( el.add(1, el.mul(4, pitchenv)), pitch, ) ) );到目前为止,我们的底鼓声音已经基本完成,但我想为亚音添加一些额外的泛音,并使用波形整形器将底鼓驱动到更粗糙的区域。为此,我们只需将之前的声音通过用户指定的驱动器通过 el.tanh 饱和器。完整的踢腿合成器如下所示。 function kick(pitch, click,attack,decay,drive,gate) { // 首先我们有我们的放大器包络 let env = el.adsr(attack,decay, 0.0, 0.1, gate); // 然后我们有一个音高包络,在 [5ms, 4s] 中具有 0 起音和衰减​​。 // `el.adsr` 节点使用指数段,这对我们的目的非常有用 // 在这里,但您也可以通过平方 // 或取 pitchenv 信号的平方根来或多或少地对曲线进行加权。 let pitchenv = el.adsr(0.005, click, 0.0, 0.1, gate); // 然后我们的合成:在我们的基本音高处的正弦音,其频率很快 // 由 pitchenv 调制以在 `click` 秒内从 5*pitch 扫到 1*pitch。 // 产生的声音直接通过我们的放大器包络相乘。 let clean = el.mul( env, el.cycle( el.mul( // 音高包络从 5 的乘数到 // 原始音高的 1 的乘数 el.add(1, el.mul(4) , pitchenv)), 音高, ) ) ); // 然后你可以把它驱动到一个带有 [1, 10] 增益乘法器的软限幅器中 return el.tanh(el.mul(clean, drive));} 我相信合成拍手声音可以非常详细如果你想挖得足够深,就努力吧。但我采取的方法可能是我能想到的最简单的方法,我对结果非常满意。首先,我们会注意到单个拍手声实际上只是一个瞬态,其大部分共振衰减在 400Hz – 3500Hz 频率范围内。我们可以简单地使用过滤的白噪声和轮廓的幅度包络来处理这样的事情。函数拍手(音调,攻击,衰减,门){ let no = el.noise();让 e1 = el.adsr(攻击,衰减,0.0,0.1,门); return el.bandpass(tone, 1.214, el.mul(no, e1), );} 就像上面的底鼓一样,我们的拍手功能将在脉冲串信号的上升沿触发。现在,到目前为止一切都很好,但是鼓掌的有趣特征来自于几个这样的瞬态的分层,就好像几个人试图同时鼓掌一样。真正产生差异的是时间上的细微变化。所以,这里我们简单地采用上述方法,将其复制四次,并稍微抵消攻击和衰减。最后,为了最后一点额外的字符,我已经通过另一个 el.tanh 饱和器运行了结果。

function clap(tone,attack,decay,gate) { // 分层白噪声合成 let no = el.noise();让 e1 = el.adsr(el.add(0.035, 攻击), el.add(0.06, 衰减), 0.0, 0.1, 门);让 e2 = el.adsr(el.add(0.025, 攻击), el.add(0.05, 衰减), 0.0, 0.1, 门);让 e3 = el.adsr(el.add(0.015, 攻击), el.add(0.04, 衰减), 0.0, 0.1, 门);让 e4 = el.adsr(el.add(0.005, 攻击), el.add(0.02, 衰减), 0.0, 0.1, 门); // 然后我们通过根据音调设置的带通滤波器运行结果 // 在 400Hz 和 3500Hz 之间,并且稍微饱和。返回 el.tanh( el.bandpass(tone, 1.214, el.add( el.mul(no, e1), el.mul(no, e2), el.mul(no, e3), el.mul(no, e4), ), ) );} 合成 hi hat 声音最终成为我在这个小项目上花费大部分时间的地方。就像鼓掌一样,一开始的想法似乎很明显,即踩镲,尤其是封闭的踩镲,只不过是一种瞬态,通常位于频谱的较高部分。但只是使用过滤的噪音来达到我觉得缺乏帽子的金属特征。在研究了其他一些著名的鼓合成器(例如经典的 TR-808 或 DR110)之后,我发现高音方波可以成为我正在寻找的角色的良好构建块。然后,通过一些额外的探索,我得出了一种方法,该方法使用带有单个正弦调制器和单个正弦载波的 FM 合成(通过相位调制)。调制器以恰好两倍于载波的速率运行,以生成方波的奇次谐波。然后,为了引入一些非谐波噪声特性,我使用了第二个调制器,这次是白噪声信号调制第一个调制器的频率。 FM 合成如下所示。 /** 具有相位偏移的正弦波振荡器的快速助手。 */function cycle(freq, phaseOffset) { let t = el.add(el.phasor(freq), phaseOffset);让 p = el.sub(t, el.floor(t)); return el.sin(el.mul(2 * Math.PI, p));}/** 我们的帽子合成器 */function hat(pitch) { // 合成器 let m2 = el.noise();让 m1 = cycle(el.mul(2, pitch), el.mul(2, m2));让 m0 = cycle(pitch, el.mul(2, m1)); return m0;} 这里,给帽子函数的音高决定了载波正弦音的基频,用户可以调整它来调整帽子的金属特性。现在,正如所写,我们缺少幅度包络,因此无法触发我们帽子的离散实例。让我们将其添加到:function hat(pitch,attack,decay,gate) { // Synthesis let m2 = el.noise();让 m1 = cycle(el.mul(2, pitch), el.mul(2, m2));让 m0 = cycle(pitch, el.mul(2, m1)); // 带有 [5ms, 200ms] 起音和 [5ms, 4000ms] 衰减的放大器包络 let env = el.adsr(attack,decay, 0.0, 0.1, gate); return el.mul(m0, env);} 最后,特别是由于白噪声调制器,我们在中低频范围内得到了很多我们不需要的频率内容,而且我们的帽子声音也不想要,所以我们可以像拍手合成器一样通过带通滤波器运行上面的合成器。这里我们引入了tone参数来设置滤波器的截止频率,由用户调整。

function hat(pitch,tone,attack,decay,gate) { // 合成 let m2 = el.noise();让 m1 = cycle(el.mul(2, pitch), el.mul(2, m2));让 m0 = cycle(pitch, el.mul(2, m1)); // 然后我们通过根据音调设置的带通滤波器运行结果 // 在 800Hz 和 18kHz 之间。让 f = el.bandpass(tone, 1.214, m0); // 最后,我们得到了在 [5ms, 200ms] 内具有起音和在 [5ms, 4000ms] 内衰减的放大器包络 let env = el.adsr(attack,decay, 0.0, 0.1, gate); return el.mul(f, env);} 我们有!当然,您也可以在 GitHub 上找到帽子合成器。最后,我很高兴地与大家分享,上面介绍的所有声音都可以在您自己的项目中免费使用,并且现在可以在 npm 上使用。首先,您可以简单地将 @nick-thompson/drumsynth 添加到您的项目中: 最后,一个简短的使用示例,其中包含在脉冲串的每一步上播放的踢腿、拍手和帽子: const core = require('elementary -core');const el = require('@nick-thompson/elementary');const ds = require('@nick-thompson/drumsynth');// 最简单的例子:踢腿、帽子和拍手// 以 4Hz.core.on('load', function() { let gate = el.train(4); core.render( el.add( ds.kick(40, 0.104, 0.005, 0.4, 4, 门), ds.clap(800, 0.005, 0.204, 门), ds.hat(317, 12000, 0.005, 0.1, 门), ), ););