Найти звуковой эффект внутри аудиофайла

У меня загружены 3-часовые файлы MP3, и каждые ~ 15 минут воспроизводится отчетливый 1-секундный звуковой эффект, который сигнализирует о начале новой главы.

Можно ли идентифицировать каждый раз, когда воспроизводится этот звуковой эффект, чтобы я мог отметить временные смещения?

Звуковой эффект каждый раз одинаков, но, поскольку он был закодирован в формате файла с потерями, вариации будут небольшими.

Смещения по времени будут храниться в метаданных кадра главы ID3.


Пример источника, где звуковой эффект воспроизводится дважды.

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 образца). Поэтому я могу представить, что какое-то усреднение в каждом цикле может привести к тому, что это займет значительно больше времени.


person Craig Francis    schedule 29.09.2018    source источник
comment
Аудио представляет собой данные непрерывной волны, но временной ряд является дискретным, поэтому то, что вы здесь делаете, действительно будет работать только в том случае, если все вхождения ваших звуковых клипов синхронизированы с учетом частоты дискретизации. Вы можете попробовать выполнить обнаружение начала, а затем используйте начала, чтобы сопоставить ноты.   -  person Lie Ryan    schedule 30.09.2018
comment
Спасибо @LieRyan, вы правильно заметили, и это подчеркнуло, что эти звуковые эффекты не так похожи, как я думал. Я добавил несколько файлов с примерами и создал спектрограммы двух семплов (включая обнаружение начала, подробности). Я также поиграл с их усреднением и использованием frames_to_time , но должен признать, что не уверен, что делаю это правильно (хотя буду продолжать попытки). Спасибо еще раз.   -  person Craig Francis    schedule 30.09.2018
comment
Я особо не вникал в это, но одна из идей заключалась в том, чтобы рассчитать корреляцию между звуком маркера и файлом в целом. Корреляция должна иметь пики в моменты времени, когда маркеры появляются в файле.   -  person Matthias    schedule 30.09.2018
comment
@Matthias, спасибо за предложение, у меня была игра, но на самом деле есть только 3 пика, и выстроить их в линию было не особенно точно. В настоящее время я играю с данными, которые входят в спектрограмму, так как я думаю, что это могло бы помочь более подробному анализу звука (например, удар из пистолета звучит не так, как барабан) ... но должен признать, что я я тут многое предполагаю :-)   -  person Craig Francis    schedule 14.10.2018


Ответы (4)


Попытка напрямую сопоставить образцы сигналов во временной области не является хорошей идеей. Сигнал mp3 сохранит перцептивные свойства, но вполне вероятно, что фазы частотных составляющих будут сдвинуты, так что выборочные значения не будут совпадать.

Вы можете попытаться сопоставить огибающие громкости вашего эффекта и вашего семпла. Это с меньшей вероятностью будет затронуто процессом mp3.

Во-первых, нормализуйте свой образец, чтобы встроенные эффекты были того же уровня, что и ваш эталонный эффект. Построение новых сигналов из эффекта и сэмпла с использованием среднего значения пиковых значений за временные рамки, которые достаточно короткие, чтобы зафиксировать соответствующие функции. Еще лучше использовать перекрывающиеся рамки. Затем используйте взаимную корреляцию во временной области.

Если это не сработает, вы можете проанализировать каждый кадр с помощью БПФ, это даст вам вектор признаков для каждого кадра. Затем вы пытаетесь найти совпадения последовательности функций в вашем эффекте с образцом. Аналогично предложению https://stackoverflow.com/users/1967571/jonnor. MFCC используется при распознавании речи, но, поскольку вы не обнаруживаете речь, БПФ, вероятно, в порядке.

Я предполагаю, что эффект воспроизводится сам по себе (без фонового шума), и он добавляется к записи электронным способом (в отличие от записи через микрофон). Если это не так, проблема усложняется.

