Pull to refresh

Порт SoX для Android или попытка создать идеальный плеер

Reading time 4 min
Views 8K
Здравствуй, Хабраюзер!

Несколько месяцев назад у меня появилась идея создания плеера под Android с огромным количеством эффектов. Единственным на то время (не знаю как сейчас:) плеером, с хоть какой-то обработкой звука, был PowerAMP, но количество аудио-эффектов в нем было мягко говоря скудным. Я попробовал реализовать эту идею. Из этого мало что вышло, но то, что получилось я расскажу в этом топике. Итак, кого заинтересовало, прошу под кат.

В поисках библиотеки...


Для начала я решил найти ту самую OpenSource библиотеку, которая может гибко обрабатывать звук, применяя различные эффекты. В конце концов я случайно познакомился с SoX. С первого взгляда это была идеальная библиотека для реализации такого проекта, но, оказалось, не все так просто…

Первые проблемы


По ходу знакомства с SoX я обнаружил такие проблемы:
1. Библиотека отлично работает на всех основных ОС, НО не оптимизирована под ARM.
2. Описание API библиотеки, как и само API, было достаточно скудным, почти все функции было проще реализовывать через командную строку.
3. SoX, вместо того, чтобы использовать один FFmpeg для декодирования, использует целый ряд библиотек (можете посмотреть на официальном сайте). Следовательно, все эти библиотеки так же придется собирать под Android NDK.
4. SoX отлично воспроизводил и декодировал практически любые форматы, но, при применении эффектов, аудио можно было либо декодировать в файл или byte массив, либо воспроизвести через Alsa/CoreAudio. Третий вариант не подходил однозначно, так как в Андроид Alsa теоретически есть, но работает не всегда и, вообще, это не рекомендованый метод воспроизведения звука. Следовательно, единственный вариант — декодировать все в byte массив и отдавать его в Java (AudioTrack). Но даже это реализовать оказалось достаточно сложно, так как… объясню на примере:)
Вот часть кода официального примера применения обработки звука с использованием эффектов:


  assert(sox_init() == SOX_SUCCESS);
  assert(in = sox_open_read(argv[1], NULL, NULL, NULL));
  /* Change "alsa" in this line to use an alternative audio device driver: */
  assert(out= sox_open_write("default", &in->signal, NULL, "alsa", NULL, NULL));

  chain = sox_create_effects_chain(&in->encoding, &out->encoding);

  e = sox_create_effect(sox_find_effect("input"));
  args[0] = (char *)in, assert(sox_effect_options(e, 1, args) == SOX_SUCCESS);
  assert(sox_add_effect(chain, e, &in->signal, &in->signal) == SOX_SUCCESS);

  e = sox_create_effect(sox_find_effect("trim"));
  args[0] = "10", assert(sox_effect_options(e, 1, args) == SOX_SUCCESS);
  assert(sox_add_effect(chain, e, &in->signal, &in->signal) == SOX_SUCCESS);

  if (in->signal.rate != out->signal.rate) {
    e = sox_create_effect(sox_find_effect("rate"));
    assert(sox_effect_options(e, 0, NULL) == SOX_SUCCESS);
    assert(sox_add_effect(chain, e, &in->signal, &out->signal) == SOX_SUCCESS);
  }

  if (in->signal.channels != out->signal.channels) {
    e = sox_create_effect(sox_find_effect("channels"));
    assert(sox_effect_options(e, 0, NULL) == SOX_SUCCESS);
    assert(sox_add_effect(chain, e, &in->signal, &out->signal) == SOX_SUCCESS);
  }

  e = sox_create_effect(sox_find_effect("output"));
  args[0] = (char *)out, assert(sox_effect_options(e, 1, args) == SOX_SUCCESS);
  assert(sox_add_effect(chain, e, &in->signal, &out->signal) == SOX_SUCCESS);

  sox_flow_effects(chain, NULL, NULL);

В данном случае аудио декодируется, и применяется несколько эффектов, затем все выводится через ALSA. Как видите для того, чтобы создать effect_chain надо сначала иметь 2 потока: для чтения и для вывода информации. Вот официальный пример декодирования аудио файла по частям в byte array:

  assert(sox_init() == SOX_SUCCESS);

  /* Open the input file (with default parameters) */
  assert(in = sox_open_read(argv[1], NULL, NULL, NULL));
#if defined FIXED_BUFFER
  assert(out = sox_open_mem_write(buffer, buffer_size, &in->signal, NULL, "sox", NULL));
#else
  assert(out = sox_open_memstream_write(&buffer, &buffer_size, &in->signal, NULL, "sox", NULL));
#endif
  while ((number_read = sox_read(in, samples, MAX_SAMPLES)))
    assert(sox_write(out, samples, number_read) == number_read);

Как видите, в данном случае аудио декодируется частями, и программа получает буфер. Этот пример полностью рабочий, НО в нем практически невозможно применить эффекты (что и является целью программы), потому что для этого пришлось бы для каждой части аудио (размеров в 16484 байта) создавать поток чтения и записи в другой буфер и затем эти потоки обрабатывать. У меня это не вышло, да и ни в одном примере такая возможность не описана.
Тогда логично было бы декодировать все аудио с применением эффектов в один буффер, НО этот вариант использует много оперативной памяти (когда я тестировал доходило до 100 мб при декодировании небольшого аудио файла в 10-11 мегабайт).

Но все-таки решено попробовать...


Не смотря на эти проблемы, я решил начать портировать SoX и все сопутствующие библиотеки. Неделя ушла на знакомство с Android NDK. В принципе портирование было не сложным, однотипным процессом, который я описывал в прошлой статье. Некоторые библиотеки уже были собраны (например FFmpeg). В конце концов я получил скомпилированный sox.so, который действительно работал и работает сейчас:) Пришло время решить проблему 3. Все оказалось не слишком сложно — создал отдельный поток, который по мере заполнения буфера отправляет его в Java программу, а та его воспроизводит. Проблема с памятью почти решена (все-таки ее используется много, но приложение это не роняет). Я получил приложение которое воспроизводит почти все форматы аудио и добавляет к нему эффекты (сейчас flanger, можно добавить любой другой). С одной стороны уже можно продолжать разработку, НО я получил сразу две новые проблемы:

1. Программа слишком сильно использует процессор (доходит до 80% при декодировании). Оптимизировать С код под ARM я не умею, поэтому решить это самостоятельно не вижу возможным.
2. Программа работает нестабильно. Приблизительно 1-2 из 10 запусков она ничего не воспроизводит (тестировал на Galaxy Tab). Эту проблему я так же не мог решить…

Итого, чтобы работа не пропала, я решил опубликование весь код на GitHub и написать статью тут. Ссылка на код. Если кто-то видит возможности решить эти проблемы — пишите мне в email или в личку. Надеюсь моя задумка кого либо заинтересует:)
Tags:
Hubs:
+22
Comments 44
Comments Comments 44

Articles