VCL.TMediaPlayer. Проблема записи при невыбранном файле



Автор: Кирилл Грибунин

Проблема выявлена в Delphi 6, UpdatePack 2, под управлением Windows 2000, Windows ME, Windows 98.

Нужно записать что-то, например, с микрофона, после чего сохранить это в файл, например test.wav.

Что для этого делаем. Так как файла на момент начала записи ещё нет, оставляем свойство FileName пустым. Свойство DeviceType ставим dtWaveAudio, т.к. при dtAutoSelect получим при открытии exсeption с сообщением, что невозможно определить тип устройства из расширения файла.

Далее пишем следующий код:


MediaPlayer1.Open;
MediaPlayer1.StartRecording;

Запись не начинается. Отладка функции StartRecording показывает, что последняя строка этой функции:


FError := mciSendCommand( FDeviceID, mci_Record, FFlags, Longint(@RecordParm));

возвращает код 274 (MCIERR_UNSUPPORTED_FUNCTION).

Изучение проблемы показывает следующее. В методе Open написан следующий код:


procedure TMediaPlayer.Open;
begin
...
if FDeviceType <> dtAutoSelect then
FFlags := FFlags or mci_Open_Type
else
FFlags := FFlags or MCI_OPEN_ELEMENT;
...
end;

т.е., если DeviceType не dtAutoSelect, то флаг MCI_OPEN_ELEMENT не ставится. Этот код, видимо был написан после прочтения следующего места в MSDN:

To use automatic type selection (via the entries in the registry), assign the filename and file extension to the lpstrElementName member of the structure identified by lpOpen, set the lpstrDeviceType member to NULL, and set the MCI_OPEN_ELEMENT flag.

Но программист MPlayer.pas не прочитал другого места в MSDN, в котором описывается ещё одно применение флага MCI_OPEN_ELEMENT:

If a compound device supports recording, an application can open the device without specifying a name for the device element. In this case, the application uses the MCI_OPEN_ELEMENT flag with a zero-length, zero-terminated string for the element name. Recorded data is not saved in a permanent file until the application explicitly saves it with the MCI_SAVE command, specifying the name of the destination file.

ТИПОВЫЕ РЕШЕНИЯ

Видимо, ситуация следующая. В MSDN указывается, что флаг MCI_OPEN_ELEMENT следует использовать не во всех случаях, а в двух ситуациях:

  1. Когда используем автоматическое определение устройства по имени файла (при чтении)
  2. Когда имя файла не указывается (при записи)
При открытии конкретного устройства (например, CD-AUDIO) для чтения, этот флаг указывать не нужно.

Таким образом, уже при открытии нужно знать, что мы собираемся дальше делать с устройством. Так как в логике MediaPlayer в методе Open этого нам знать не дано, то можно попытаться в коде этого метода сделать предположения:

  • если имя файла указано, то мы наверняка будем использовать плейер для чтения. В этом случае, если устройство Auto, то флаг указываем, иначе — нет.
  • если имя файла не указано, то мы будем использовать устройство для записи. Флаг ставим.

Код этого участка получается следующим:


if FElementName <> '' then
begin
if FDeviceType = dtAutoSelect then
FFlags := FFlags or MCI_OPEN_ELEMENT
else
FFlags := FFlags or mci_Open_Type;
end
else
FFlags := FFlags or MCI_OPEN_ELEMENT;

С таким исправлением тестовый пример работает нормально.

Надеюсь, что вышеприведённый код, хотя и не особенно корректный (в силу некорректных предположений о дальнейшем использовании устройства при открытии), но как-то может помочь программистам, пытающимся использовать MediaPlayer для записи хоть каким-нибудь образом.

P.S. Код MPlayer.pas вообще довольно неряшлив. Например, в том же методе Open видим следующий фрагмент, в котором два раза проверяется условие FDeviceType <> dtAutoSelect, первый раз — вхолостую:


if FDeviceType <> dtAutoSelect then
FFlags := FFlags or mci_Open_Type;
if FDeviceType <> dtAutoSelect then
FFlags := FFlags or mci_Open_Type
else
FFlags := FFlags or MCI_OPEN_ELEMENT;

Скачать тест StoneTest_38.zip (12k)

КОММЕНТАРИЙ

В MPlayer.pas от Delphi 5 Update pack 1 это место имеет другой вид (кстати, это единственное отличие в коде модуля от версии Delphi 6):


if FDeviceType <> dtAutoSelect then
begin
if FElementName <> '' then
FFlags := FFlags or MCI_OPEN_ELEMENT;
FFlags := FFlags or mci_Open_Type;
end
else
FFlags := FFlags or MCI_OPEN_ELEMENT;

Но, тем не менее, описанная проблема имеет место быть.

Приведенный автором код работает не во всех случаях (проверено в Delphi 5, WinNT4 SP6). А именно: нельзя записать в существующий файл WAV и нельзя открыть устройство для воспроизведения без указания файла (например - CDAudio как диск).

Эксперимент показывает, что для успешной операции записи звука в файл (новый или существующий), при явном указании типа устройства, должны быть установлены оба флага - MCI_OPEN_TYPE и MCI_OPEN_ELEMENT. Исправленный для этого случая код выглядит следующим образом:


FFlags := FFlags or MCI_OPEN_ELEMENT;
if FDeviceType <> dtAutoSelect then
FFlags := FFlags or mci_Open_Type;

Однако, при этом TMediaPlayer может не работать с другими типами устройств. Проверка показала, что так оно и есть - пишутся и проигрываются WAV файлы нормально, AVI - тоже проигрывается, а вот устройство CDAudio даже открываться не хочет - Exception на основании ошибки 296 (MCIERR_INVALID_FILE). Остальные типы устройств не проверялись.

Заранее нельзя определить, открывают TMediaPlayer для записи или нет. Напрашивается мысль, что MCI_OPEN_ELEMENT в случае FDeviceType <> dtAutoSelect можно выставлять только при условии, если устройство поддерживает запись. Об этом можно узнать из флага FCanRecord, значение которого определяется в методе


procedure TMediaPlayer.GetDeviceCaps;

Но эта процедура вызывается (и может быть вызвана) только после открытия устройства. Получается замкнутый круг.

Вывод. Исходный код TMediaPlayer нуждается в серьезной доработке. Как "заплатный" вариант, представляется попытка открыть устройство с MCI_OPEN_ELEMENT, а в случае ошибки - сбросить флаг и повторить попытку:


// перенесенный оригинальный участок кода
if FShareable then
FFlags := FFlags or mci_Open_Shareable;
OpenParm.dwCallback := Handle;
// измененный участок
FFlags := FFlags or MCI_OPEN_ELEMENT;
repeat // фиктивный цикл здесь нужен для break
if FDeviceType <> dtAutoSelect then
begin
FFlags := FFlags or mci_Open_Type;
FError := mciSendCommand(0, mci_Open, FFlags, Longint(@OpenParm));
if FError = 0 then
break;
FFlags := FFlags and not MCI_OPEN_ELEMENT;
end;
FError := mciSendCommand(0, mci_Open, FFlags, Longint(@OpenParm));
until true;
// последующий оригинальный участок кода
if FError <> 0 then {problem opening device}
raise EMCIDeviceError.Create(ErrorMessage)


Далее: Запись WAV-файлов »»