如何将从旋律提取算法中得到的音高轨迹转换为类似哼唱的音频信号

19

作为一个在家研究项目的一部分,我正在尝试找到一种将歌曲降噪/转换成哼唱音频信号的方法(即我们听歌时感知到的潜在旋律)。在继续描述我对这个问题的尝试之前,我想提醒一下,尽管我有大量分析图像和视频的经验,但我完全是一个新手,没有音频分析经验。

通过搜索Google,我找到了一堆旋律提取算法。给定一首多声部音频信号(例如.wav文件),它们输出一个音高轨迹——在每个时间点估计主导音高(来自歌手的声音或某种旋律生成器的声音)并跟踪随时间变化的主导音高。

我阅读了几篇论文,它们似乎计算了歌曲的短时Fourier变换,然后在谱图上进行一些分析,以获取和跟踪主导音高。旋律提取只是我正试图开发的系统中的一个组件,所以只要它可以对我的音频文件进行良好的处理,并且代码可用,我就不介意使用任何可用的算法。由于我是新手,我很乐意听取关于哪些算法已知能够很好地工作以及如何找到其代码的任何建议。

我找到了两个算法:

  1. Yaapt音高跟踪
  2. Melodia

我选择了Melodia,因为不同音乐类型的结果看起来非常令人印象深刻。请参见此处查看其结果。你听到的每一首歌的哼唱声基本上就是我感兴趣的内容。

“这个问题我需要你的帮助解决,即对于任意歌曲生成这种哼唱声。”

该算法(作为vamp插件提供)输出一个音高轨迹——[时间戳,音高/频率]——一个Nx2矩阵,在第一列中是时间戳(秒),第二列是在相应时间戳检测到的主导音高。下面是从算法获得的音高轨迹与歌曲时域信号(上方)和其谱图/短时傅里叶变换叠加在一起的可视化。负值的音高/频率代表未发声/非旋律段的算法主导音高估计。因此,所有音高估计>=0都对应于旋律,其余则不重要。

Pitch-track overlay with a song's waveform and spectrogram

现在我想将这个音高轨迹转换回类似哼唱声的音频信号-就像作者在他们的网站上展示的那样。

下面是我编写的MATLAB函数:

function [melSignal] = melody2audio(melody, varargin)
% melSignal = melody2audio(melody, Fs, synthtype)
% melSignal = melody2audio(melody, Fs)
% melSignal = melody2audio(melody)
%
% Convert melody/pitch-track to a time-domain signal
%
% Inputs:
%
%     melody - [time-stamp, dominant-frequency] 
%           an Nx2 matrix with time-stamp in the 
%           first column and the detected dominant 
%           frequency at corresponding time-stamp
%           in the second column. 
% 
%     synthtype - string to choose synthesis method
%      passed to synth function in synth.m
%      current choices are: 'fm', 'sine' or 'saw'
%      default='fm'
% 
%     Fs - sampling frequency in Hz 
%       default = 44.1e3
%
%   Output:
%   
%     melSignal -- time-domain representation of the 
%                  melody. When you play this, you 
%                  are supposed to hear a humming
%                  of the input melody/pitch-track
% 

    p = inputParser;
    p.addRequired('melody', @isnumeric);
    p.addParamValue('Fs', 44100, @(x) isnumeric(x) && isscalar(x));
    p.addParamValue('synthtype', 'fm', @(x) ismember(x, {'fm', 'sine', 'saw'}));
    p.addParamValue('amp', 60/127,  @(x) isnumeric(x) && isscalar(x));
    p.parse(melody, varargin{:});

    parameters = p.Results;

    % get parameter values
    Fs = parameters.Fs;
    synthtype = parameters.synthtype;
    amp = parameters.amp;

    % generate melody
    numTimePoints = size(melody,1);
    endtime = melody(end,1);
    melSignal = zeros(1, ceil(endtime*Fs));

    h = waitbar(0, 'Generating Melody Audio' );

    for i = 1:numTimePoints

        % frequency
        freq = max(0, melody(i,2));

        % duration
        if i > 1
            n1 = floor(melody(i-1,1)*Fs)+1;
            dur = melody(i,1) - melody(i-1,1);
        else
            n1 = 1;
            dur = melody(i,1);            
        end

        % synthesize/generate signal of given freq
        sig = synth(freq, dur, amp, Fs, synthtype);

        N = length(sig);

        % augment note to whole signal
        melSignal(n1:n1+N-1) = melSignal(n1:n1+N-1) + reshape(sig,1,[]);

        % update status
        waitbar(i/size(melody,1));

    end

    close(h);

