X-Meter
(проект для обучения и использования в качестве заготовки)

В качестве основы использована программа RLC-Meter.
Дает представление о том, как работать со звуком в среде Windows – генерировать и оцифровывать сигналы с помощью звуковой карты.
Для среды разработки Borland C++ Builder (BCB) 1.0.

Download – Скачать исходный код v2.15 (архив 168 кБ).


Что делает приложение:

– при наличии звуковой карты запускает запись и воспроизведение (16 бит стерео на частоте 44100 Гц),
– на выходе звуковой карты генерирует синус 11025 Гц, который может быть подан на вход,
– оцифровывает сигнал и измеряет амплитуды в правом и левом каналах,
– амплитуду в правом канале отображает на стрелочной шкале, а также дублирует ее числовым значением,
– амплитуду в левом канале отображает столбиком «Level» («контроль за перегрузкой»),
– измеряет загрузку процессора процедурой обработки сигнала.


Описание работы

Собственно весь рабочий код сосредоточен в файле Unit1.cpp. Остальные файлы генерируются средой BCB автоматически. Для тех, кто впервые сталкивается с визуальным программированием, сообщаю основную концепцию: все элементы приложения (кнопки, надписи и проч.) выбираются из палитры компонентов и размещаются в будущем окне, а задача программиста – написать код, реагирующий на события (которые могут генерироваться этими же элементами).

Пустые процедуры-обработчики событий BCB генерирует также автоматически. Вам остается только заполнить их своим кодом. В приложении используется четыре таких обработчика (имена система дала по умолчанию):

TForm1::TForm1( ) – сюда передается управление после старта приложения (для инициализация переменных и прочей подготовки),
TForm1::FormClose( ) – процедура, вызываемая при закрытии приложения (чтобы корректно завершить процессы и убрать за собой всякий «мусор»),
TForm1::Button3Click( ) – нажатие на кнопку «?» (вывод информационного сообщения),
TForm1::PaintBox1Paint( ) – процедура, вызываемая при запросе на перерисовку изображения шкалы (компонент PaintBox1 сам о себе не заботится, например, когда приложение сворачивается, а потом опять разворачивается).

Кроме того, для работы со звуком потребуются два специальных обработчика – по завершению работы с очередной порцией данных (здесь имена даем сами):

callback_play_fun( ) – очередной буфер данных проигран,
callback_record_fun( ) – буфер заполнен оцифрованными данными.

Если говорить строго, то две последние процедуры являются обработчиками сообщений Windows, из потока которых надо вычленить только нужные нам «WOM_DONE» и «WIM_DATA» соответственно.

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

1. Заполняем структуру strFormat требуемыми значениями скорости, разрядности и проч. Она понадобится на следующем шаге.
2. Открываем аудио-устройства с помощью API-функций Windows waveOutOpen( ) для генерации звука и waveInOpen( ) для оцифровки. В качестве параметров передаем адрес этой структуры и еще говорим, что будем использовать механизм CALLBACK_FUNCTION, т.е. способ взаимодействия с драйвером путем приема сообщений от Windows. Адрес обработчика сообщений также передается в качестве одного из параметров.
3. Если открытие прошло успешно (нулевой результат), то все готово для процесса, осталось только его запустить.
4. В качестве «толчка» можно искусственно симулировать подачу сообщений «WOM_DONE» и «WIM_DATA». После этого все действие сведется к периодическому вызову функций callback_play_fun( ) и callback_record_fun( ) и жонглированию буферами. Частота вызовов будет определяться длиной буферов и скоростью их проигрывания/оцифровки. Но четкой периодичности не будет, поскольку Windows – среда многозадачная, и указанные вызовы будут временно прерываться или откладываться из-за других процессов. Это не играет большой роли при условии, что возможные задержки меньше времени проигрывания (или записи) одного буфера.

Написание корректно работающих обработчиков – наиболее важная часть работы. Получив сообщение «WOM_DONE», процедура должна сгенерировать новые данные для того буфера, который только что проигрался, а получив сообщение «WIM_DATA» – обработать данные того буфера, оцифровка которого была завершена. По окончании буфер следует вновь поставлен на очередь. Чтобы процесс не прерывался – по крайней мере один буфер должен всегда стоять в очереди. Этого можно достичь использованием двух и более буферов (в данном приложении используется двойная буферизация).

Буферы на очередь ставятся командами waveOutWrite( ) и waveInAddBuffer( ). Особенность Windows такова, что эти команды должны предваряться заполнением структур WAVEHDR для каждого буфера и вызовами функций waveOutPrepareHeader( ) и waveInPrepareHeader( ) соответственно. Неизменяемые элементы этих структур (адрес буферов и длина) можно заполнить однократно, на этапе начальной подготовки. В переменной lpWaveHdr возвращается адрес структуры WAVEHDR только что «вернувшегося» блока, так что из элемента lpData этой структуры можно извлечь указатель на область данных, а из dwBufferLength – ее длину и использовать их далее в цикле обработки. Обработка оцифрованного сигнала использована простейшая – для каждого канала (левого и правого) подсчитываются суммы квадратурных (т.е. сдвинутых на 90 градусов) составляющих амплитуды для частоты 11025 Гц (1/4 от частоты оцифровки), затем эти суммы нормируются (завершается усреднение), и далее по ним вычисляется амплитуда.

При генерации синуса никаких дополнительных вычислений не производится, и буфер новыми данными не заполняется – все просчитано заранее на этапе начальной инициализации. Буфер просто вновь ставится в очередь. Можно было бы «рассчитывать» зачения синуса и на лету (кавычки потому, что алгоритм их генерации, см. конец процедуры PrepareAll( ), вас наверняка улыбнет) – это практически не загружает процессор.

Для определения степени загрузки используется внутренний счетчик тактов, имеющийся в каждом современном процессоре. Функция QueryPerformanceCounter( ) делает «моментальный снимок» этого счетчика. По разнице показаний и тактовой частоте определяется время выполнения критического участка программы. В нашем случае вычисляется время обработки одного буфера данных. Если это время превысит время заполнения буфера, то это означает, что процессор не справляется с задачей, и загрузка превышает 100%. Результатом будут пропуски полученных данных.

Об оптимальной длине буфера.

С уменьшением длины растет доля времени, приходящаяся на вспомогательные операции (постановка буфера в очередь, вызов Callback-функции и проч.). Кроме того, часть времени (да еще случайным образом) будут отнимать другие процессы, запущенные в среде Windows. Поэтому существует некоторое минимальное значение, зависящее от быстродействия и конфигурации системы, которого надо избегать (использовать буфер с запасом). С другой стороны, при увеличении длины буфера увеличивается время реакции вашей программы на входные данные – ведь вызовы Callback-функции будут случаться реже. Если компромисс не находится, то выходом может стать увеличение количества буферов.

О переносимости.

При попытках компилятора ругаться на непонятные слова вроде waveInPrepareHeader включите в программу строку:

#include "mmsystem.h"
Если программа будет переноситься в другую среду разработку (или в BCB более старшей версии), то вам придется провести ревизию операций по выводу информации на экран (изображения шкалы, стрелки и числовых значений). При тестировании на многоядерных системах приложения, сгенерированного BCB1, выяснилось, что компонент TImage, использованный для отображения шкалы и стрелки, приводит к зависаниям. Вместо него применен другой компонент – TPaintBox, который, однако, требует дополнительных усилий по сохранению внешнего вида при внешних воздействиях (перекрытии другими окнами, сворачивании/разворачиваении приложения и т.п.).


antiradio.narod.ru/x-meter
Дата создания документа: 21.11.2012. Последнее обновление: 23.11.2012.

Сайт создан в системе uCoz