«Как работать с микшером?» (Очерк очевидца в одном юните)

altСтатья посвящена вопросам по работе с микшером 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 err0 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 err0 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 err0 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 err0 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 err0 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 err0 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 err0 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 err0 then
begin
lbl_error.visible:=true;
exit;
end;
end;

end.
{ —— Заключение —— }
Итак, я надеюсь, что в результате разбора данного кода становится понятна структура и принцип работы с микшером через WinAPI.

Понравилась статья? Поделиться с друзьями: