Совсем недавно я освоил написание плагинов для этого проигрывателя. Следует отметить, что официального SDK от NullSoft нет, и эта компания изредка выдает общественности куски документации по своему плееру. Некоторые описанные возможности не работают или работают неправильно, некоторые совсем плохо прокомментированы, поэтому мне пришлось много экспериментировать, чтобы написать нечто рабочее. Кстати, пример написанного мной плагина вместе с полностью работоспособным сервисом ведения статистики прослушиваний и создания подписей-картинок для форумов можно посмотреть тут: MusicSign.
Существует достаточно всевозможных тонкостей написания плагинов для WinAmp'а, и я постараюсь их описать в этой статье. Примеры буду приводить на ассемблере MASM32 (собственно, сами плагины я на нем и пишу), но примеры будут несложные, поэтому знающим c/c++ и немного Win32 API будет легко их понять.
Для начала я приведу код простейшего плагина общего назначения (существуют еще и другие типы плагинов, например, ввода или вывода, но я их рассматривать не буду), который умеет всего лишь выдавать три MessageBox'а. На одном из сайтов с документацией по созданию плагинов есть похожий код на си (собственно, больше там ничего и нет).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
.486 ; create 32 bit code .model flat, stdcall ; 32 bit memory model option casemap :none ; case sensitive include \masm32\include\windows.inc include \masm32\include\masm32.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\macros\macros.asm ;макрос для работы с Unicode-строками, входит в поставку масма, не работает с русскими строками. Как работать с русскими символами в Unicode, я рассказывал в одной из предыдущих статей include \masm32\macros\ucmacros.asm includelib \masm32\lib\masm32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib GPPHDR_VER=10h ;эта константа - всегда должна быть 0x10, судя по документации winampGeneralPurposePlugin STRUCT ;структура, описывающая плагин общего назначения для винампа version dd ? ; version of the plugin, defined in "gen_empty.h" description dd ? ; name/title of the plugin, defined in "gen_empty.h" init dd ? ; function which will be executed on init event config dd ? ; function which will be executed on config event quit dd ? ; function which will be executed on quit event hwndParent dd ? ; (?) hDllInstance dd ? ; (?) winampGeneralPurposePlugin ENDS .data ;секция данных - необходимые значения plugin_name db "Test Plugin",0 ;имя плагина - оно будет отображаться в списке плагинов винампа ;теперь инициализируем структуру для винампа - версия 0x10, указатель на строку, ;указатель на функцию для инициализации плагина (вызывается после загрузки плагина самим винампом), ;указатель на функцию, вызываемую винампом для конфигурации плагина, ;указатель на функцию, вызываемую при выгрузке плагина. ;Дальше идут два поля, заполняемые самим винампом - hWnd окна винампа и dllInstance. plugin winampGeneralPurposePlugin {GPPHDR_VER,offset plugin_name,offset init,offset config,offset quit,0,0} .code LibMain proc instance:DWORD,reason:DWORD,reserved:DWORD ;Функция, вызываемая при загрузке dll .if reason == DLL_PROCESS_ATTACH mov eax,1 ;вернем 1 при аттаче dll .endif ret LibMain ENDP ;эта функция должна быть экспортируемой - ее вызовет винамп при загрузке плагина для получения указателя на структуру с информацией о плагине winampGetGeneralPurposePlugin PROC mov eax,offset plugin ;вернем этот указатель ret winampGetGeneralPurposePlugin ENDP config PROC ;вызывается винампом при конфигурации плагина invoke MessageBox,0,chr$("Вызвана функция конфигурации плагина."),offset plugin_name,0 xor eax,eax ret config ENDP init PROC ;вызывается при загрузке плагина invoke MessageBox,0,chr$("Плагин инициализирован."),offset plugin_name,0 xor eax,eax ret init ENDP quit PROC ;вызывается при выгрузке плагина invoke MessageBox,0,chr$("Плагин выгружен."),offset plugin_name,0 xor eax,eax ret quit ENDP end LibMain ;конец кода, указываем точку входа в dll - LibMain |
Сохраним этот код под именем gen_test.asm. Префикс gen_ говорит винампу о том, что это плагин общего назначения. Чтобы получить dll, создаем файл gen_test.def следующего содержания:
1 2 |
LIBRARY gen_test EXPORTS winampGetGeneralPurposePlugin |
Теперь все готово для компиляции. Создаем bat-файл с таким содержанием:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@echo off cls REM тут пути к вашим директориям масма SET PATH=C:\Masm32\bin SET INCLUDE=C:\Masm32\INCLUDE SET LIB=C:\Masm32\LIB ML /nologo -c -coff %1.asm if errorlevel 1 goto terminate LINK /nologo %1.obj /SUBSYSTEM:WINDOWS /FILEALIGN:512 /VERSION:4.0 /MERGE:.rdata=.text /ignore:4078 /RELEASE /BASE:0x400000 /DLL /DEF:%1.def if errorLevel 1 goto terminate echo OK :terminate |
Сохраняем его в директорию с нашими файлами gen_test.asm и gen_test.def, далее запускаем командную строку, переходим в директорию с проектом командой cd и вызываем наш бат-файл с параметром, например:
1 |
имя_бат_файла gen_test |
В той же директории получим несколько файлов, среди которых будет и gen_test.dll. Поместим его в директорию плагинов винампа (Plugins). Теперь после запуска винампа будем наблюдать MessageBox с сообщением о загрузке плагина. Потом можно будет зайти в настройки и увидеть сообщение о вызове функции конфигурации плагина при нажатии на кнопку настройки плагина:
После закрытия винампа появится третье сообщение о выгрузке плагина.
Итак, первый плагин написан и работает, теперь я расскажу о некоторых тонкостях. Заодно я рассмотрю такой пример плагина: винамп будет выдавать сообщение с именем исполнителя песни при воспроизведении. Общение с винампом реализовано с помощью оконных сообщений. Чтобы узнавать об изменениях статуса проигрывания, необходимо встроиться в цепочку обработки сообщений главным окном Winamp'а. С этим связана первая тонкость - с некоторой версии винамп стал поддерживать Unicode на современных системах, поэтому необходимо будет обеспечить совместимость со всеми версиями проигрывателя и системами. Для начала необходимо будет определить, юникодовое ли окно у винампа, и затем только встраиваться в цепь обработки. Введем две новые переменные в секцию данных:
1 2 3 |
.data? old_proc dd ? ;указатель на изначальную процедуру обработки сообщений окна винампа is_unicode dd ? ;поддерживается ли окном юникод |
Теперь добавим код в функции инициализации и выгрузки плагина, а также пустую функцию обработки сообщений. Сперва, потребуется ее прототип, впишем его после инклюдов:
1 |
CheckPlaying PROTO :DWORD,:DWORD,:DWORD,:DWORD |
Теперь - код функции init:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
init PROC ;вызывается при загрузке плагина invoke MessageBox,0,chr$("Плагин инициализирован."),offset plugin_name,0 mov old_proc,FUNC(GetWindowLong,plugin.hwndParent,GWL_WNDPROC) ;сохраняем указатель на изначальную процедуру обработки mov is_unicode,FUNC(IsWindowUnicode,plugin.hwndParent) ;поддерживает ли окно юникод? .if is_unicode==1 ;если да invoke SetWindowLongW,plugin.hwndParent,GWL_WNDPROC,offset CheckPlaying ;встаиваемся в цепочку обработки сообщений (Unicode-версия) .else invoke SetWindowLong,plugin.hwndParent,GWL_WNDPROC,offset CheckPlaying ;встраиваемся в цепочку обработки собщений .endif xor eax,eax ret init ENDP |
Код функции quit:
1 2 3 4 5 6 7 8 9 10 11 |
quit PROC ;вызывается при выгрузке плагина .if is_unicode==1 ;если окно поддерживает юникод invoke SetWindowLongW,plugin.hwndParent,GWL_WNDPROC,old_proc ;возвращаем изначальный указатель на функцию обработки сообщений (Unicode-версия) .else invoke SetWindowLong,plugin.hwndParent,GWL_WNDPROC,old_proc ;возвращаем изначальный указатель на функцию обработки сообщений .endif invoke MessageBox,0,chr$("Плагин выгружен."),offset plugin_name,0 xor eax,eax ret quit ENDP |
Наконец, пустая функция обработки оконных сообщений винампа:
1 2 3 4 5 6 7 8 |
CheckPlaying PROC hWin:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD .if is_unicode==1 ;если окно поддерживает юникод invoke CallWindowProcW,old_proc,plugin.hwndParent, uMsg, wParam, lParam ;вызываем следующую по цепочке функцию обработки сообщений (Unicode-версия) .else invoke CallWindowProc,old_proc,plugin.hwndParent, uMsg, wParam, lParam ;вызываем следующую по цепочке функцию обработки сообщений .endif ret CheckPlaying ENDP |
Теперь плагин по сути ничего нового не делает, но добавилась база для написания кода обработки. Сначала необходимо определить, нажата ли кнопка Play, или окну винампа отослано какое-то другое сообщение. Для этого нам потребуется несколько констант из SDK. Сюда же я добавлю константы для получения информации о файлах, подробнее о которых я расскажу дальше.
1 2 3 4 5 6 7 |
WM_WA_IPC equ WM_USER ;используется для отправки винампу оконных сообщений IPC_ISPLAYING equ 104 ;позволяет узнать, воспроизводится ли в данный момент какой-либо файл IPC_PLAYING_FILE equ 3003 ;сообщение, отсылаемое винампу при воспроизведении файла IPC_GETLISTPOS equ 125 ;узнать текущую воспроизводимую позицию в плейлисте IPC_GETPLAYLISTFILEW equ 214 ;узнать путь к файлу в Unicode IPC_GET_EXTENDED_FILE_INFOW equ 3026 ;получить расширенную информацию о файле в Unicode |
Для получения информации о файле существует два способа - простые оконные сообщения для получения базовой информации и оконное сообщение для получения различной расширенной информации, в том числе, ID3-тега. Для получения расширенной информации существует специальная структура:
1 2 3 4 5 6 |
extendedFileInfoStruct STRUCT ;структура для получения расширенной информации о файле filename dd ? ;указатель на строку с именем файла metadata dd ? ;указатель на строку, которую мы хотим получить, например, Artist, Title, Genre и т.д. retvalue dd ? ;указатель на буфер, кудда будет записан результат retlen dd ? ;длина буфера extendedFileInfoStruct ENDS |
Добавим ее в секцию данных. Заодно, добавим в секцию данных Unicode-строку для получения названия исполнителя (в Unicode):
1 2 3 |
fileinfo extendedFileInfoStruct <> WSTR ArtistW,"Artist" |
Теперь есть всё необходимое, чтобы усовершенствовать код процедуры CheckPlaying. Информацию получать и выводить в MessageBox мы будем в Unicode, хотя винамп поддерживает и ANSII.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
CheckPlaying PROC hWin:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD LOCAL temp_info [2048] :BYTE ;локальная переменная для хранения информации об исполнителе .if uMsg==WM_WA_IPC && lParam==IPC_PLAYING_FILE ;если нам пришло сообщение о начале воспроизведения файла invoke SendMessage,plugin.hwndParent,WM_WA_IPC,0,IPC_ISPLAYING ;проверим, воспроизводится ли файл cmp eax,1 ;если нет, то jne no_action ;передадим управление дальше mov word ptr temp_info[0],0 ;обнулим первое слово в локальной переменной на случай, если винамп не вернет нам ничего, чтобы строка была корректной и null-завершенной invoke SendMessage,plugin.hwndParent,WM_WA_IPC,0,IPC_GETLISTPOS ;получим текущий файл в плейлисте invoke SendMessage,plugin.hwndParent,WM_WA_IPC,eax,IPC_GETPLAYLISTFILEW ;узнаем путь к нему в Unicode mov fileinfo.filename,eax ;запишем указатель на строку с полученныМ путем в структуру mov fileinfo.metadata,offset ArtistW ;получатть будем название исполнителя lea eax,temp_info ;и записывать его в локальную переменну. mov fileinfo.retvalue,eax mov fileinfo.retlen,1022 ;длина - в словах, т.к. Unicode invoke SendMessage,plugin.hwndParent,WM_WA_IPC,offset fileinfo,IPC_GET_EXTENDED_FILE_INFOW ;получаем информацию invoke MessageBoxW,plugin.hwndParent,addr temp_info,offset ArtistW,0 ;выведем названи исполнителя .endif no_action: .if is_unicode==1 ;если окно поддерживает юникод invoke CallWindowProcW,old_proc,plugin.hwndParent, uMsg, wParam, lParam ;вызываем следующую по цепочке функцию обработки сообщений (Unicode-версия) .else invoke CallWindowProc,old_proc,plugin.hwndParent, uMsg, wParam, lParam ;вызываем следующую по цепочке функцию обработки сообщений .endif ret CheckPlaying ENDP |
Вот и все. Компилируем плагин и копируем его в директорию плагинов винампа. Кликаем на любом файле в плейлисте или воспроизводим файл каким-либо другим образом и видим MessageBox:
Остается один вопрос - как же определить, что играет в винампе - видео или аудио? Для решения этой проблемы я перепробовал множество методов, в том числе и сообщение для получения - видео играет или нет, получения разрешения видео, получения типа файла через стркутуру extendedFileInfoStruct, и ни один из них не дал должного результата. В итоге я решил проблему простой проверкой расширения файла.
Скачать весь комплект исходников и dll'ки можно тут: ZIP.
В дальнейшем я могу рассказать о том, как писать плагины к плееру AIMP, если эта тема кого-либо заинтересует.
Спасибо!
Хорошо бы еще статейку о создания плагина к foobar2000 =))
А это что? http://dev.winamp.com/wiki/Plug-in_Developer#SDK_Documentation
Там недостаточно информации для написания полноценного плагина. Почти все страницы в вики пустые, и есть пример только написания пустого плагина.
спасибо за статью, получившийся плагин работает под аимпом, есть какие-то дополнительные тонкости?
Да, под аимпом работает. Из тонкостей - не поддерживались некоторые сообщения в версии аимпа 2.5 юникодовые (IPC_GET_EXTENDED_FILE_INFOW например), но в версии 2.6 вроде бы как совместимость полная.