person Paul John Leonard    schedule 06.10.2018
comment
Спасибо @paul-john-leonard, на данный момент я просто вырезал звуковой эффект из файла сэмпла, поэтому я предполагаю, что на данный момент нет необходимости его нормализовать (тем не менее, это может понадобиться при просмотре реальные файлы). Я также использовал librosa.stft, который, как я полагаю, выполняет бит FFT и передает данные через util.normalize. Как вы думаете, я на том же пути, который вы предлагаете, или я что-то упускаю? Кроме того, он добавляется в электронном виде, но я думаю, что в процессе записи может происходить некоторое сжатие, поэтому могут быть небольшие вариации. - person Craig Francis; 14.10.2018
comment
Я не знаком с librosa, но из вашего кода похоже, что вы идете по тому же пути, что и я. - person Paul John Leonard; 15.10.2018
comment
Как вы упоминаете в своем собственном ответе, следующим шагом будет определение совпадения. Если вы застряли на ручной настройке, то примеры реальных и ложных совпадений можно использовать в качестве обучающих данных, скажем, для ближайших соседей. - person Paul John Leonard; 15.10.2018

Это проблема обнаружения аудиособытий. Если звук всегда один и тот же, и в то же время нет других звуков, это, вероятно, можно решить с помощью подхода сопоставления шаблонов. По крайней мере, если нет других звуков с другим значением, которые звучат похоже.

Самый простой вид сопоставления с шаблоном состоит в том, чтобы вычислить взаимную корреляцию между вашим входным сигналом и шаблоном.

  1. Вырежьте пример звука для обнаружения (используя Audacity). Берите как можно больше, но избегайте начала и конца. Сохраните это как файл .wav
  2. Загрузите шаблон .wav, используя librosa.load()
  3. Разбейте входной файл на серию перекрывающихся кадров. Длина должна быть такой же, как ваш шаблон. Это можно сделать с помощью librosa.util.frame.
  4. Переберите кадры и вычислите взаимную корреляцию между кадром и шаблоном, используя numpy.correlate.
  5. Высокие значения взаимной корреляции указывают на хорошее совпадение. Порог может применяться для того, чтобы решить, что является событием, а что нет. А по номеру кадра можно вычислить время события.

Вероятно, вам следует подготовить несколько более коротких тестовых файлов, в которых есть несколько примеров звука для обнаружения, а также другие типичные звуки.

Если объем записей непостоянен, вы захотите нормализовать его перед запуском обнаружения.

Если взаимная корреляция во временной области не работает, вы можете вычислить мелспектрограмму или функции MFCC и выполнить взаимную корреляцию. Если это также не дает удовлетворительных результатов, модель машинного обучения можно обучить с помощью обучения с учителем, но для этого необходимо пометить набор данных как событие/несобытие.

person Jon Nordby    schedule 06.10.2018
comment
Спасибо, @jonnor, я не использовал librosa.util.frame напрямую, но я играл с librosa.stft, который в своем исходном коде использует его для окна временных рядов... по общему признанию, я пока понятия не имею, что делает STFT (я м использовать его, потому что он используется для создания спектрограммы). Но будет ли ваше предложение лучшим / более простым маршрутом, или я наткнулся на что-то, что в основном такое же? Кроме того, спасибо за упоминание об обнаружении аудиособытий, я изо всех сил пытался найти дополнительную информацию, где я также посмотрю на MelSpectrogram/MFCC. - person Craig Francis; 14.10.2018

Чтобы уточнить ответы @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 я сначала пытался использовать hop_length, равный 64, а не значение по умолчанию (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-й) кадр и, если он совпадал, помещал их в список потенциальных совпадений. Затем, по мере перехода к следующим кадрам источника, он мог сравнивать его со следующими кадрами шаблона, пока все кадры в шаблоне не совпадут (или не совпадут).

person Craig Francis    schedule 04.01.2019

Это может быть не ответ, это просто то, к чему я пришел, прежде чем начать исследовать ответы @jonnor и @paul-john-leonard.

Я смотрел на спектрограммы, которые вы можете получить с помощью librosa stft и amplitude_to_db, и подумал, что если я возьму данные, которые входят в графики, с небольшим округлением, я потенциально могу найти 1 воспроизводимый звуковой эффект:

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

Код, который я написал ниже, вроде работает; хотя это:

  1. Возвращает довольно много ложных срабатываний, которые можно исправить, настроив параметры того, что считается совпадением.

  2. Мне нужно было бы заменить функции librosa чем-то, что может анализировать, округлять и выполнять проверки соответствия за один проход; поскольку 3-часовой аудиофайл приводит к тому, что у python заканчивается память на компьютере с 16 ГБ ОЗУ примерно через 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

#--------------------------------------------------
person Craig Francis    schedule 14.10.2018