根据音色将声音按相似度分类排序

10

解释

我想要能够根据声音的音色(音调)对列表中的声音集合进行排序。这里有一个玩具示例,其中我手动对我创建并上传到此存储库的12个声音文件的频谱图进行了排序。我知道这些已经正确排序,因为每个文件产生的声音与其前面的文件完全相同,只是添加了一种效果或滤镜。

例如,声音xyz的正确排序如下:

  • 声音x和y相同,但y有失真效果
  • 声音y和z相同,但z过滤掉高频
  • 声音x和z相同,但z有失真效果,且z过滤掉高频

正确的排序应该是x, y, z

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


上图中声音文件:
  • 长度相同
  • 音高相同
  • 同时开始
  • 幅度相同(响度级别)
即使这些条件不成立,我也希望我的排序能够正常工作(但如果不能解决问题,我会接受最佳答案) 例如,在下图中:
  • MFCC_8 的起始位置与第一张图片中的 MFCC_8 不同
  • MFCC_9 与第一张图片中的 MFCC_9 相同,但被复制了一次(因此长度加倍)
如果将第一张图片中的 MFCC_8 和 MFCC_9 替换为下图中的 MFCC_8 和 MFCC_9,则希望声音的排序保持完全相同。

我打算在我的真实程序中通过声音变化类似这样来分割mp3文件。


我的程序目前为止

这是生成帖子中第一张图片的程序。我需要将函数sort_sound_files中的代码替换为实际基于音色对声音文件进行排序的代码。需要完成的部分在底部,声音文件在此存储库上。我还在jupyter笔记本中有这段代码,其中还包括一个更类似于我实际想要的程序的第二个示例。

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),这种类型的振荡可以放置在频率滤波器、效果器(如失真)或甚至音高上。我意识到这使得问题变得更加棘手,它超出了我所问的范围。你有什么建议吗?我甚至可以使用几种不同的类型,并且只同时查看一种属性。


如果你想基于声音特征而不是音符来确定相似性,那通常被称为“音色”。 - Jon Nordby
@jonnor 是的。我在想如何将音色记录到可以使用的格式中。比如一个数组或其他什么东西,而数组中的行/列代表什么。 - Sam
音色是一个相当复杂的知觉概念,通常很难从音频的音符中分离出来。最好的方法是使用一些学习模型/嵌入,将其映射到仅有音色的低维空间中。 - Jon Nordby
@jonnor 当你说“将其映射到低维空间”时,你到底是什么意思?我已经阅读了更多相关内容,看起来MFCC特征对于比较音色非常有用。你有什么建议可以用来将其降维的模型吗? - Sam
输出结果是几个数字,可能是2-10个,仅代表音色 - 即将其与音乐本身分离/独立。在MFCC中,这两件事仍然纠缠在一起。不幸的是,目前我还不知道有关此的模型。 - Jon Nordby
5个回答

5

这个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

5

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)

4
我想出了一种方法,不确定它是否完全符合您的期望,但对于您的第一个数据集来说非常接近。基本上,我正在查看您的.wav文件的功率谱密度的功率谱密度,并按照其归一化积分进行排序。(我没有好的信号处理理由来做这件事。PSD可以让您了解每个频率上有多少能量。我最初尝试按PSD排序,但结果很差。考虑到您处理创建的文件时会产生更多的变异性,我认为这会以这种方式改变频谱密度中的变异性,所以我就试了一下。) 如果这样做符合您的需求,我希望您能找到一种方法来证明这种方法的可行性。 步骤1: 这很简单,只需将y更改为self.y,将其添加到您的MFCC类中即可:
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

sorted results

关于代码中的问题和UserWarning的最后一个要点

我并不熟悉你在这里使用的模块,但看起来它正在尝试对长度为1536的文件进行2048的窗口长度FFT。 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')

这是您第一组的结果

diff1

和第二组

diff2

