AdBlock для радио

в 15:11, , рубрики: dst, fft, JTransforms, octave, блокировка рекламы, звук, Работа со звуком

Автор статьи — польский программист Томек Рекавек, разрабатывает проект Jackrabbit Oak в рамках Apache Software Foundation для Adobe. Статья опубликована в личном блоге автора 24 февраля 2016 года.

Польское «Радио-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);

Получится такой график:

AdBlock для радио - 1

Хорошо заметен пик, описывающий положение jingle.wav в audio.wav. Что меня удивило, так это простота метода: всю работу делает xcorr(), остальной код только для чтения файлов и отображения результата.

Я хотел реализовать тот же алгоритм на Java, и тогда у меня будет инструмент, который:

  1. считывает аудиопоток со стандартного входа (например, от ffmpeg),
  2. анализирует его в поиске джинглов,
  3. выводит тот же поток на 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.

AdBlock для радио - 2

Похоже, всё работает как надо :)

Дальнейшая работа

Конечная цель — отключить рекламу на уровне аппаратного усилителя, получая «реальный» FM-сигнал, а не некий интернет-поток. Об этом рассказано в следующей статье.

Обновление (июнь 2018)

Обсуждение на Hacker News
Обсуждение на Wykop
Обсуждение на Reddit

Автор: m1rko

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js