end

这段代码的基本逻辑如下:在每个时间点,我合成一个短暂的波形(例如正弦波),其频率等于该时间点检测到的主导音调/频率,并持续时间等于它与输入旋律矩阵中下一个时间戳之间的差距。我只是在想是否正确执行了此操作。

然后,我从此函数获得的音频信号并将其与原始歌曲一起播放(旋律在左声道,原始歌曲在右声道)。尽管生成的音频信号似乎相当好地分割了产生旋律的来源(声音/主音器乐)-在声音所在处活动,在其他地方为零-但信号本身远非像作者在他们的网站上展示的哼唱(我得到的是beep beep beeeeep beep beeep beeeeeeeep)。具体而言,下面是显示输入歌曲时间域信号和使用我的函数生成的旋律时间域信号的可视化。

enter image description here

一个主要问题是-尽管我在每个时间戳上都给出了要生成的波的频率和持续时间,但我不知道如何设置波的幅度。目前,我将幅度设置为平坦/恒定值,并且我怀疑这就是问题所在。

有人对此有什么建议吗?我欢迎任何编程语言(最好是MATLAB、Python、C++)的建议,但我想这里我的问题更加普遍——如何在每个时间戳上生成波形?

我脑海中有几个想法/修复方法:

  1. 通过从原始歌曲的时间域信号中获取幅度的平均值/最大估计来设置幅度。
  2. 完全改变我的方法——计算歌曲音频信号的谱图/短时傅里叶变换。截断/零或软地消除所有其他频率,除了在我的音高跟踪中(或接近我的音高跟踪)的那些频率。然后计算反短时傅里叶变换以获得时间域信号。

你可以创建一个MIDI文件,其中包含与你的旋律相同的音高、弯曲音高、持续时间和时间,为其选择一个好听的乐器,并在你选择的程序/库中渲染它。或者,你可以给每个音符一个幅度包络,从强劲开始(要么从0迅速建立,要么开始强劲),逐渐减少到安静的保持量,并在结尾处逐渐消失。这被称为ADSR包络。 - Patashu
我的新家庭项目——作为一个边桌——在某种程度上并不令人印象深刻。至少让我感觉像石器时代的伊戈尔(开个玩笑)。 - Burhan Khalid
4个回答

5
如果我理解正确,你似乎已经准确地表示了音高,但问题在于你所生成的声音并不“足够好听”。
从您的第二种方法开始:仅过滤音高之外的内容不会带来任何好处。通过仅删除与本地音高估计相对应的少数频率频段,您将失去输入信号的纹理,这就是使其声音好听的东西。实际上,如果您将其推向极端,并删除除音高之外的所有内容,仅保留与音高对应的一个样本并进行ifft,则会得到完全的正弦波,这就是您目前正在做的事情。 如果您仍然想这么做,我建议您仅通过将滤波器应用于时间信号而不是进出频域来执行所有这些操作,因为后者更昂贵和繁琐。该滤波器将具有围绕您要保留的频率的小截止频率,这也将允许具有更好纹理的声音。
但是,如果您已经拥有您满意的音高和持续时间估计,但希望改善声音渲染,我建议您用一些实际的哼唱(或小提琴或长笛或您喜欢的任何东西)替换您的正弦波,无论您如何磨练它们,它们始终会听起来像傻傻的哔哔声。如果内存是个问题或者您所代表的歌曲不属于一个良好的均衡音阶(例如中东歌曲),则可以仅为几个频率设置哼唱样本,而不是为音阶的每个音符都有一份哼唱样本。然后,您将通过从这些哼唱样本之一进行采样率转换来推导任何频率的哼唱声音。为进行采样转换选择几个样本,将允许您选择与您需要产生的频率“最佳”比率倾向的样本,因为采样转换的复杂性取决于该比率。显然,相对于仅拥有一组可供选择的样本,添加采样率转换将需要更多的工作和计算。
使用真实样本的库将在您渲染的质量上产生很大的差异。这还将使您能够为您播放的每个新音符具有逼真的攻击。
然后,是的,像您建议的那样,您可能还想通过遵循输入信号的瞬时幅度来播放振幅,以产生更加细腻的歌曲渲染。
最后,我还会尝试调整您的持续时间估计,以使从一个声音到另一个声音的过渡更加平滑。根据我非常喜欢的音频文件的表现(哔哔bee beeep beeeeeeeep)和您显示的图表猜测,看起来您在歌曲的渲染中插入了许多中断。通过延长持续时间估计以消除任何短于0.1秒的静默,您可以避免切断您歌曲的每个音符,同时保留原始歌曲中的真正的静默。

