Автор статьи — польский программист Томек Рекавек, разрабатывает проект Jackrabbit Oak в рамках Apache Software Foundation для Adobe. Статья опубликована в личном блоге автора 24 февраля 2016 года.
Польское «Радио-3» (так называемая «Тройка») знаменито хорошей музыкой и интеллигентными ведущими. С другой стороны, оно страдает наличием громких и раздражающих рекламных блоков в трансляции, где обычно рекламируется какая-нибудь электроника или лекарство. Я слушаю «Тройку» почти постоянно на работе и дома, поэтому задался вопросом: как удалить рекламу? Кажется, мне удалось найти решение.
Моя цель — создать приложение, которое приглушает рекламу. Коммерческий блок начинается и заканчивается джинглами, поэтому программа должна распознать эти конкретные звуки и выключить звук между ними.
Знаю, что данная область математики/информатики называется цифровой обработкой сигналов, но мне DSP всегда казалась магией. Что ж, отличная возможность узнать что-то новое. Я провёл день или два, пытаясь выяснить, какой механизм использовать для анализа аудиопотока. И в конце концов нашёл то что надо: это взаимная корреляция или кросс-корреляция (cross-correlation).
Обычно все ссылаются на реализацию MATLAB. Но MATLAB — дорогостоящее приложение, которое упрощает выполнение сложных математических операций, в том числе DSP. К счастью, есть бесплатная альтернатива под названием Octave. Кажется, в Octave несложно запустить взаимную корреляцию на двух аудиофайлах. Нужно лишь выполнить следующие команды:
Получится такой график:
Хорошо заметен пик, описывающий положение
Я хотел реализовать тот же алгоритм на Java, и тогда у меня будет инструмент, который:
Использование stdin и stdout позволит подключить новый анализатор к другим приложениям, отвечающим за аудиотрансляцию и воспроизведение результата.
Первым делом Java-программа должна прочитать джингл (сохранённый в виде файла
Здесь каждый сэмпл сохраняется в виде 16-битного числа с обратным порядком байтов (little endian). В Java такое число называется
Чтобы реализовать функцию
Выглядит страшновато, но большинство функций — тривиальные операции с массивами. В основе кросс-корреляции лежит применение быстрого преобразования Фурье на звуковом образце.
Как человек, который не имел опыта работы с DSP, я просто рассматриваю FFT как функцию, которая берёт массив с описанием звукового образца — и возвращает массив с комплексными числами, представляющими частоты. Такой минималистичный подход хорошо сработал: я запустил реализацию FFT из пакета JTransforms и получил те же результаты, что в Octave. Я думаю, здесь отчасти карго-культ, но блин, это работает!
Алгоритм выше предполагает, что
Я немного поэкспериментировал с длиной буфера и получил наилучшие результаты с размером буфера в 1,5 раза больше размера джингла.
Получить поток в формате PCM несложно. Это можно сделать с помощью вышеупомянутого
Я также подготовил простую автономную версию анализатора, которая сама подключается к потоку «Тройки» (без внешнего
Похоже, всё работает как надо :)
Конечная цель — отключить рекламу на уровне аппаратного усилителя, получая «реальный» FM-сигнал, а не некий интернет-поток. Об этом рассказано в следующей статье.
Обсуждение на Hacker News
Обсуждение на Wykop
Обсуждение на Reddit
Польское «Радио-3» (так называемая «Тройка») знаменито хорошей музыкой и интеллигентными ведущими. С другой стороны, оно страдает наличием громких и раздражающих рекламных блоков в трансляции, где обычно рекламируется какая-нибудь электроника или лекарство. Я слушаю «Тройку» почти постоянно на работе и дома, поэтому задался вопросом: как удалить рекламу? Кажется, мне удалось найти решение.
Цифровая обработка сигналов
Моя цель — создать приложение, которое приглушает рекламу. Коммерческий блок начинается и заканчивается джинглами, поэтому программа должна распознать эти конкретные звуки и выключить звук между ними.
Знаю, что данная область математики/информатики называется цифровой обработкой сигналов, но мне DSP всегда казалась магией. Что ж, отличная возможность узнать что-то новое. Я провёл день или два, пытаясь выяснить, какой механизм использовать для анализа аудиопотока. И в конце концов нашёл то что надо: это взаимная корреляция или кросс-корреляция (cross-correlation).
Octave
Обычно все ссылаются на реализацию MATLAB. Но MATLAB — дорогостоящее приложение, которое упрощает выполнение сложных математических операций, в том числе DSP. К счастью, есть бесплатная альтернатива под названием Octave. Кажется, в Octave несложно запустить взаимную корреляцию на двух аудиофайлах. Нужно лишь выполнить следующие команды:
pkg load signal
jingle = wavread('jingle.wav')(:,1);
audio = wavread ('audio.wav')(:,1);
[R, lag] = xcorr(jingle, audio);
plot(R);
Получится такой график:
Хорошо заметен пик, описывающий положение
jingle.wav
в audio.wav
. Что меня удивило, так это простота метода: всю работу делает xcorr()
, остальной код только для чтения файлов и отображения результата.Я хотел реализовать тот же алгоритм на Java, и тогда у меня будет инструмент, который:
- считывает аудиопоток со стандартного входа (например, от ffmpeg),
- анализирует его в поиске джинглов,
- выводит тот же поток на stdout и/или отключает его.
Использование stdin и stdout позволит подключить новый анализатор к другим приложениям, отвечающим за аудиотрансляцию и воспроизведение результата.
Чтение звуковых файлов
Первым делом Java-программа должна прочитать джингл (сохранённый в виде файла
.wav
) в массив. В файле есть некоторая дополнительная информацию вроде заголовков, метаданных и прочего, но нам нужен только звук. Подходящий формат называется PCM, это просто список чисел, представляющих звуки. Преобразовать WAV в PCM может ffmpeg:ffmpeg -i input.wav -f s16le -acodec pcm_s16le output.raw
Здесь каждый сэмпл сохраняется в виде 16-битного числа с обратным порядком байтов (little endian). В Java такое число называется
short
, а для автоматического преобразования входного потока в список значений short
можно использовать класс ByteBuffer
:ByteBuffer buf = ByteBuffer.allocate(4);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(bytes);
short leftChannel = buf.readShort(); // stereo stream
short rightChannel = buf.readShort();
Реверс-инжиниринг xcorr
Чтобы реализовать функцию
xcorr()
на Java, я изучил исходный код Octave. Не изменяя конечный результат, я смог заменить вызов xcorr() следующими строчками — их нужно переписать на Java:N = length(audio);
M = 2 ^ nextpow2(2 * N - 1);
pre = fft(postpad(prepad(jingle(:), length(jingle) + N - 1), M));
post = fft(postpad(audio(:), M));
cor = ifft(pre .* conj(post));
R = real(cor(1:2 * N));
Выглядит страшновато, но большинство функций — тривиальные операции с массивами. В основе кросс-корреляции лежит применение быстрого преобразования Фурье на звуковом образце.
Быстрое преобразование Фурье
Как человек, который не имел опыта работы с DSP, я просто рассматриваю FFT как функцию, которая берёт массив с описанием звукового образца — и возвращает массив с комплексными числами, представляющими частоты. Такой минималистичный подход хорошо сработал: я запустил реализацию FFT из пакета JTransforms и получил те же результаты, что в Octave. Я думаю, здесь отчасти карго-культ, но блин, это работает!
Запуск xcorr на потоке
Алгоритм выше предполагает, что
audio
представляет собой массив, в котором мы ищем jingle
. Это не совсем подходит для радиотрансляции, где у нас непрерывный поток звука. Чтобы запустить анализ, я создал циклический буфер чуть больше, чем продолжительность джингла, который нужно распознать. Входящий поток заполняет буфер, и как только он заполнен, запускается тест кросс-корреляции. Если ничего не найдено, то самая старая часть буфера отбрасывается — и снова ожидаем его заполнения.Я немного поэкспериментировал с длиной буфера и получил наилучшие результаты с размером буфера в 1,5 раза больше размера джингла.
Объединяем всё вместе
Получить поток в формате PCM несложно. Это можно сделать с помощью вышеупомянутого
ffmpeg
. Команда ниже перенаправляет поток на стандартный вход java
, а затем выводит Got jingle 0
или Got jingle 1
, когда в потоке найден соответствующий образец.ffmpeg -loglevel -8 \
-i http://stream3.polskieradio.pl:8904/\;stream \
-f s16le -acodec pcm_s16le - \
| java -jar target/analyzer-1.0.0-SNAPSHOT-jar-with-dependencies.jar \
2 \
src/test/resources/commercial-start-44.1k.raw 500 \
src/test/resources/commercial-end-44.1k.raw 700
Автономная версия
Я также подготовил простую автономную версию анализатора, которая сама подключается к потоку «Тройки» (без внешнего
ffmpeg
) и воспроизводит результат с помощью javax.sound
. Всё вмещается в один файл JAR и содержит базовый пользовательский интерфейс с кнопками Star и Stop. Его можно скачать здесь. Если не любите запускать на своей машине чужие JAR (что совершенно правильно), то все исходники лежат на GitHub.Похоже, всё работает как надо :)
Дальнейшая работа
Конечная цель — отключить рекламу на уровне аппаратного усилителя, получая «реальный» FM-сигнал, а не некий интернет-поток. Об этом рассказано в следующей статье.
Обновление (июнь 2018)
Обсуждение на Hacker News
Обсуждение на Wykop
Обсуждение на Reddit