基于数值相对接近程度(考虑百分比变化而非差异),与第一组相比,第二组的排序将对任何微调非常敏感。 编辑2 我最好的答案是尝试像这样做。为简单起见,我将描述音高频率为音符的频率,光谱频率为从信号处理角度的频率变化。希望这有意义。
我希望音量的振荡能够影响到所有的音高,因此对于功率谱密度的贡献将取决于音量在频谱频率方面的振荡情况。当不同的音高频率被不同程度地阻尼时,您需要开始考虑哪些音高频率对您正在做的事情很重要。我认为在您的第一个示例中排序如此成功的原因可能是由于变化在各个音高频率上普遍存在(或几乎普遍存在)。也许有一种方法可以考虑查看不同音高频率或音高频率带的功率谱密度。我还没有完全吸收其他答案引用的论文中的信息,但如果您理解了数学,我会从那里开始。作为免责声明,我只是随便编了点东西来回答您的问题。您可能想考虑在更专注于此类问题的网站上提出后续问题。

我直到后来才意识到另一个非常重要的事情,那就是会有很多属性在振荡。例如,第一组中声音5和声音6之间的区别在于声音6是声音5加上音量振荡(LFO),这种类型的振荡可以放置在频率滤波器、效果器(如失真)甚至音高上。我意识到这使问题变得更加棘手,超出了我所问的范围。你有什么建议吗?我甚至可以使用几种不同的排序方式,只查看一个属性。 - Sam
顺便说一句,你的答案很棒,我会等到最后再关闭问题,看看是否还有其他答案,但这个答案很有用。 - Sam
@Sam,我在对我的问题进行另一次编辑时尝试回答了你的问题。我希望我能给你比这更多的信息,但实际上我不是信号处理或声音/音乐技术方面的专家。 - ramzeek
diff_matrix(L, V, mfccs) 中的 L 和 V 是什么? - Sam
回顾我的代码,我认为这是从sort_matrices输出的,即L,V = sort_mfccs(mfccs) - ramzeek

4
有趣的问题。您可能会发现,音色是一种相对复杂的量,不容易通过单个数字来量化。 然而,一些研究试图提取所谓的音色“数值参数”,以便进行分组和比较。
这样的研究包括:Geoffroy Peeters, 2011, The Timbre Toolbox: Extracting audio descriptors from musical signals
在这篇论文中(应该可以免费获取),您将找到声音的各种数量,并且您会看到音色也扩展到了频谱领域之外。然而,为了指引您朝着合适的方向前进,我建议您查看“频谱质心”和“频谱展宽”。在计算距离方面,可以通过将声音视为音色参数的多维空间中的点来完成。
以下是与librosa相关部分的链接列表: 你可以为完整的音频文件进行操作,或者根据你的需求进行选择 :-)

有趣!谢谢分享。从论文和代码中我可以看出,spectral_centroid 应该是归一化功率谱密度的局部积分的平方根。出于好奇,您知道为什么要使用振幅(PSD的平方根)而不是能量(与振幅的平方有关)吗?这是因为人耳的工作原理吗?您如何选择 spectral_bandwidth 的顺序 p?(音乐的数学和物理并不是我的专业领域!) - ramzeek
从Peeters的论文中,他们使用$p_k$作为“幅度STFT、功率STFT、谐波正弦部分或ERB模型输出”的归一化版本。我不知道他们为什么在librosa中选择了特定的频谱-我的猜测是为了方便使用。在Peeters的论文中,他们选择$p=2$作为阶数。我不知道这与听觉系统之间是否有任何关系。 - Jens C. Thuren Lindahl

0

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

要运行程序,请输入以下其中之一:

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

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

如果发现错误,将打印适当的错误消息,并且如果可以,程序可能会继续运行。如果比较了两个不匹配的文件,则匹配结果将被打印为“NO MATCH”,如果比较了两个匹配的文件,则将打印“MATCH ...”,列出匹配的两个文件并给出匹配分数。

链接: https://github.com/charlesconnell/AudioCompare


这些文件并非相互派生,我有一个合成器软件,用第一个声音信号为正弦波开始,声音二则是在正弦波上加了一种效果,声音三是在声音二的基础上再加了另一种效果。然后我编写了一个 MIDI 轨道,在节拍一开始播放四拍的音符,并将每个声音设置为从节拍一的开始开始,最后将每个声音导出到 WAV/MP3 文件。不过这种方法仅适用于我的玩具示例,因为我知道这样会更容易。我打算像这样根据音符变化来分割 MP3 文件 - Sam

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