非常感谢您的批评和建议。问题正如salamon所指出的那样。我没有确保连续的正弦波之间连续,从而在它们之间引入了一个尖锐的不连续性。因此,信号听起来不正确。修复后,声音好多了。 - cdeepakroy
关于用来自真实乐器的样本库取代正弦波以获得更逼真的信号,我想提一点。是否有这样的样本库可用,或者是否存在任何乐器的数学模型呢?显然,一个乐器声音的独特之处在于泛音或谐波——也就是基频的整数倍。我读过一些资料,说衡量乐器声音特征的是这些谐波振幅与基频 f 之间的比例。而音高轨道似乎是一系列连续的频率,而非离散的音符。 - cdeepakroy
@cdeepakroy 是和不是。是的,你提到的这些比例在同类型乐器中往往相似,但它们仍然因具体乐器而异,在每个音符内部以及弹奏音符的力度上也会有所变化。而且,能量 tends 环绕基频的整数倍积聚,但绝不容易被简化为它们:这幅插图说明了一切:http://kozco.com/tech/audacity/piano_G1.jpg。所以如果手头没有样本库,添加谐波肯定会听起来更好,但仍然远非自然之音。 - Lolo
@cdeepakroy,我理解了你的第一条评论:尖锐的不连续性是致命的。我没有(现在仍然没有)看到你原始帖子中描述的问题。值得一提的是,许多声音编辑工具使用另一种方法来解决这个问题:而不是调整相位(通常只有在接近正弦信号时才可能),它们使用交叉淡入淡出(快速降低前一个信号的幅度并逐渐增加新信号的幅度)。 - Lolo

3
尽管我无法访问您的synth()函数,但根据其参数,我认为您的问题是因为未处理相位。也就是说,仅仅将波形片段连接在一起是不够的,您必须确保它们具有连续的相位。否则,每次连接两个波形片段时,都会在波形中创建不连续性。如果是这种情况,我猜您听到的频率始终相同,并且听起来更像锯齿波而不是正弦波 - 我对吗?
解决方法是将片段n的起始相位设置为片段n-1的结束相位。以下是一个示例,演示如何连接具有不同频率的两个波形,而不会创建相位不连续性:
fs = 44100; % sampling frequency

% synthesize a cosine waveform with frequency f1 and starting additional phase p1
p1 = 0;
dur1 = 1;
t1 = 0:1/fs:dur1; 

x1(1:length(t1)) = 0.5*cos(2*pi*f1*t1 + p1);

% Compute the phase at the end of the waveform
p2 = mod(2*pi*f1*dur1 + p1,2*pi);

dur2 = 1;
t2 = 0:1/fs:dur2; 
x2(1:length(t2)) = 0.5*cos(2*pi*f2*t2 + p2); % use p2 so that the phase is continuous!

x3 = [x1 x2]; % this should give you a waveform without any discontinuities

请注意,尽管这给您提供了一个连续的波形,但频率转换是瞬间的。如果您希望从time_n到time_n+1逐渐改变频率,则必须使用McAulay-Quatieri插值等更复杂的方法。但无论如何,如果您的片段足够短,这应该听起来足够好。
关于其他评论,如果我理解正确,您的目标只是要能够听到频率序列,而不是让其听起来像原始源。在这种情况下,振幅并不重要,您可以保持它固定。
如果您想让它听起来像原始源,那就完全不同了,可能超出了此讨论的范围。
希望这回答了您的问题!

