Изменение громкости Windows с помощью ПДУ
Странно почему меня не посещала эта мысль раньше, но это весьма удобно менять громкость звука, например, при просмотре фильма, используя пульт дистанционного управления.
Итак эта статья затрагивает сразу две темы:
- Использование пульта дистанционного управления
- Изменение системной громкости Windows
Работа с пультом
Для решения первой задачи у нас в качестве аппаратного обеспечения имеется ТВ-тюнер Behold TV H6 с пультом ДУ. Для работы с пультом предназначена библиотека BeholdRC.dll, описание экспортируемых функций, а также пример работы на Delphi, мы можем найти на сайте разработчика. Впрочем, функций всего несколько.
Получение информации о нажатии клавиш можно реализовать двумя способами, либо проверять состояние по таймеру, либо использовать отдельный поток. Второй вариант представляется более эффективным и интересным.
Итак реализуемый класс будет иметь имя TRemoteVolumeControl, и являться наследником TThread.
На данном этапе разработки, код главной формы будет отвечать за 3 вещи:
- Создания объекта TRemoteVolumeControl при старте приложения
- Прием сообщений об нажатии кнопок изменения громкости (возможно для обновления интерфейса программы)
- Заверешние потока, при закрытии программы
Для всего этого добавим пользовательское сообщение, процедуру его обработки, и переменную типа TRemoteVolumenControl.
const WM_REMOTE_CONTROL = WM_USER + 1;
type
TMainForm = class(TForm)
....
FRCListener : TRemoteVolumeControl;
procedure wmRemoteControlMsg(var msg :TMessage); message WM_REMOTE_CONTROL;
end;
|
Переопределим конструктор нашего класса-потока так, чтобы в качестве параметров передавать ему дескриптор окна для приемки сообщения, и код сообщения. При передаче сообщения, WParam будет содержать код нажатой кнопки.
procedure TMainForm.FormCreate(Sender: TObject);
begin
FRCListener := TRemoteVolumeControl.Create(handle, WM_REMOTE_CONTROL);
end;
procedure TMainForm.wmRemoteControlMsg(var msg: TMessage);
begin
infoLabel.caption := Format('0x%.2x', [msg.WParam]);
end;
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if assigned(FRCListener) then
FRCListener.Terminate();
end; |
Пульты бывают разных модификаций, с разным количеством кнопок, что мы опишем следующим перечислением (тип пульта мы получаем с помощью библиотеки BeholdRC):
TRCType = (rctUnknown = -1, rctButtons30 = 0, rctButtons28, rctButtons34);
|
Для работы с библиотекой нам необходимо описать сигнатуры экспортируемых функций. Все они используют модель вызовов cdecl. Алгоритм вызовов обычно следующий: узнать количество карт, инициализировать карту по индексу. Можно получить ее имя и тип пульта, опрашивать коды нажатой клавиши. В конце работы необходимо вызвать процедуру UnInit.
const BEHOLD_RC_DLL = 'BeholdRC.dll';
var
UnInit : function() : boolean; cdecl;
OpenCard : function(Index : Cardinal) : boolean; cdecl;
GetCardName : function(Index : Cardinal) : PWideChar; cdecl;
GetCardCount : function() : cardinal; cdecl;
GetRemoteType : function() : TRCType; cdecl;
GetRemoteCode : function() : Cardinal; cdecl;
GetRemoteCodeEx : function() : Cardinal; cdecl;
|
Сразу приведу описание класса TRemoteVolumeControl:
TRemoteVolumeControl = class(TThread)
strict private
FVolControl : IAudioEndpointVolume;
FLibHandle : THandle;
FNotifyWindow : HWND;
FNotifyMessage : Cardinal;
function InitRemoteControl():boolean;
function InitVolumeControl():boolean;
procedure Done(sender : TObject);
procedure RCKeyPress(const code : integer);
protected
procedure Execute; override;
public
constructor Create(NotifyWindow : HWND; NotifyMsg : cardinal);
end;
|
Как мы видим параметры конструктора класса сохраняются в private члены FNotifyWindow & FNotifyMessage. Для инициализации работы с пультом и изменения громкости предназначены две функции InitRemoteControl (пульт) и InitVolumeControl (громкость звука). При завершении работы потока будет вызван метод Done. Таким образом, конструктор класса проводит настройку параметров и инициализацию библиотек.
constructor TRemoteVolumeControl.Create(NotifyWindow: HWND; NotifyMsg: cardinal);
begin
inherited Create(false);
FNotifyWindow := NotifyWindow;
FNotifyMessage := notifyMsg;
FreeOnTerminate := true;
OnTerminate := Done;
if not (InitRemoteControl() and InitVolumeControl()) then Terminate();
end; |
Если не удалось инициализировать работу с пультом, или управление громкостью, то поток завершается. При этом необходимо удалить ссылки на COM-объект управления звуком и закрыть библиотеку пульта.
procedure TRemoteVolumeControl.Done(sender : TObject);
begin
FVolControl := nil;
CoUninitialize();
if assigned(unInit) then
UnInit();
if (FLibHandle > 0) then
FreeLibrary(FLibHandle);
end; |
Для работы с пультом, необходимо загрузить библиотеку, установить точки входа в экспортируемые функции, выбрать устройство. Соответствующий код тривиален, но пусть будет:
function TRemoteVolumeControl.InitRemoteControl(): boolean;
begin
result := false;
FLibHandle := LoadLibrary(BEHOLD_RC_DLL);
if (FLibHandle = 0) then exit;
@UnInit := GetProcAddress(FLibHandle, 'UnInit');
@OpenCard := GetProcAddress(FLibHandle, 'OpenCard');
@GetCardName := GetProcAddress(FLibHandle, 'GetCardName');
@GetCardCount := GetProcAddress(FLibHandle, 'GetCardCount');
@GetRemoteType := GetProcAddress(FLibHandle, 'GetRemoteType');
@GetRemoteCode := GetProcAddress(FLibHandle, 'GetRemoteCode');
@GetRemoteCodeEx := GetProcAddress(FLibHandle, 'GetRemoteCodeEx');
if getCardCount() = 0 then exit;
result := OpenCard(0);
end; |
В “описании” API библиотеки сказано, что оптимальный интервал проверки состояния нажатой клавиши 30-50мсек. С этим утверждением я не соглашусь. Практические опыты показывают, что нажатие на кнопку и ее отпускание занимает примерно 150-200мс, что очень хорошо демонстрируется нажатием кнопки отключения звука Mute (при интервале опроса в 50 мс, звук включается-выключается 2-3 раза). Основной рабочий цикл программы весьма прост: опросить состояние пульта (getRemoteCode), если нажата клавиша (код отличен от $FF), то произвести необходимые манипуляции (процедура RCKeyPress), отправиться в сон на 180мс до следующей проверки. Необходимые манипуляции в данном случае это известить главную форму приложения, и изменить громкость. Но пока что мы не рассматриваем детали.
procedure TRemoteVolumeControl.Execute;
var code : cardinal;
begin
while not terminated do begin
code := getRemoteCode();
if (code <> $FF) then RCKeyPress(code);
sleep(180);
end;
end; |
Как видим, работа с пультом весьма проста.
Управление звуком
Вообще я ни разу не задумывался о том как изменять громкость звука в системе. Поэтому отправился изучать документацию. Впрочем в детали совсем не вдавался. Насколько я понял, начиная с Windows Vista, методы управления звуком кардинально изменились. Об этом нам рассказывает раздел MSDN Core Audio API. Управлять звуком мы можем на глобальном уровне, либо изменяя только свой звуковой поток. Нас интересует первый вариант. Для работы нам потребуются несколько интерфейсов: IMMDeviceEnumerator, представляет коллекцию мультимедиа устройств; IMMDevice представляет одной устройство, и интерфейс IAudioEndpointVolume для управления громкостью. К сожалению Delphi не содержит описания интерфейсов, поэтому потребовалось переписать заголовочные файлы mmDeviceApi.h & EndpointVolume.h (результат прикреплен в конце статьи). В гугле вы можете найти файл mmDevApi.pas с описанием интерфейсов на Delphi, но там описаны только те интерфейсы, что нужны для изменения громкости, а не полностью соответствующие заголовочные файлы.
Стоит отметить, что используя эти интерфейсы мы можем не только управлять громкостью, но и получать уведомления о том, что громкость была изменена (IAudioEndpointVolumeCallback).
Теперь если быть кратким, то наша функция InitVolumeControl должна получить ссылку на интерфейс IAudioEndpointVolume, что мы и сделаем:
function TRemoteVolumeControl.InitVolumeControl: boolean;
var devEnum : IMMDeviceEnumerator;
device : IMMDevice;
begin
CoInitialize(nil);
devEnum := CreateComObject(CLSID_MMDeviceEnumerator) as IMMDeviceEnumerator;
device := devEnum.GetDefaulAudioEndPoint(eRender, eMultimedia);
FVolControl := device.Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, nil) as IAudioEndpointVolume;
result := assigned(FVolControl);
end;
|
Теперь мы можем описать функцию RCKeyPress. Во-первых мы уведомляем форму о нажатой клавише, отправляя ей ее код. Далее в зависимости от нажатой клавиши мы либо увеличиваем/уменьшаем громкость (VolumeStepUp/VolumeStepDown). При нажатии на кнопку mute, мы должны определить текущее состояние и инвертировать его. Нажав на кнопку выключения мы можем отправить форме сообщение WM_CLOSE, хотя мы ее уже уведомили об этом отправив код кнопки.
procedure TRemoteVolumeControl.RCKeyPress(const code : integer);
var mute : boolean;
begin
PostMessage(FNotifyWindow, FNotifyMessage, code, 0);
case code of
$0C : FVolControl.VolumeStepUp(nil);
$18 : FVolControl.VolumeStepDown(nil);
$11 : begin
mute := FVolControl.GetMute();
FVolControl.SetMute(not mute, nil);
end;
$12 : PostMessage(FNotifyWindow, WM_CLOSE, 0 ,0 );
end;
end; |