Как работать с микшером
Оформил: DeeCo
Автор: Вадим Забродин
Статья посвящена вопросам по работе с микшером
Windows посредством API. В качестве примера работы предлагается код,
выполняющий многие функции стандартного микшера Windows.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes,
Graphics, Controls, Forms, Dialogs,
StdCtrls, ComCtrls, ExtCtrls,
//
// НЕ ЗАБУДЬТЕ указать mmsystem в uses
//
mmsystem;
type
TFormMixer = class(TForm)
cb_devs: TComboBox;
cb_line: TComboBox;
cb_ctrl: TComboBox;
Label1: TLabel;
label_type: TLabel;
Label2: TLabel;
label_count: TLabel;
Label3: TLabel;
label_bounds: TLabel;
cb_chg: TCheckBox;
tb_chg: TTrackBar;
pb_chg: TProgressBar;
Timer1: TTimer;
cbox_chg: TComboBox;
cb_dest: TComboBox;
lbl_Error: TLabel;
procedure FormCreate(Sender: TObject);
procedure cb_devsChange(Sender: TObject);
procedure cb_lineChange(Sender: TObject);
procedure cb_ctrlChange(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure tb_chgChange(Sender: TObject);
procedure cb_chgClick(Sender: TObject);
procedure cbox_chgChange(Sender: TObject);
procedure cb_destChange(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
procedure smchg(var msg: TMessage); message MM_MIXM_CONTROL_CHANGE;
procedure smchg2(var msg: TMessage); message MM_MIXM_LINE_CHANGE;
end;
TMIXERCONTROLDETAILS_BOOLEAN = record
fValue: longint;
end;
TMIXERCONTROLDETAILS_UNSIGNED = record
dwValue: DWORD;
end;
var
FormMixer: TFormMixer;
implementation
{$R *.DFM}
var
numdevs: integer; // Кол-во устройств (микшеров)
mixer: HMIXER; // Handle микшера
mixerlineid: array[0..255] of integer; // Уникальный ID дорожки микшера
m_ctrl: array[0..255] of TMIXERCONTROL; // Элементы управления дорожки микшера
ctrl_sel: integer;
procedure TFormMixer.FormCreate(Sender: TObject);
var
i: integer;
caps: TMIXERCAPS;
begin
//
// Узнаем кол-во микшеров (т.е. звуковых карт)
//
numdevs := mixerGetNumDevs;
if numdevs = 0 then
begin
showmessage('Нет ваших миксеров... а звуковушка у вас есть?');
exit;
end;
//
// Через mixerGetDevCaps можно узнать много чего, например версию драйвера,
// но нам нужно только название микшера
//
for i := 0 to numdevs - 1 do
begin
mixerGetDevCaps(i, @caps, sizeof(caps));
cb_devs.Items.Add(caps.szPName);
end;
cb_devs.ItemIndex := 0;
cb_devs.OnChange(self);
end;
procedure TFormMixer.cb_devsChange(Sender: TObject);
var
caps: TMIXERCAPS;
line: TMIXERLINE;
MaxLinecnt: integer;
i, err: integer;
maxSrc: integer;
curDest: integer;
curSrc: integer;
begin
//
// Чтобы познакомиться поближе с микшером - открываем его. Заодно
// передаём ему handle нашего окна, для callback'ов (на случай если кто-то
// еще будет менять установки микшера, например через стандартный микшер винды)
//
mixerOpen(@mixer, cb_devs.ItemIndex, handle, 0, CALLBACK_WINDOW);
//
// Опять узнаем способности выбранного микшера
//
mixerGetDevCaps(mixer, @caps, sizeof(caps));
//
// Здесь небольшая непонятка для неподготовленного: в mixerGetDevCaps мы подставили
// handle микшера, хотя всего несколько строк назад мы подставляли его порядковый номер.
// Это НЕ ошибка. mixerGetDevCaps понимает и то и другое. Кто-то просто хотел облегчить
// жизнь простому API'шному программисту, но из-за этого возникла путаница.
//
// "Хотели как лучше, вышло как всегда" (иногда мне кажется, что это девиз MicroSoft)
//
MaxLinecnt := 255;
cb_dest.Clear;
//
// В микшере дорошки управления делятся на группы по "Направлению".
// У каждой звуковушки есть, как минимум, два "Направления" (destinations)
// "Воспроизведение", "Запись". (Их может быть и больше, но это реже).
//
// Для того чтобы узнать какие группы есть у данной звуковушки вызываем mixerGetLineInfo(...
// Этой функции подставляем handle микшера (Здесь и далее уже не действует порядковый номер),
// структуру TMIXERLINE, в которой устанавливаем её длину (.cbStruct) и порядковый номер группы
// (.dwDestination), и устанавливаем нужные флаги.
//
// MIXER_GETLINEINFOF_DESTINATION - мы хотим узнать info по группе dwDestination
// MIXER_OBJECTF_HMIXER - подставленный handle - микшера.
//
// В результате вызова структура TMIXERLINE заполняется кучей полезных данных, из которых
// мы берем пока только имя группы (.szname)
//
// Повторяем вызов до тех пор, пока mixerGetLineInfo(... не вернет ошибку, что у нее закончились группы.
//
line.cbStruct := sizeof(line);
curDest := -1;
for i := 0 to maxlinecnt do
begin
inc(curDest);
line.dwDestination := curDest;
err := mixerGetLineInfo(mixer, @line, MIXER_GETLINEINFOF_DESTINATION or
MIXER_OBJECTF_HMIXER);
if err <> 0 then
break;
cb_dest.Items.add(line.szname);
end;
cb_dest.ItemIndex := 0;
cb_dest.OnChange(self);
end;
procedure TFormMixer.cb_destChange(Sender: TObject);
var
caps: TMIXERCAPS;
line: TMIXERLINE;
MaxLinecnt: integer;
i, err: integer;
maxSrc: integer;
curDest: integer;
curSrc: integer;
begin
curDest := cb_dest.ItemIndex;
// Терерь мы хотим узнать какие дорожки есть в выбранной группе
// (чтобы когда нибудь в оддаленном будущем добраться и до элементов управления дорожек ;) ).
//
// Для этого заново вызываем mixerGetLineInfo(... с просьбой снова предоставить info по выбранной группе
//
line.cbStruct := sizeof(line);
line.dwDestination := curDest;
err := mixerGetLineInfo(mixer, @line, MIXER_GETLINEINFOF_DESTINATION or
MIXER_OBJECTF_HMIXER);
maxSrc := line.cConnections;
//
// В (.cConnections) хранится очень важная для нас информация - кол-во дорожек микшера
// входящих в данную группу
//
cb_line.Clear;
mixerlineid[0] := line.dwLineID;
cb_line.Items.add(line.szname);
//
// !Attention! Здесь опять путаница:
// Группа дорожек сама является дорожкой микшера (советую перечитать эту фразу еще раз),
// у которой могут быть свои элементы управления (к которым мы тоже хотим получить доступ).
// именно поэтому мы добавили в список дорожек cb_line и само название группы дорожек.
//
//
// Далее практически повторяется цикл по перебору групп микшера. Только теперь
// (.dwDestination) всегда равен порядковому номеру выбранной группы, а меняется
// поле (.dwSource) в которое мы подставляем порядковый номер дорожки.
//
// Ну и конечно меням флаг на MIXER_GETLINEINFOF_SOURCE
//
curSrc := 0;
for i := 0 to maxSrc - 1 do
begin
line.dwDestination := curDest;
line.dwSource := i;
err := mixerGetLineInfo(mixer, @line, MIXER_GETLINEINFOF_SOURCE or
MIXER_OBJECTF_HMIXER);
if err <> 0 then
break;
cb_line.Items.add('- ' + line.szname);
mixerlineid[i + 1] := line.dwLineID;
end;
//
// Касательно массива mixerlineid, который мы заполняем уникальными идентификаторами дорожки (.dwLineID) -
// это сделано не из природного стремления сохранить под боком побольше информации, эти ID нам еще понадобятся.
// (Конечно можно каждый раз вызывать mixerGetLineInfo(... и получать эти ID на блюдечке - это тоже ничему не противоречит)
//
cb_line.ItemIndex := 0;
cb_line.OnChange(self);
end;
procedure TFormMixer.cb_lineChange(Sender: TObject);
var
LineID: integer;
line: TMIXERLINE;
ctrl: TMIXERLINECONTROLS;
i: integer;
begin
//
// Итак, теперь мы хотим узнать какие элементы управления есть у выбранной дорожки микшера.
//
cb_ctrl.Clear;
LineId := mixerlineid[cb_line.ItemIndex]; // Вспоминаем ID дорожки
//
// И опять пользуемся вездесущей mixerGetLineInfo(... !!!
//
// Только теперь мы устанавливаем поле (.dwLineID) и ставим флажок
// MIXER_GETLINEINFOF_LINEID
//
line.cbStruct := sizeof(line);
line.dwLineID := LineID;
mixerGetLineInfo(mixer, @line, MIXER_GETLINEINFOF_LINEID);
//
// Из этого вызова мы выносим важную величину - (.cControls),
// она рассказывает нам сколько элементов управления у данной дорожки
//
if line.cControls = 0 then
exit;
//
// Теперь нам понадобится новая структура TMIXERLINECONTROLS, которя является
// заголовком для массива структур TMIXERCONTROL (то есть самих элементов управления)
//
// В TMIXERLINECONTROLS устанавливаем следующие св-ва:
//
// cbStruct - размер TMIXERLINECONTROLS
// cbmxctrl - размер одного TMIXERCONTROL
// dwLineID - ID дорожки для которой мы хотим узнать список контролей
// cControls - кол-во элементов привязанных к данной дорожке
// (Важно установить его правильно, иначе мы получим элементов меньше чем надо или получим ошибку)
// pamxctrl - указатель на массив TMIXERCONTROL
//
ctrl.cbStruct := sizeof(ctrl);
ctrl.cbmxctrl := sizeof(TMIXERCONTROL);
ctrl.dwLineID := LineID;
ctrl.cControls := line.cControls;
ctrl.pamxctrl := @m_ctrl;
//
// После заполнения структуры вызываем mixerGetLineControls(... с флажком MIXER_GETLINECONTROLSF_ALL
// (для других целей можно использовать другие флаги, они описаны в SDK по mixerGetLineControls, но мы сейчас
// пишем микшер, так что нам надо все).
//
// После вызова mixerGetLineControls заполняем cb_ctrl именами элементов управления
//
mixerGetLineControls(mixer, @ctrl, MIXER_GETLINECONTROLSF_ALL);
for i := 0 to ctrl.cControls - 1 do
begin
cb_ctrl.Items.Add(m_ctrl[i].szName);
end;
cb_ctrl.ItemIndex := 0;
cb_ctrl.OnChange(self);
end;
procedure TFormMixer.cb_ctrlChange(Sender: TObject);
var
txt: string;
i, err: integer;
mxc: TMIXERCONTROLDETAILS;
mxcd: TMIXERCONTROLDETAILS_UNSIGNED;
mxcl: array[0..100] of integer; //TMIXERCONTROLDETAILS_UNSIGNED;
begin
lbl_error.visible := false;
cb_chg.visible := false;
tb_chg.visible := false;
pb_chg.visible := false;
cbox_chg.visible := false;
timer1.Enabled := false;
ctrl_sel := cb_ctrl.itemindex;
//
// Перед началом работы с контролем дорожкой микшера проверим состоит ли
// она из одного item'а или нескольких (.cMultipleItems)...
//
// ДА! Бывает и такое. (Например в SDK сказано, что так и будет выглядеть эквалайзер встроенный
// в микшер. Сам эквалайзер будет одним элементом управления с item'ами для каждой полосы частот)
//
// Конечно, можно сойти с ума обрабатывая такие вещи, поэтому в данном примере мы ограничимся
// одним случаем "MultipleItem'овсти".
//
if m_ctrl[ctrl_sel].cMultipleItems = 0 then
begin
//
// Для выяснения подробностей элемента управления нам понадобится структура TMIXERCONTROLDETAILS,
// которая потребует следующего заполнения:
//
// cbStruct - размер TMIXERCONTROLDETAILS
// dwControlID - ID элемента управления (получен нами с помощью mixerGetLineControls )
// cChannels - кол-во каналов с которыми работает контроль
//
// (вообще говоря может быть 2,3,4 и т.д. Так что установка здесь 1 есть некий произвол, правда система
// обычно пытается его обойти: если каналов 2 а мы требуем 1, то обработка ведется как будто бы канал один
// и чтение/запись применяются сразу к двум каналам)
//
// Например: "Громкость" какой-то дорожки обычно два канала (левый и правый), но в данном примере мы ставим
// cChannels = 1 и управляем громкостью сразу двух каналов (баланс, конечно, теряется, но это пример,
// а не коммерческое приложение ;) )
//
//
// cbDetails - размер значения элемента управления (TMIXERCONTROLDETAILS_UNSIGNED)
//
// (тоже некий произвол... т.к. в общем случае размер не равен 4 байтам)
//
// paDetails - указатель на структуру TMIXERCONTROLDETAILS_UNSIGNED
//
mxc.cbStruct := sizeof(mxc);
mxc.dwControlID := m_ctrl[ctrl_sel].dwControlID;
MXC.cMultipleItems := 0;
mxc.cChannels := 1;
mxc.cbDetails := sizeof(mxcd);
mxc.paDetails := @mxcd;
err := mixerGetControlDetails(mixer, @mxc, 0);
if err <> 0 then
begin
lbl_error.visible := true;
exit;
end;
//
// После вызова mixerGetControlDetails заполняется структура mxc и mxcd (если все правильно)
// и в mxcd.dwValue лежит значение элемента управления
//
label_count.caption := inttostr(mxcd.dwValue);
end
else
begin
//
// Теперь более сложный случай - много item'ов внутри элемента управления.
//
// Ограничемся одним случаем (который правда есть почти в каждой звуковой карте).
// Этот случай - источник звука при записи. Хотя в стандартном микшере это выглядит
// как набор CheckBox'ов у каждой дорожки микшера, на самом деле это сложный элемент управления:
// бинарный массив (0-1) прикрепленный к группе "Запись".
//
// Далее показано как обработать его. Поступаем практически также, как и в случае с одиночным эл.управления.
//
// Отличия:
//
// cMultipleItems - кол-во значений в элементе
// cbDetails - размер массива значений (TMIXERCONTROLDETAILS_UNSIGNED)
// paDetails - указатель на массив
//
mxc.cbStruct := sizeof(mxc);
mxc.dwControlID := m_ctrl[ctrl_sel].dwControlID;
MXC.cMultipleItems := m_ctrl[ctrl_sel].cMultipleItems;
mxc.cChannels := 1;
mxc.cbDetails := sizeof(mxcl);
mxc.paDetails := @mxcl;
err := mixerGetControlDetails(mixer, @mxc, 0);
if err <> 0 then
begin
lbl_error.visible := true;
exit;
end;
//
// Т.к. мы рассматриваем случай источника звука, а в 99% карточек не может быть выбрано более одного источника,
// то ищем первое ненулевое значение и делаем вид, что мы считали один mxcd и в нем лежит номер дорожки-источника звука.
//
// (Произвол? Произвол, но работает на всех картах побывавших в моих руках от ESS866 до SB Audigy Platinum)
//
for i := 0 to MXC.cMultipleItems - 1 do
if mxcl[i] <> 0 then
begin
label_count.caption := inttostr(i);
mxcd.dwValue := i;
break;
end;
end;
//
// Теперь, когда мы знаем, чему равно значение элемента управления, неплохо бы узнать
// какой это элемент и чему может быть равно это значение
//
// Для этого смотрим значение dwControlType. Для того чтобы не разбираться со всеми вариантами
// мы накладываем маску MIXERCONTROL_CT_CLASS_MASK и у нас остается только класс элемента управления
//
// (Для любителей разбираться: см. Windows SDK статью "MIXERCONTROL")
//
case (m_ctrl[ctrl_sel].dwControlType and MIXERCONTROL_CT_CLASS_MASK) of
MIXERCONTROL_CT_CLASS_CUSTOM:
//
// Тут все ясно и без объяснений: Загадочный тип, его мы разбирать не будем
//
txt := 'Дополнительный';
MIXERCONTROL_CT_CLASS_FADER:
//
// Fader, он же регулятор. Практически все ползунки громкости - Fader'ы
//
begin
txt := 'Регулятор';
tb_chg.visible := true;
tb_chg.position := (mxcd.dwValue);
end;
MIXERCONTROL_CT_CLASS_LIST:
//
// Список значений... Мы договорились, что у нас только один список - источник звука и в нем
// установлено только одно значение.
//
begin
txt := 'Список';
cbox_chg.visible := true;
cbox_chg.Items.Clear;
for i := 1 to MXC.cMultipleItems do
begin
cbox_chg.Items.Add(cb_line.Items.Strings[i + cb_line.ItemIndex]);
end;
cbox_chg.ItemIndex := mxcd.dwValue;
end;
MIXERCONTROL_CT_CLASS_METER:
//
// Индикатор. На некоторых картах есть индикатор уровня громкости при Master Volume или Microphone
//
// Из-за того, что индикатор имеет свойство постоянно меняться, но при этом не возникает Message об изменении,
// в программу добавлен таймер, который запускается как только мы выбираем Meter.
//
begin
txt := 'Индикатор';
pb_chg.visible := true;
pb_chg.position := (mxcd.dwValue);
timer1.Enabled := true;
end;
MIXERCONTROL_CT_CLASS_NUMBER:
//
// Какое-то число... в своей работе не встречал
//
begin
txt := 'Число';
pb_chg.visible := true;
pb_chg.position := (mxcd.dwValue);
end;
MIXERCONTROL_CT_CLASS_SLIDER:
//
// Slider... Принципиальное отличие от Fader'а не улавливаю...
//
begin
txt := 'Движок';
tb_chg.visible := true;
tb_chg.position := (mxcd.dwValue);
end;
MIXERCONTROL_CT_CLASS_SWITCH:
//
// Switch, он же CheckBox (0 или 1)
//
begin
txt := 'Переключатель';
cb_chg.visible := true;
cb_chg.checked := (mxcd.dwValue = 1);
end;
MIXERCONTROL_CT_CLASS_TIME:
//
// Время... время чего - я так и не понял...
//
begin
txt := 'Время';
pb_chg.visible := true;
pb_chg.position := (mxcd.dwValue);
end;
end;
label_type.caption := txt;
//
// Границы изменения значения элемента управления
//
label_bounds.Caption :=
inttostr(m_ctrl[ctrl_sel].bounds.dwMinimum) + '-' +
inttostr(m_ctrl[ctrl_sel].bounds.dwMaximum);
end;
//
// Это CallBack процедуры. Необходимость в них возникает, если кто-то управляет микшером
// с другой программы
//
procedure TFormMixer.smchg(var msg: TMessage);
begin
cb_ctrl.OnChange(self);
end;
procedure TFormMixer.smchg2(var msg: TMessage);
begin
cb_ctrl.OnChange(self);
end;
//
// Timer нужен для отрисовки текущего положения индикатора.
//
// Алгоритм работы ничем не отличается от cb_ctrlChange. Только теперь мы знаем на что смотреть,
// это значительно упрощает процедуру.
//
procedure TFormMixer.Timer1Timer(Sender: TObject);
var
mxc: TMIXERCONTROLDETAILS;
mxcd: TMIXERCONTROLDETAILS_UNSIGNED;
err: integer;
begin
mxc.cbStruct := sizeof(mxc);
mxc.dwControlID := m_ctrl[ctrl_sel].dwControlID;
MXC.cMultipleItems := 0;
mxc.cChannels := 1;
mxc.cbDetails := sizeof(mxcd);
mxc.paDetails := @mxcd;
err := mixerGetControlDetails(mixer, @mxc, 0);
if err <> 0 then
begin
lbl_error.visible := true;
exit;
end;
label_count.caption := inttostr(mxcd.dwValue);
pb_chg.position := (mxcd.dwValue);
end;
//
// Далее идут обработчики Event'ов изменения положения регуляторов
//
// Алгоритмы работы ничем не отличаются от алгоритма чтания cb_ctrlChange и Timer1Timer
//
// Единственное отличие: вместо mixerGetControlDetails используем mixerSetControlDetails
//
// Ну и конечно в mxcd записываем новое значение элемента
//
procedure TFormMixer.tb_chgChange(Sender: TObject);
var
mxc: TMIXERCONTROLDETAILS;
mxcd: TMIXERCONTROLDETAILS_UNSIGNED;
err: integer;
begin
mxc.cbStruct := sizeof(mxc);
mxc.dwControlID := m_ctrl[ctrl_sel].dwControlID;
MXC.cMultipleItems := 0;
mxc.cChannels := 1;
mxc.cbDetails := sizeof(mxcd);
mxc.paDetails := @mxcd;
mxcd.dwValue := tb_chg.Position;
err := mixerSetControlDetails(mixer, @mxc, 0);
if err <> 0 then
begin
lbl_error.visible := true;
exit;
end;
label_count.caption := inttostr(mxcd.dwValue);
end;
procedure TFormMixer.cb_chgClick(Sender: TObject);
var
mxc: TMIXERCONTROLDETAILS;
mxcd: TMIXERCONTROLDETAILS_UNSIGNED;
err: integer;
begin
mxc.cbStruct := sizeof(mxc);
mxc.dwControlID := m_ctrl[ctrl_sel].dwControlID;
MXC.cMultipleItems := 0;
mxc.cChannels := 1;
mxc.cbDetails := sizeof(mxcd);
mxc.paDetails := @mxcd;
if cb_chg.checked then
mxcd.dwValue := 1
else
mxcd.dwValue := 0;
err := mixerSetControlDetails(mixer, @mxc, 0);
if err <> 0 then
begin
lbl_error.visible := true;
exit;
end;
label_count.caption := inttostr(mxcd.dwValue);
pb_chg.position := (mxcd.dwValue);
end;
procedure TFormMixer.cbox_chgChange(Sender: TObject);
var
i: integer;
mxc: TMIXERCONTROLDETAILS;
mxcl: array[0..100] of integer; //TMIXERCONTROLDETAILS_UNSIGNED;
err: integer;
begin
for i := 0 to m_ctrl[ctrl_sel].cMultipleItems - 1 do
begin
if i = cbox_chg.ItemIndex then
mxcl[i] := 1
else
mxcl[i] := 0;
end;
mxc.cbStruct := sizeof(mxc);
mxc.dwControlID := m_ctrl[ctrl_sel].dwControlID;
MXC.cMultipleItems := m_ctrl[ctrl_sel].cMultipleItems;
mxc.cChannels := 1;
mxc.cbDetails := sizeof(mxcl);
mxc.paDetails := @mxcl;
err := mixerSetControlDetails(mixer, @mxc, 0);
if err <> 0 then
begin
lbl_error.visible := true;
exit;
end;
end;
end.
{ ------ Заключение ------ }
Итак, я надеюсь, что в результате разбора данного
кода становится понятна структура и принцип работы с микшером через WinAPI.
|