5

我加载了 3 小时的 MP3 文件,每 15 分钟播放一次独特的 1 秒音效,这标志着新篇章的开始。

是否可以识别每次播放此音效的时间,以便我可以记录时间偏移?

每次的音效都差不多,但是因为是有损文件格式编码的,所以会有少量的变化。

时间偏移将存储在ID3 Chapter Frame MetaData中。


示例 Source,音效播放两次。

ffmpeg -ss 0.9 -i source.mp3 -t 0.95 sample1.mp3 -acodec copy -y

ffmpeg -ss 4.5 -i source.mp3 -t 0.95 sample2.mp3 -acodec copy -y


我对音频处理很陌生,但我最初的想法是提取 1 秒音效的样本,然后使用librosa在 python 中使用为两个文件提取浮点时间序列,对浮点数进行四舍五入,然后尝试获取一场比赛。

import numpy
import librosa

print("Load files")

source_series, source_rate = librosa.load('source.mp3') # 3 hour file
sample_series, sample_rate = librosa.load('sample.mp3') # 1 second file

print("Round series")

source_series = numpy.around(source_series, decimals=5);
sample_series = numpy.around(sample_series, decimals=5);

print("Process series")

source_start = 0
sample_matching = 0
sample_length = len(sample_series)

for source_id, source_sample in enumerate(source_series):

    if source_sample == sample_series[sample_matching]:

        sample_matching += 1

        if sample_matching >= sample_length:

            print(float(source_start) / source_rate)

            sample_matching = 0

        elif sample_matching == 1:

            source_start = source_id;

    else:

        sample_matching = 0

这不适用于上面的 MP3 文件,但适用于 MP4 版本 - 它能够找到我提取的样本,但它只是一个样本(不是全部 12 个)。

我还应该注意,这个脚本只需要 1 分钟多一点的时间来处理 3 小时的文件(其中包括 237,426,624 个样本)。所以我可以想象,对每个循环进行某种平均会导致这需要更长的时间。

4

4 回答 4

3

尝试在时域中直接匹配波形样本并不是一个好主意。mp3 信号将保留感知属性,但频率分量的相位很可能会发生偏移,因此样本值将不匹配。

您可以尝试尝试匹配效果和样本的音量包络。这不太可能受到 mp3 进程的影响。

首先,标准化您的样本,使嵌入的效果与您的参考效果相同。通过使用峰值在时间帧上的平均值来从效果和样本构造新波形,时间帧足够短以捕获相关特征。最好还是使用重叠帧。然后在时域中使用互相关。

如果这不起作用,那么您可以使用 FFT 分析每个帧,这将为您提供每个帧的特征向量。然后,您尝试查找效果中的特征序列与样本的匹配。类似于https://stackoverflow.com/users/1967571/jonnor建议。MFCC 用于语音识别,但由于您没有检测语音 FFT 可能没问题。

我假设效果是自己播放的(没有背景噪音),并以电子方式添加到录音中(而不是通过麦克风录音)。如果不是这种情况,问题就会变得更加困难。

于 2018-10-06T20:54:41.477 回答
2

这是一个音频事件检测问题。如果声音总是一样的,同时没有其他声音,可能可以用模板匹配的方法来解决。至少如果没有其他听起来相似的其他含义的声音。

最简单的模板匹配是计算输入信号和模板之间的互相关。

  1. 剪下一个要检测的声音示例(使用 Audacity)。尽可能多地服用,但要避免开始和结束。将此存储为 .wav 文件
  2. 使用 librosa.load() 加载 .wav 模板
  3. 将输入文件切成一系列重叠的帧。长度应与您的模板相同。可以用librosa.util.frame完成
  4. 遍历帧,并使用numpy.correlate计算帧和模板之间的互相关。
  5. 高互相关值表明匹配良好。可以应用阈值来决定什么是事件或不是事件。并且帧数可以用来计算事件发生的时间。

您可能应该准备一些较短的测试文件,其中包含一些要检测的声音示例以及其他典型声音。

如果录音的音量不一致,您需要在运行检测之前对其进行标准化。

如果时域中的互相关不起作用,您可以计算 melspectrogram 或 MFCC 特征并进行互相关。如果这也不能产生好的结果,可以使用监督学习来训练机器学习模型,但这需要将一堆数据标记为事件/非事件。

于 2018-10-06T19:48:13.350 回答
2

为了跟进@jonnor 和@paul-john-leonard 的答案,它们都是正确的,通过使用帧(FFT)我能够进行音频事件检测。

我已经在以下位置编写了完整的源代码:

https://github.com/craigfrancis/audio-detect