@justin 非常感谢你的解决方案。这确实是问题所在,在修复后听起来好多了。但我觉得我需要插入更好的振幅才能更加逼真,但这与我在这篇文章中提出的问题略有偏差。我读到,感知振幅取决于频率(频率越高,感知振幅越高)。我想知道是否可以找到一些数学模型来描述这种依赖关系,以便我可以根据主导音高/频率改变振幅。也许那样会听起来更好。 - cdeepakroy
另外,您能否详细介绍一下McAulay-Quatieri插值方法,或者给我指一篇更简单的文章——即使在应用了一些平滑处理之后,在信号从有声到无声的转换点仍然听到一些啁啾声。 - cdeepakroy
1
改变正弦波的振幅只会对合成信号的感知真实性产生微小影响 - 它仍然基本上听起来像单一的正弦波。如果你想让它听起来像原始源,则有两个选择:要么获取一个用于源(声音/乐器)的合成器并使用f0序列来指导合成,要么使用源分离算法而不是f0估计算法来直接分离主导源的信号(至少尝试这样做,这仍然是一个开放的研究问题)。 - jjs
对于源分离,您可以尝试Melodyne(我认为有免费试用版),或者J.-L. Durrieu的代码,如果您正在寻找开源软件:http://www.durrieu.ch/research/jstsp2010.htmlMcAulay-Quatieri插值仅适用于在两个非零频率和幅度值(a1,f1)-->(a2,f2)之间进行插值,它无法帮助您在语音/非语音之间的边界上。对于这种情况,您应该只需平滑攻击即可。无论如何,由于您使用的是单个正弦波(纯调性),攻击永远不会听起来完全“自然”。 - jjs

1

您至少有两个问题。

首先,正如您所推断的那样,您的分析舍弃了原始频谱中旋律部分的所有振幅信息。您需要一种能够捕获该信息的算法(不仅仅是多声部输入的整个信号的振幅,或任何自然音乐声音的FFT音高频率点的振幅)。这是一个非平凡的问题,位于旋律音高提取和盲源分离之间。

其次,声音具有音色,包括泛音和包络,在恒定频率下也有。您的合成方法只创建了单个正弦波,而哼唱可能会产生许多更有趣的泛音,包括许多高于基频的更高频率。为了获得略微更自然的声音,您可以尝试分析自己哼唱单个音高的频谱,并尝试重新创建所有这些十几个泛音正弦波,而不仅仅是一个,并且在分析时为每个频率时间戳合适的相对振幅进行合成。您还可以查看自己哼唱短音符的随时间变化的振幅包络,并使用该包络来调制合成器的振幅。


“分析自己/人类哼唱单音的频谱”的建议很有趣。但我相信我只能做到这一点,可能只对几个八度内的音符进行分析。然后我需要一个数学模型,它将允许我以插值或外推的方式获得任何连续频率的信号,这正是旋律提取算法的音高轨迹所给出的。这样的数学模型是否存在?我对这个领域还不熟悉,如果您能指向相关文献,那就太好了。” - cdeepakroy
关于声音共振峰的文献可能涵盖了您想尝试的一些模型。 - hotpaw2

0

使用 libfmp.c8 将数值转换为声音

import IPython.display as ipd
import libfmp.b
import libfmp.c8
data = vamp.collect(audio, samplerate, "mtg-melodia:melodia", parameters=params)
hop, melody = data['vector']
timestamps=np.arange(0,len(melody)) * float(hop)
melody_pos = melody[:]
melody_pos[melody<=0] = 0   #get rid off - vals
d = {'time': ts, 'frequency':pd.Series(melody_pos) }
df=pd.DataFrame(d)
traj = df.values
x_traj_mono = libfmp.c8.sonify_trajectory_with_sinusoid(traj, len(audio), sr, smooth_len=50, amplitude=0.8)
ipd.display(ipd.Audio(x_traj_mono+y, rate=sr))```

1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接