如何实现细粒度合成(音调移位器)



由于音乐速度和音高是耦合在一起的,如果我加快音乐速度,音高也会增加。相反,如果我放慢音乐的节奏,音高也会降低。

然而,我看到使用颗粒合成,我可以解耦速度和间距。所以,我目前正在努力实现细粒度合成。

首先,我认为我成功地实现了双速和半速,而球场是一样的。代码如下:

※粒度为2000。这意味着我使用0.04毫秒的声音作为一个颗粒。(2000个样本*1 s/44100个样本=0.04s=40ms)

// get music
const $fileInput = document.createElement('input');
$fileInput.setAttribute('type', 'file');
document.body.appendChild($fileInput);
$fileInput.addEventListener('change', async (e) => {
const music = await $fileInput.files[0].arrayBuffer();
const actx = new (window.AudioContext || window.webkitAudioContext)({ latencyHint: 'playback', sampleRate: 44100 });
const audioData = await actx.decodeAudioData(music);
const original = audioData.getChannelData(0);
const arr = [];
const grainSize = 2000;
// Please choose one code out of double speed code or half speed code
// copy and paste audio processing code here
});
// double speed
// ex: [0,1,2,3, 4,5,6,7, 8] => [0,1, 4,5, 8] discard 2 items out of 4 items
for (let i = 0; i < original.length; i += grainSize) {
if (original[i + (grainSize / 2) - 1] !== undefined) {
for (let j = 0; j < grainSize / 2; j++) {
arr.push(original[i + j]);
}
} else {
for (let j = i; j < original.length; j++) {
arr.push(j);
}
}
}
// half speed
// ex: [0,1, 2,3, 4] => [0,1,0,0, 2,3,0,0, 4,0,0] add 'two' zeros after every 'two' items
for (let i = 0; i < original.length; i += grainSize) {
if (original[i + grainSize - 1] !== undefined) {
for (let j = 0; j < grainSize; j++) {
arr.push(original[i + j]);
}
} else {
for (let j = i; j < original.length; j++) {
arr.push(original[j]);
}
}
for (let j = 0; j < grainSize; j++) {
arr.push(0);
}
}
// play sound
const f32Arr = Float32Array.from(arr);
const audioBuffer = new AudioBuffer({ length: arr.length, numberOfChannels: 1, sampleRate: actx.sampleRate });

audioBuffer.copyToChannel(f32Arr, 0);
const absn = new AudioBufferSourceNode(actx, { buffer: audioBuffer });

absn.connect(actx.destination);
absn.start();

但问题是,我完全不知道如何实现变桨器(即不同的变桨,相同的速度)

在我看来,相同的速度意味着相同的AudioBuffer大小。因此,我手中唯一的变量是粒度。但我真的不知道该怎么办。如果你能分享一些你的知识,我将不胜感激。非常感谢!


致Phil Freihofner

你好,谢谢你的友好解释。我试过你的方法。据我所知,您的方法是一个执行以下操作的过程:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // input data (10 samples)
→ [0, 2, 4, 6, 8] // double speed, 1 octave high (sampling interval: 2)
→ [0, 0, 2, 2, 4, 4, 6, 6, 8, 8] // change duration

结果听起来1个八度音阶高,持续时间相同(成功的音高转换)。然而,我不知道如果我以1.5的采样间隔对输入数据进行采样,该怎么办?我的意思是,我不知道如何使[0, 1, 3, 4, 6, 7, 9]的长度与输入数据的长度相同。

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // input data
// from
[0, 1, 3, 4, 6, 7, 9] // sampling interval: 1.5
// to
[?, ?, ?, ?, ?, ?, ?, ?, ?, ?]

同时,我了解到音高转换可以通过一种方式实现,据我所知,这种方式如下:

  • [1]使每个颗粒在与原始源相同的位置启动
  • [2] 以不同的速度播放每个颗粒

此外,我发现如果我像下面这样转换输入数据,我可以实现音高偏移和时间拉伸:

input data = [0, 1, 2, 3, 4, 5, 6, 7]
grain size = 4
<pitch shifting>
in case of p = 2
result = [0, 2, 0, 2, 4, 6, 4, 6] // sounds like one octave high
// If I remember correctly, [0, 2, 0, 0, 4, 6, 0, 0] is also fine
// (and it is more fit to the definition above ([1] and [2])
// but the sound was not good (stuttering).
// I found that [0, 2, 0, 2...] is better.
in case of p = 1.5
result = [0, 1, 3, 0, 4, 5, 7, 4]
in case of p = 0.5
result = [0, 0, 1, 1, 4, 4, 5, 5] // sounds like one octave low
<time stretching>
in case of speed = 2
result = [0, 1, 4, 5]
in case of speed = 1.2
result = [0, 1, 2, 4, 5, 6]
in case of speed = 0.5
result = [0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 4, 5, 6, 7]
// If I remember correctly, [0, 1, 2, 3, 0, 0, 0, 0...] is also fine
// but the sound was not good (stuttering)
in case of speed = 0.75
result = [0, 1, 2, 3, 0, 4, 5, 6, 7, 4]

不管怎样,谢谢你的回答。

我还没有仔细阅读您的代码来具体评论它,但我可以评论如何实现音高转换的一般理论。

颗粒通常被赋予体积封套,具有淡入和淡出。我已经看到了Hann函数(Hanning Window)作为一种可能性。此外,颗粒是重叠的,窗口形成了交叉渐变。

假设一个颗粒是2000帧,但有窗口。如果你每1000帧制作一个颗粒,并以相同的间隔(每1000帧)重叠播放,你应该能听到相当于原始声音的声音。

改变重叠颗粒之间的播放距离是如何实现声音的不同时间长度的。例如,与其每1000帧播放一个颗粒,不如使用900或1100。

我很确定有一些因素需要考虑,比如窗户的大小和形状,以及颗粒之间可能的间隔范围,但我还没有做到。我对此进行了简单的实验,主要是用Java进行的,但在播放过程中会出现一些人为因素。

我认为在StackOverflow的信号处理网站咨询是获得更多细节信息的好选择。

编辑:我刚刚意识到我误解了你的问题!你问的是在保留声音播放时间的同时改变音高。我不知道这是不是";最好的";方式,但我会考虑分两步来做这件事的计划。首先,将声音更改为所需的音高。然后,将新声音的持续时间更改为原始声音的持续时长。

第一步可以用线性插值来完成。我在前面的一个问题中试图解释如何做到这一点。在第二步中,我们将转换后的波分解成颗粒。

然而,我刚刚注意到,Spektre在那篇帖子上有一个额外的答案,它直接使用你的问题,通过FFT。这可能是一个更好的方法,但我自己还没有尝试过实现它。

编辑2,回答添加到OP:的问题

给定10帧的PCM数据如下[0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9](我使用的是-1到1的带符号浮点。您可能需要将其缩放以转换为您的格式。)

为了将播放的音高更改为1.5x(但也更改长度),我们得到以下数据:[0,0.15,0.3,0.45,0.6,0.75,0.9]

0.15是通过线性插值得出的介于点0.1和0.2之间的值。如果速度为1.25倍,数据点如下:[0,0.125,0.25,0.375,0.5,0.625,0.75,0.875,?(取决于下面的0.9)]

序列中索引1的线性插值计算如下:pitchShiftedAudioData1=原始PCM1*(1-0.25)+原始PCM2*0.75;

换言之,由于我们降落在原始PCM1和原始PCM2之间0.25的点上,因此上面计算了如果数据从1线性前进到2,该值将是多少。

在完成所有这些之后,仍然需要额外的步骤来将间距偏移的数据形成颗粒。必须对每个颗粒使用窗口函数。如果窗口只有10帧长(太短,但会说明),则可能的窗口如下:[0.01,0.15,0.5,0.85,1,0.85,0.5,0.15,0.01]。(实际上,它应该遵循Hann函数。)

这被应用于来自不同起点的数据,以创建颗粒,其中N是信号阵列中的索引。

[信号[N]*window[0],信号[N+1]*window1,信号[N+2]*window2。。。,信号[N+10]*窗口[10]]

为了创建新的信号,将得到的颗粒依次放置、重叠并求和。颗粒的相对位置(距离远近)决定了时间。这是我对用蛮力完成时间转换的天真理解,我已经取得了一些不错但不太好的结果。

我希望这能澄清我试图描述的内容!

如果您无法理解,请考虑取消勾选此项作为答案。其他人可能会参与进来,从而能够提供更容易理解的信息或更正。

时间偏移是非常先进的,IMHO,所以期待一些复杂的计算和编码(除非有人推荐工具)。

最新更新