根据音色(音调)按相似度对声音进行排序

Sort sounds by similarity based on timbre(tone)

说明

我希望能够根据声音的 音色(音调) 对列表中的一组声音进行排序。这是一个玩具示例,我手动对我创建的 12 个声音文件和 uploaded to this repo 的频谱图进行排序。我知道这些排序正确,因为每个文件产生的声音与之前文件中的声音完全相同,但添加了一种效果或过滤器。

例如,声音 xyz 的正确排序,其中

会是x, y, z

仅通过查看声谱图,我可以看到一些视觉指标,提示应该如何对声音进行排序,但我希望通过让计算机识别此类指标来自动化排序过程。


上图中声音的声音文件

我希望即使所有这些条件都正确,我的排序也能正常工作(但我会接受最佳答案,即使它不正确'解决这个问题)

例如下图中

如果第一张图片中的 MFCC_8 和 MFCC_9 替换为下图中的 MFCC_8 和 MFCC_9,我希望声音的排序保持不变完全一样。

对于我的真实程序,我打算通过声音变化来分解一个mp3文件like this


我目前的计划

这是生成 first image in this post. I need the code in the function sort_sound_files to be replaced with some code that actually sorts the sound files based on timbre. The part which needs to be done is near the bottom and the sound files on on this repo. I also have this code in a jupyter notebook 的程序,其中还包括第二个示例,它与我实际希望此程序执行的操作更相似

import librosa
import librosa.display
import matplotlib.pyplot as plt
import numpy as np
import math
from os import path
from typing import List


class Spec:
    name: str = ''
    sr: int = 44100


class MFCC(Spec):

    mfcc: np.ndarray  # Mel-frequency cepstral coefficient
    delta_mfcc: np.ndarray  # delta Mel-frequency cepstral coefficient
    delta2_mfcc: np.ndarray  # delta2 Mel-frequency cepstral coefficient
    n_mfcc: int = 13

    def __init__(self, soundFile: str):
        self.name = path.basename(soundFile)
        y, sr = librosa.load(soundFile, sr=self.sr)
        self.mfcc = librosa.feature.mfcc(y, n_mfcc=self.n_mfcc, sr=sr)
        self.delta_mfcc = librosa.feature.delta(self.mfcc, mode="nearest")
        self.delta2_mfcc = librosa.feature.delta(self.mfcc, mode="nearest", order=2)


def get_mfccs(sound_files: List[str]) -> List[MFCC]:
    '''
        :param sound_files: Each item is a path to a sound file (wav, mp3, ...)
    '''
    mfccs = [MFCC(sound_file) for sound_file in sound_files]
    return mfccs


def draw_specs(specList: List[Spec], attribute: str, title: str):
    '''
        Takes a list of same type audio features, and draws a spectrogram for each one
    '''
    def draw_spec(spec: Spec, attribute: str, fig: plt.Figure, ax: plt.Axes):
        img = librosa.display.specshow(
            librosa.amplitude_to_db(getattr(spec, attribute), ref=np.max),
            y_axis='log',
            x_axis='time',
            ax=ax
        )
        ax.set_title(title + str(spec.name))
        fig.colorbar(img, ax=ax, format="%+2.0f dB")

    specLen = len(specList)
    fig, axs = plt.subplots(math.ceil(specLen/3), 3, figsize=(30, specLen * 2))
    for spec in range(0, len(specList), 3):

        draw_spec(specList[spec], attribute, fig, axs.flat[spec])

        if (spec+1 < len(specList)):
            draw_spec(specList[spec+1], attribute, fig, axs.flat[spec+1])

        if (spec+2 < len(specList)):
            draw_spec(specList[spec+2], attribute, fig, axs.flat[spec+2])


sound_files_1 = [
    '../assets/transients_1/4.wav',
    '../assets/transients_1/6.wav',
    '../assets/transients_1/1.wav',
    '../assets/transients_1/11.wav',
    '../assets/transients_1/13.wav',
    '../assets/transients_1/9.wav',
    '../assets/transients_1/3.wav',
    '../assets/transients_1/7.wav',
    '../assets/transients_1/12.wav',
    '../assets/transients_1/2.wav',
    '../assets/transients_1/5.wav',
    '../assets/transients_1/10.wav',
    '../assets/transients_1/8.wav'
]
mfccs_1 = get_mfccs(sound_files_1)


##################################################################
def sort_sound_files(sound_files: List[str]):
    # TODO: Complete this function. The soundfiles must be sorted based on the content in the file, do not use the name of the file

    # This is the correct order that the sounds should be sorted in
    return [f"../assets/transients_1/{num}.wav" for num in range(1, 14)]  # TODO: remove(or comment) once method is completed
##################################################################


sorted_sound_files_1 = sort_sound_files(sound_files_1)
mfccs_1 = get_mfccs(sorted_sound_files_1)

draw_specs(mfccs_1, 'mfcc', "Transients_1 Sorted MFCC-")
plt.savefig('sorted_sound_spectrograms.png')

编辑

我后来才意识到这一点,但另一件非常重要的事情是会有很多属性在波动。例如,第一组声音 5 和声音 6 之间的区别在于,声音 6 是声音 5,但在音量上有振荡(LFO),这种类型的振荡可以放在频率滤波器上,效果(如失真)甚至投球。我意识到这使问题变得更加棘手,并且超出了我的要求范围。你有什么建议吗?我什至可以使用几种不同的分类,一次只看一种 属性。

Sam,我认为你可以用机器学习比较两张图片,或者用 numpy 作为数据数组。

这只是一个想法解决方案(不是完整答案): 如果可以将两个直方图转换为大小相等的平面数组 通过 numpy.ndarray.flatten

array1 = numpy.array([1.1, 2.2, 3.3])
array2 = numpy.array([1, 2, 3])
diffs = array1 - array2 # array([ 0.1,  0.2,  0.3])
similarity_coefficient = np.sum(diffs)

比较两个音频文件或音频文件的目录以衡量它们的相似性。可能派生自另一个文件的文件被标记为匹配项。

到 运行 程序,键入以下之一:

./audiocompare -f file1 -f file2
./audiocompare -f file1 -d dir1
./audiocompare -d dir1 -f file1
./audiocompare -d dir1 -d dir2

“-f”参数后的参数必须是文件名,“-d”参数后的参数必须是仅包含音频文件的目录。输入文件必须是 WAVE 或 MP3 文件。您可以列出相同的文件或目录两次。

如果发现错误,将打印相应的错误信息,程序可能会继续运行。如果比较了两个 non-matching 文件,匹配结果将打印为“NO MATCH”,如果比较了两个匹配文件,则打印为“MATCH ...”,列出匹配的两个文件,并给出匹配分数。

Link: https://github.com/charlesconnell/AudioCompare

我想出了一个方法,不确定它是否完全符合您的期望,但对于您的第一个数据集,它非常接近。基本上我正在查看 .wav 文件的功率谱密度 power spectral density 并按其归一化积分排序。 (我没有很好的信号处理理由这样做。PSD 让你知道每个频率有多少能量。我最初尝试按 PSD 排序但结果很糟糕。认为当你处理你正在创建的文件时更多可变性,我认为这会以这种方式改变光谱密度的变化,并尝试了一下。)如果这满足您的需要,我希望您能找到该方法的理由。

第 1 步: 这非常简单,只需将 y 更改为 self.y 即可将其添加到您的 MFCC class:

class MFCC(Spec):

    mfcc: np.ndarray  # Mel-frequency cepstral coefficient
    delta_mfcc: np.ndarray  # delta Mel-frequency cepstral coefficient
    delta2_mfcc: np.ndarray  # delta2 Mel-frequency cepstral coefficient
    n_mfcc: int = 13

    def __init__(self, soundFile: str):
        self.name = path.basename(soundFile)
        self.y, sr = librosa.load(soundFile, sr=self.sr) # <--- This line is changed
        self.mfcc = librosa.feature.mfcc(self.y, n_mfcc=self.n_mfcc, sr=sr)
        self.delta_mfcc = librosa.feature.delta(self.mfcc, mode="nearest")
        self.delta2_mfcc = librosa.feature.delta(self.mfcc, mode="nearest", order=2)

第 2 步: 计算PSD的PSD并积分(或者真的只是求和):

def spectra_of_spectra(mfcc):
    # first calculate the psd
    fft = np.fft.fft(mfcc.y)
    fft = fft[:len(fft)//2+1]
    psd1 = np.real(fft * np.conj(fft))
    # then calculate the psd of the psd
    fft = np.fft.fft(psd1/sum(psd1))
    fft = fft[:len(fft)//2+1]
    psd = np.real(fft * np.conj(fft))
    return(np.sum(psd)/len(psd))

除以长度(规范化)有助于比较不同长度的不同文件。

第 3 步: 排序

def sort_mfccs(mfccs):
    values = [spectra_of_spectra(mfcc) for mfcc in mfccs]
    sorted_order = [i[0] for i in sorted(enumerate(values), key=lambda x:x[1], reverse = True)]
    return([i for i in sorted_order], [values[i] for i in sorted_order])

测试

mfccs_1 = get_mfccs(sound_files_1)
sort_mfccs(mfccs_1)
1.wav
2.wav
3.wav
4.wav
5.wav
6.wav
7.wav
8.wav
9.wav
10.wav
12.wav
11.wav
13.wav

请注意,除了 11.wav12.wav 之外,文件的排序方式与您期望的方式一致。

我不确定您是否同意第二组文件的顺序。我想这就是对我的方法有多有用的测试。

mfccs_2 = get_mfccs(sorted_sound_files_2)
sort_mfccs(mfccs_2)
12.wav
22.wav
26.wav
31.wav
4.wav
13.wav
34.wav
30.wav
21.wav
23.wav
7.wav
38.wav
11.wav
3.wav
9.wav
36.wav
16.wav
17.wav
33.wav
37.wav
8.wav
28.wav
5.wav
25.wav
20.wav
1.wav
39.wav
29.wav
18.wav
0.wav
27.wav
14.wav
35.wav
15.wav
24.wav
10.wav
19.wav
32.wav
2.wav
6.wav

关于代码回复中问题的最后一点:UserWarning

我不熟悉您在这里使用的模块,但看起来它正在尝试在长度为 1536 的文件上执行 FFT,window 长度为 2048。FFT是任何类型的频率分析的基石。在行 self.mfcc = librosa.feature.mfcc(self.y, n_mfcc=self.n_mfcc, sr=sr) 中,您可以指定 kwarg n_fft 来删除它,例如 n_fft = 1024。但是,我不确定为什么 librosa 使用 2048 作为默认值,因此您可能需要在更改之前仔细检查。

编辑

绘制这些值将有助于更多地显示比较。数值差异越大,文件差异越大。

def diff_matrix(L, V, mfccs):
    plt.figure()
    plt.semilogy(V, '.')
    for i in range(len(V)):
        plt.text(i, V[i], mfccs[L[i]].name.split('.')[0], fontsize = 8)
    plt.xticks([])
    plt.ylim([0.001, 1])
    plt.ylabel('Value')

这是您第一组的结果

和第二组

根据值彼此之间的接近程度(想想百分比变化而不是差异),与第一组相比,第二组的排序对任何调整都非常敏感。

编辑 2

我对你下面的回答最好的尝试是尝试这样的事情。为简单起见,我将从信号处理的角度将 音高频率 描述为音符的频率,将 频谱频率 描述为频率变化。我希望这是有道理的。

我希望音量上的振荡能够击中所有音高,因此对 PSD 的贡献将取决于音量在频谱频率方面的振荡方式。当不同的音调频率受到不同的阻尼时,您需要开始考虑哪些音调频率对您正在做的事情很重要。我认为我的排序在您的第一个示例中如此成功的原因可能是因为音高频率的变化无处不在(或几乎无处不在)。或许有一种方法可以考虑在不同的音调频率或音调频带上查看 PSD。我还没有完全吸收其他答案中引用的论文中的信息,但如果你理解数学,我会从那里开始。作为免责声明,我只是玩弄了一些东西来尝试回答你的问题。您可能需要考虑在 site more focused on questions like this.

上提出 follow-up 问题

有趣的问题。您可能会发现音色是一个有点复杂的量,仅用一个数字并不那么容易量化。 然而,一些研究试图提取so-to-say声音音色的“数值参数”,以便分组和比较。

例如这样的研究:Geoffroy Peeters, 2011, The Timbre Toolbox: Extracting audio descriptors from musical signals.

在论文中(应该可以免费获得),您会发现声音的各种量,并且您会看到音色也扩展到频谱域之外。但是,为了向您指明合适的方向,我会看一下“光谱质心”和“光谱传播”。在计算距离方面,这可以通过多种方式完成,将声音视为驻留在 multi-dimensional space 音色参数中。

这是 librosa 相关部分的链接列表:

您可以完全 sound-file 或适合您的目的:-)

https://github.com/AudioCommons/timbral_models 包预测了八种音色特征:硬度、深度、亮度、粗糙度、温暖度、锐度、隆隆声和混响。

我按每一个排序了。

from timbral_models import timbral_extractor
from pathlib import Path
from operator import itemgetter

path = Path("sort-sounds-by-similarity-from-sound-file/assets/transients_1/")
timbres = [
    {"file": file, "timbre": timbral_extractor(str(file))} for file in path.glob("*wav")
]

itemgetters = {key: itemgetter(key) for key in timbres[0]["timbre"]}

for timbre, get_timbre in itemgetters.items():
    print(f"Sorting by {timbre}")
    for item in sorted(timbres, key=lambda d: get_timbre(d["timbre"])):
        print(item["file"].name)
    print()

输出;

Sorting by hardness
1.wav
2.wav
6.wav
3.wav
4.wav
13.wav
7.wav
9.wav
8.wav
10.wav
5.wav
11.wav
12.wav

Sorting by depth
4.wav
12.wav
5.wav
6.wav
9.wav
8.wav
7.wav
3.wav
10.wav
11.wav
2.wav
1.wav
13.wav

Sorting by brightness
1.wav
2.wav
3.wav
9.wav
10.wav
6.wav
5.wav
8.wav
7.wav
4.wav
13.wav
11.wav
12.wav

Sorting by roughness
3.wav
1.wav
2.wav
7.wav
8.wav
9.wav
5.wav
6.wav
4.wav
10.wav
13.wav
11.wav
12.wav

Sorting by warmth
7.wav
6.wav
8.wav
12.wav
9.wav
11.wav
4.wav
5.wav
10.wav
13.wav
2.wav
3.wav
1.wav

Sorting by sharpness
1.wav
3.wav
2.wav
10.wav
9.wav
5.wav
7.wav
6.wav
8.wav
13.wav
4.wav
11.wav
12.wav

Sorting by boominess
8.wav
9.wav
6.wav
5.wav
4.wav
7.wav
12.wav
2.wav
3.wav
10.wav
1.wav
11.wav
13.wav

Sorting by reverb
12.wav
11.wav
9.wav
13.wav
6.wav
8.wav
7.wav
10.wav
4.wav
3.wav
2.wav
1.wav
5.wav