一些注意事项:

  • 为了创建模板,我使用了 ffmpeg:

    ffmpeg -ss 13.15 -i source.mp4 -t 0.8 -acodec copy -y templates/01.mp4;

  • 我决定使用librosa.core.stft,但我需要为stft我正在分析的 3 小时文件自行实现此功能,因为它太大而无法保存在内存中。

  • 使用时,stft我首先尝试使用 64 的 hop_length,而不是默认值 (512),因为我认为这会给我更多的数据来使用......理论可能是正确的,但 64 太详细了,并导致它大部分时间都失败了。

  • 我仍然不知道如何使帧和模板之间的互相关起作用(通过numpy.correlate)...相反,我获取了每帧的结果(1025 个桶,而不是 1024 个,我认为这与找到的赫兹频率有关)并做到了一个非常简单的平均差异检查,然后确保平均值高于某个值(我的测试用例工作在 0.15,我在需要 0.55 上使用的主文件 - 大概是因为主文件被压缩了很多):

    hz_score = abs(source[0:1025,x] - template[2][0:1025,y])
    hz_score = sum(hz_score)/float(len(hz_score))

  • 检查这些分数时,将它们显示在图表上非常有用。我经常使用类似以下的东西:

    import matplotlib.pyplot as plt
    plt.figure(figsize=(30, 5))
    plt.axhline(y=hz_match_required_start, color='y')

    while x < source_length:
    debug.append(hz_score)
    if x == mark_frame:
    plt.axvline(x=len(debug), ymin=0.1, ymax=1, color='r')

    plt.plot(debug)
    plt.show()

  • 创建模板时,您需要剪掉任何前导静音(以避免匹配错误)和额外的约 5 帧(似乎压缩/重新编码过程改变了这一点)......同样,删除最后 2帧(我认为这些帧包括来自周围环境的一些数据,特别是最后一个可能有点偏离)。

  • 当您开始寻找匹配项时,您可能会发现前几帧没问题,然后就失败了……您可能需要在一两帧后重试。我发现支持多个模板(声音的细微变化)的过程更容易,并且会检查它们的第一个可测试(例如第 6 个)帧,如果匹配,则将它们放入潜在匹配列表中。然后,随着它前进到源的下一帧,它可以将其与模板的下一帧进行比较,直到模板中的所有帧都匹配(或失败)。

于 2019-01-04T02:04:17.263 回答
0

这可能不是一个答案,这只是我在开始研究@jonnor 和@paul-john-leonard 的答案之前到达的地方。

我正在查看您可以通过使用 librosastft和获得的频谱图amplitude_to_db,并认为如果我将进入图表的数据进行一些四舍五入,我可能会发现正在播放的 1 声音效果:

https://librosa.github.io/librosa/generated/librosa.display.specshow.html

我在下面编写的代码很有效;虽然它:

  1. 确实会返回很多误报,这可以通过调整被认为匹配的参数来解决。

  2. 我需要用可以一次性解析、舍入和进行匹配检查的东西替换 librosa 函数;作为一个 3 小时的音频文件,导致 python 在具有 16GB RAM 的计算机上用完大约 30 分钟后内存不足,甚至还没有达到舍入位。


import sys
import numpy
import librosa

#--------------------------------------------------

if len(sys.argv) == 3:
    source_path = sys.argv[1]
    sample_path = sys.argv[2]
else:
    print('Missing source and sample files as arguments');
    sys.exit()

#--------------------------------------------------

print('Load files')

source_series, source_rate = librosa.load(source_path) # The 3 hour file
sample_series, sample_rate = librosa.load(sample_path) # The 1 second file

source_time_total = float(len(source_series) / source_rate);

#--------------------------------------------------

print('Parse Data')

source_data_raw = librosa.amplitude_to_db(abs(librosa.stft(source_series, hop_length=64)))
sample_data_raw = librosa.amplitude_to_db(abs(librosa.stft(sample_series, hop_length=64)))

sample_height = sample_data_raw.shape[0]

#--------------------------------------------------

print('Round Data') # Also switches X and Y indexes, so X becomes time.

def round_data(raw, height):

    length = raw.shape[1]

    data = [];

    range_length = range(1, (length - 1))
    range_height = range(1, (height - 1))

    for x in range_length:

        x_data = []

        for y in range_height:

            # neighbours = []
            # for a in [(x - 1), x, (x + 1)]:
            #     for b in [(y - 1), y, (y + 1)]:
            #         neighbours.append(raw[b][a])
            #
            # neighbours = (sum(neighbours) / len(neighbours));
            #
            # x_data.append(round(((raw[y][x] + raw[y][x] + neighbours) / 3), 2))

            x_data.append(round(raw[y][x], 2))

        data.append(x_data)

    return data

source_data = round_data(source_data_raw, sample_height)
sample_data = round_data(sample_data_raw, sample_height)

#--------------------------------------------------

sample_data = sample_data[50:268] # Temp: Crop the sample_data (318 to 218)

#--------------------------------------------------

source_length = len(source_data)
sample_length = len(sample_data)
sample_height -= 2;

source_timing = float(source_time_total / source_length);

#--------------------------------------------------

print('Process series')

hz_diff_match = 18 # For every comparison, how much of a difference is still considered a match - With the Source, using Sample 2, the maximum diff was 66.06, with an average of ~9.9

hz_match_required_switch = 30 # After matching "start" for X, drop to the lower "end" requirement
hz_match_required_start = 850 # Out of a maximum match value of 1023
hz_match_required_end = 650
hz_match_required = hz_match_required_start

source_start = 0
sample_matched = 0

x = 0;
while x < source_length:

    hz_matched = 0
    for y in range(0, sample_height):
        diff = source_data[x][y] - sample_data[sample_matched][y];
        if diff < 0:
            diff = 0 - diff
        if diff < hz_diff_match:
            hz_matched += 1

    # print('  {} Matches - {} @ {}'.format(sample_matched, hz_matched, (x * source_timing)))

    if hz_matched >= hz_match_required:

        sample_matched += 1

        if sample_matched >= sample_length:

            print('      Found @ {}'.format(source_start * source_timing))

            sample_matched = 0 # Prep for next match

            hz_match_required = hz_match_required_start

        elif sample_matched == 1: # First match, record where we started

            source_start = x;

        if sample_matched > hz_match_required_switch:

            hz_match_required = hz_match_required_end # Go to a weaker match requirement

    elif sample_matched > 0:

        # print('  Reset {} / {} @ {}'.format(sample_matched, hz_matched, (source_start * source_timing)))

        x = source_start # Matched something, so try again with x+1

        sample_matched = 0 # Prep for next match

        hz_match_required = hz_match_required_start

    x += 1

#--------------------------------------------------
于 2018-10-14T12:08:44.187 回答