Мультимедиа. Работа со звуком
Цель лекции: Научиться создавать приложения с поддержкой звука
Изначально в IBM-совместимых персональных компьютерах вся работа со звуком заключалась в подаче сигналов с помощью встроенного динамика, а каких-либо средств для воспроизведения музыки вовсе не было, поскольку предполагалось, что персональные компьютеры будут использоваться исключительно в деловой сфере. Однако такое положение не устраивало конечных пользователей, и вскоре появилось довольно большое число звуковых карт, позволявших обеспечить качественное воспроизведение звука.
В наши дни к мультимедийным возможностям программ предъявляются довольно жёсткие требования. К счастью, библиотека Juce, изначально ориентированная именно на создание подобных приложений, предоставляет разработчику множество классов для воспроизведения и записи звука. Детальный их разбор потребовал бы написания отдельной книги, поэтому в данной главе мы коснёмся лишь базовых принципов взаимодействия некоторых из них на простейших примерах, чтобы дать читателю основу для будущей самостоятельной работы с документацией к библиотеке.
Воспроизведение аудио-файлов базовыми методами
За воспроизведение аудио-файлов в библиотеке Juce отвечает класс AudioTransportSource, который создаёт собственную нить (поток) и позволяет начинать проигрывание с произвольной позиции, а также в любой момент останавливать его.
Для того, чтобы объект класса AudioTransportSource мог "понять" данные аудио-файла, последний должен быть прочитан с помощью объекта класса, унаследованного от AudioFormat. Библиотека Juce предоставляет возможность воспроизведения ряда базовых форматов аудио:
- AiffAudioFormat — читает и записывает файлы несжатого аудио, Audio Interchane File Format (*.aif, *.aiff);
- FlacAudioFormat — читает и записывает файлы аудио, сжатого без потерь качества, Free Lossless Audio Codec (*.flac);
- OggVorbisAudioFormat — читает и записывает файлы аудио, сжатого с потерей качества, Ogg Vorbis (*.ogg);
- WavAudioFormat — читает и записывает файлы несжатого аудио, Waveform Audio File Format (*.wav).
Для хранения информации о доступных форматах аудио и принятия решения о том, какой из них использовать для открытия текущего файла, в библиотеке имеется класс AudioFormatManager, который в том числе создаёт чтеца (класс AudioFormatReader), читающего сэмплы из аудио-потока.
Непосредственно управляет устройством воспроизведения звука (звуковой картой) объект класса AudioSourcePlayer, который служит промежуточным звеном между ним и объектом класса AudioTransportSource.
Рассмотрим взаимодействие всех этих классов на примере простого проигрывателя, воспроизводящего вышеперечисленные форматы аудио.
За основу будущего проекта возьмём пример из "Стандартные диалоги" , в котором для открытия файла использовался FilenameComponent. При запуске программы в её ярлыке будет отображаться название аудио-устройства, используемого по умолчанию, а при открытии аудио-файла — его имя ( рис. 19.1).
Все объекты, отвечающие за воспроизведение аудио-файлов, объявим в качестве закрытых членов компонента содержимого ( пример 19.1).
#ifndef _TCentralComponent_h_ #define _TCentralComponent_h_ //-------------------------------------------------- #include "../JuceLibraryCode/JuceHeader.h" //-------------------------------------------------- class TCentralComponent : public Component, public ButtonListener, public ChangeListener { public: TCentralComponent(); ~TCentralComponent(); void paint(Graphics&); void resized(); // Функция, отслеживающая щелчки по кнопке void buttonClicked(Button*); void changeListenerCallback(ChangeBroadcaster*); private: // Текущий файл для воспроизведения File CurrentFile; // Менеджер аудио- и MIDI-устройств AudioDeviceManager* pAudioDeviceManager; // Аудио-устройство AudioIODevice* pAudioDevice; // Промежуточное звено между источником звука // и аудио-устройством AudioSourcePlayer AudioPlayer; // Источник воспроизведения звука AudioTransportSource TransportSource; // Класс для принятия данных от чтеца аудио-потока AudioFormatReaderSource* pCurrentAudioFileSource; // Ярлык для вывода названия аудио-устройства // при запуске программы, а в последующем - // названия открытого аудио-файла Label* pNameLabel; // Кнопка вызова диалога выбора файла TextButton* pChooseButton; // Кнопка начала / остановки воспроизведения TextButton* pPlayButton; // Компонент выбора файла FilenameComponent* pFileNameDialog; // Предотвращает создание копии конструктора и оператора = TCentralComponent(const TCentralComponent&); const TCentralComponent& operator= (const TCentralComponent&); }; //-------------------------------------------------- #endifЛистинг 19.1. Объявление класса компонента содержимого TCentralComponent (файл TCentralComponent.h)
Их инициализация и установление связей между ними, необходимых для совместной работы, представлены в листинге 19.2 .
Листинг
#include "TCentralComponent.h" //---------------------------------------------- #define tr(s) String::fromUTF8(s) //---------------------------------------------- TCentralComponent::TCentralComponent() : Component("Central Component"), pNameLabel(0), pChooseButton(0) { pNameLabel = new Label(L"Name Label", tr("Выберите файл...")); pNameLabel->setFont(Font(15.0000f, Font::bold)); pNameLabel->setJustificationType(Justification::centred); pNameLabel->setEditable(false, false, false); pNameLabel->setColour(Label::textColourId, Colours::black); pNameLabel->setColour(Label::backgroundColourId, Colours::azure); pNameLabel->setColour(Label::outlineColourId, Colours::black); addAndMakeVisible(pNameLabel); pChooseButton = new TextButton(L"Choose Button"); pChooseButton->setButtonText(tr("Выбрать...")); pChooseButton->addListener(this); addAndMakeVisible(pChooseButton); pPlayButton = new TextButton(L"Play Button"); pPlayButton->setButtonText(tr("Играть")); pPlayButton->setColour(TextButton::buttonColourId, Colours::lightgreen); pPlayButton->addListener(this); addAndMakeVisible(pPlayButton); // Файл для воспроизведения ещё не открыт, аудио-поток пока отсутствует pCurrentAudioFileSource = 0; LastOpenedFile = File::getCurrentWorkingDirectory(); // Создаём пустой менеджер аудио-устройств pAudioDeviceManager = new AudioDeviceManager(); // Инициируем менеджер аудио-устройств String sError = pAudioDeviceManager->initialise(1, 2, 0, true); // Сохраняем текущее аудио-устройство в переменной pAudioDevice = pAudioDeviceManager->getCurrentAudioDevice(); // Если устройство отсутствует, выводим сообщение об ошибке if(!sError.isEmpty()) { AlertWindow::showMessageBox( AlertWindow::WarningIcon, tr("Ошибка воспроизведения"), (tr("Не удалось открыть аудио-устройство:\n\n") += sError), tr("Принять"), 0 ); } // Устанавливаем связи между менеджером устройств, // AudioSourcePlayer и AudioTransportSource pAudioDeviceManager->addAudioCallback(& AudioPlayer); TransportSource.addChangeListener(this); AudioPlayer.setSource(& TransportSource); // Если есть доступное аудио-устройство... if(pAudioDevice != 0) { // ...выводим его название на ярлык программы String sName = pAudioDevice->getName(); pNameLabel->setText(sName, false); } else { pNameLabel->setText(tr("Нет устройства!"), false); } setSize(400, 150); // Создаём компонент для выбора файла pFileNameDialog = new FilenameComponent( String::empty, CurrentFile, true, false, false, "*.wav;*.aif;*.aiff;*.ogg;*.flac", String::empty, tr("Выберите файл...") ); File SearchDir = SearchDir.getSpecialLocation(File::userMusicDirectory); pFileNameDialog->setDefaultBrowseTarget(SearchDir); // Задаём его размер (иначе его не будет видно) pFileNameDialog->setSize(250, 25); }Листинг 19.2. Реализация конструктора класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)
В конструкторе класса компонента содержимого мы создаём и инициализируем менеджер аудио-устройств.
Метод const String AudioDeviceManager::initialise(int numInputChannelsNeeded, int numOutputChannelsNeeded, const XmlElement* savedState, bool selectDefaultDeviceOnFailure, const String& preferredDefaultDeviceName = String::empty, const AudioDeviceSetup* preferredSetupOptions = 0) пытается открыть все доступные для использования аудио-устройства, начиная с устройства по умолчанию. Возвращаемая строка содержит сообщение об ошибке, если что-то пошло не так. В том случае, если открытие аудио-устройств прошло успешно, возвращается пустая строка.
Метод AudioIODevice* AudioDeviceManager::getCurrentAudioDevice() const throw() возвращает указатель на текущее аудио-устройство. Мы сохраняем его в члене класса компонента содержимого pAudioDevice для последующих обращений.
Затем мы устанавливаем связь между менеджером аудио-устройств и AudioSourcePlayer посредством вызова метода void AudioDeviceManager::addAudioCallback(AudioIODeviceCallback* newCallback). Удалить связь позволяет метод void AudioDeviceManager::removeAudioCallback(AudioIODeviceCallback* callback).
Завершаем установку всех необходимых соединений установкой TransportSource в качестве источника воспроизведения для AudioSourcePlayer. Достигается это вызовом метода void AudioSourcePlayer::setSource(AudioSource* newSource).
Заметим, что в нашей программе используются настройки текущего аудио-устройства по умолчанию. Однако библиотека Juce включает структуру, принадлежащую классу AudioDeviceManager— AudioDeviceSetup — хранящую набор свойств текущего аудио-устройства и дающую возможность пользователю их изменять на этапе выполнения программы. Пример использования AudioDeviceSetup вы можете найти в демонстрационном приложении Juce (выберите в меню Demo > Audio). Посмотреть реализацию примера можно, перейдя в папку <каталог Juce>/juce/extras/JuceDemo/Source/demos и открыв файл AudioDemoSetupPage.cpp.
При первом запуске нашей программы на её ярлыке мы отображаем имя используемого аудио-устройства. Для его получения воспользуемся методом const String& AudioIODevice::getName() const throw().