КАК ЗАПИСАТЬ В ПЗУ АУДИОДАННЫЕ ИЗ WAV-ФАЙЛА И "ПРОИГРАТЬ" ИХ


А. ДОЛГИЙ, г. Москва

В последнее время получили широкое распространение всевозможные музыкальные звонки, "говорящие" часы и др. подобные устройства. Носителем звуковой информации в них служит ПЗУ, в которое записаны аудиоданные, например, из так называемого wav-файла. О том, что собой представляет подобный файл, как его подготовить и записать в ПЗУ и как самому изготовить "проигрыватель" для его воспроизведения, рассказывается в статье.

Независимо от используемой элементной базы, конструктивного исполнения и сервисных удобств принцип действия большинства "говорящих" и "играющих" устройств одинаков. Их тракт формирования звуковой информации построен по структурной схеме, показанной на рис. 1. С каждым импульсом тактового генератора G1 изменяется состояние счетчика адреса D1 и код из очередной ячейки ПЗУ DS1 поступает на цифроаналоговый преобразователь (ЦАП) А1. Сигнал с его выхода через фильтр нижних частот (ФНЧ) Z1 поступает на вход УМЗЧ (на схеме не показан), а с его выхода - на громкоговоритель.

cif-4o11.gif
Puc.1

Главное достоинство устройства, выполненного по такой схеме, - способность воспроизвести любой звук: достаточно занести в ПЗУ описывающие его коды, а затем считать их. Но где их взять? Прежде всего, в огромной коллекции различных звуков и мелодий, существующих в виде компьютерных файлов. При необходимости файл с записью нужного звукового фрагмента несложно создать самостоятельно, воспользовавшись стандартными программными средствами Windows.

Для хранения аудиоданных в файлах разработано множество форматов. Один из самых распространенных - WAV (подобные файлы имеют имена с расширением .wav). Согласно требованиям фирмы Microsoft, в любом мультимедийном приложении, официально признанном совместимым с Windows, должна быть предусмотрена возможность работы именно с такими файлами, а со всеми другими - лишь по желанию авторов приложений.

Имея нужный WAV-файл, ответить на поставленный в заголовке вопрос очень просто: достаточно найти в нем последовательность байтов 64Н, 61Н, 74Н, 61 Н (соответствующие им символы образуют слово data), пропустить следующие за ними четыре байта и "зашить" в ПЗУ оставшуюся часть файла. Сделать это поможет программа, текст которой на языке Turbo Pascal 7.0 приведен в табл. 1. Проанализировав файл, программа выводит на экран монитора сведения о содержащихся в нем аудиоданных и записывает эти данные в РПЗУ (например, АТ24С512). Последнее подключают к порту LPT компьютера с помощью адаптера, схема которого изображена на рис. 6 в статье автора этих строк "Микросхемы памяти с интерфейсом I2C. Особенности и применение" ("Радио", 2001, № 2, с. 24-26; № 3, с. 25, 26). При необходимости программу не трудно переделать для программирования РПЗУ и других типов.

Остается собрать несложное воспроизводящее устройство (оно будет описано далее) и слушать сколько угодно раз записанные в РПЗУ звуки. Однако хороший результат будет получен лишь при соблюдении некоторых условий, в противном случае воспроизведенные звуки лишь отдаленно напомнят оригинал.

Основных требований два: частота квантования (Рцв) звукового сигнала в исходном WAV-файле должна быть в точности равна таковой (т. е. частоте смены адресов ПЗУ) в устройстве воспроизведения, а коды чисел-отсчетов мгновенных значений - представлены в форме, пригодной для непосредственной подачи на ЦАП. При неравенстве частот квантования воспроизводимый звук будет замедлен или ускорен с соответствующим сдвигом по частоте всего спектра (вспомните голоса Винни-Пуха и Пятачка в известном мультфильме). А несовпадение форм представления чисел приведет к очень большим нелинейным искажениям.

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

Но не отчаивайтесь, обнаружив, что понравившийся звуковой фрагмент записан в WAV-файл в форме, непригодной для прямого переноса в ПЗУ синтезатора. Прежде всего попытайтесь преобразовать его с помощью стандартной программы, например, той, которая входит в стандартный комплект Windows 98 и под названием "Звукозапись" находится в папке "Развлечения" (Пуск>Про-граммы>Стандартные>Развлече-ния>3вукозапись). Кроме своего основного назначения, она "умеет" изменять частоту квантования звука, преобразовывать 16-разрядные данные в восьмиразрядные и обратно, а стереофоническую запись - в монофоническую.

Если стандартная программа по какой-либо причине не подошла, придется выполнить необходимые преобразования самостоятельно. А для этого нужно знать, как "устроен" WAV-файл - частный случай разработанного фирмой Microsoft формата файлов RIFF (Resource Interchange File Format), предназначенных для обмена ресурса-

ir"" CRT, DOS;

type tChunkHeader-record

Mame:array[0..3) of char;Size:lon9int end;

tWavePonnatex-record

wFonnatTag,nChannels:word;

nSamplesPerSec,nAvgBytesPerSec:longint ;

nBlockAlign,wBitsPerSample:word

•nd;

coiufc ROMTop=longint($FFFF);ROMBlockSize=128;

ProgressStep=(ROMTop+l) div 64;

Base"$378;P OUT-Base;P_IN-Base+l ;

SCLOutMask=?20; SCLInMask=$80 ;

SDAOutMask=$40;SDAInMask=$40 ;

ResType:•tring[4]=•****• ;

DataPound:boolean=false ;

var I,DataStart,ErrCount:longint;NoErr:boolean ;

B,St,01dTextAttr:byte;Wav:file of byte;

ChunkHeader:tChunkHeader ;

AChunkHeader:array tO..SizeOf(tChunkHeader)-1]

of byte absolute ChunkHeader;

WaveFormaCEx: tWaveFonnatEx ;

AWaveFonnatex: array [0. .SizeOf (tWaveFonnatex) -1]

of byte absolute WavePonnatex;

procedure SetSCL;

begin St:=St and not SCLOutMask.-port [P_0ut:l :=St end;

procedure ResetSCL;

begin St:=St or SCLOutMask.-port [P_0ut] ;=St end;

procedure PutSDA(B:boolean) ;

begin if B then St:=St and not SDAOutMask

•If St:=St or SDAOutMask;

port[P_0ut] :=St end;

function QetSDA:boolean;

begin GetSOA:-(port[P_In] and SDAInMask)=0 •nd;

procedure InitBus;

begin

St:=byte(not(SDAOutMask or SCLOutMask)) ;

port[P_Out] :"St end;

procedure Wait;

begin a- MOP;NOP;NOP;NOP •nd •nd;

procedure Start;

begin

SetSCL;Wait;PutSDA(false);Wait;ResetSCL;Wait end;

procedure Stop;

begin

PutSDA(f"l"e) ;Wait;SetSCL;Wait;PutSDA(true) ,-Wait end;

procedure PulseSCL;

begin Hait;SetSCL;Hait;ResetSCL end;

function GetBit: boolean;

begin Wait;SetSCL;Wait;GetBit:-GetSDA;ResetSCL •nd;

function SendByte(B:byte):boolean;

var M:byte;

begin

M:-$80;

repeat PutSDAKB and M)<>0> ;PulseSCL;M:=M abr 1;

until M~0;

PutSDA (trua); SendByte:-not GetBit end;

function GetByte:byte;

var B:byte,-I:integer;

begin

B:-0;

for I:*0 to 7 do begin B:"B •hi 1;i? QetBit then B:-B or 1

end;

GetByte:=B;PutSDA(tal"");PulseSCL;PutSDA(tru") ;

end:

begin

if ParamCount=0 then begin HriteLn (' HeoOxoanu napaueTp - msn UAV-fawia') ;

Halt end;

01dTextAttr:-TextAttr;ClrScr;WriteLn;

assign(Wav,Paramstr(1));reset(Wav) ;

WriteLn (' S'awi ', PileRec (Wav) .Name) ;

fillchar(ChunkHeader,SizeOf(ChunkHeader),0) ;

repeat

move(AChunkHeader[1],AChunkHeader[0] , SizeOf(AChunkHeader)) ;

Read(Wav,AChunkHeader[SizeOf(AChunkHeader)-1]) ;

if ChunkHeader.Name-'RIFF- then begin for I:=1 to 4 do Read(Wav,byte(ResType[I]));

Write CTtin", #32:21 );

if ResTypeo'WAVE' then TextAttr:°LightRed+Blink;

WriteLn (ResType) ; TextAttr: -OldTextAttr ;

end;

if ChunkHeader.Name='fmf#32 then begin fillchar(WaveForraatEx,SizeOf(WaveFormatex),0) ;

I:=0;

repeat read(Hav,AWaveFormatEx[I]);inc(I) ;

until EOF(Wav) or (I>=ChunkHeader.Size) or (I>=.Sizeof (WaveFonnatEx)) ;

with WaveFonnatEx do begin Write('tfopwaT',#32:18) ;

if wPonnatTag=l then WriteLn('HKM') elae begin

TextAttr:=LightRed+Blink;

WriteLn(wForroatTag);TextAttr:=01dTextAttr ;

end;

WriteLn ('.KaHaJTOB' ,#32:17,nChannels) ;

WriteLn (' lacTOTa KBairrOBanrin', #32:5, nSamplesPerSec,' rn');

WriteLn (' PaapWHocTt, ',#32:13, wBitsPerSanple) end end;

if ChunkHeader.Names'data' then begin dec(ChunkHeader.Size,8);DataFound:=true;

WriteLn('Bcero OTCVOTOB',#32:10, ChunkHeader.Size div WaveFonnatEx.nBlockAlign) ;

WriteLn (' JInnTeJimocTt, 3Byvamis!' ,#32:3, ChunkHeader.Size/

WaveFonnatEx.nAvgBytesPerSec:-10:3,' c') end;

until DataFound or EOF(Wav) ;

if EOF(Wav) then begin Close(Wav),-halt end;

DataStart:=FilePos(Wav) ;

WriteLn {'06i,eu pn3y ,#32 :14, (ROMTop+1)/ WaveFonnatEx.nAvgBytesPerSec:-10:3,' c') ;

WriteLn CSanKcaTfc B pn3y (Y/W ? •);

if not (ReadKey in [•Y',-y','fl',•"•)) then begin

WriteLn('Her');Close(Wav);balt end;

TextAttr:=16*Blue+Yellow;Write(#32:64,#13);

InitBus.-for I:=1 to 9 do PulseSCL.-SetSCL;

for I:>0 to ROMTop do begin if (I aod ROMBlockSize)=0 then begin Stop,-Delay(100);Start;

SendByte ($AO) ,-SendByte (Hi (I)) ,-SendByte (Lo(I)) end;

if I<ChunkHeader.Size then Read(Wav.B) el"e B:-$80;

SendByte(B) ;

if (I nod ProgressStep)=(ProgressStep-l) then Write(#176) end;

Write(#13);TextACtr:=01dTextAttr;

ErrCount: =0 ,-NoErr: =true; Seek (WAV, DataStart) ;

Stop;Delay(100);Start;SendByte($AO) ;

SendByte(0);SendByte(0);Start;SendByte($A1) ;

for I:=0 to ROMTop do begin if I<ChunkHeader.Size than Read(WAV,B) el"e B:=$80;

if BoGetByte then begin TextAttr:-16*Red;

inc(ErrCount) ;

end;

if (I mod ProgressStep)-(ProgressStep-l) then begin Write(#32);TextAttr:-16*Blue end end;

Close(Wav);TextAttr:=01dTextAttr;

WriteLn;WriteLn('OrnuGoK',#32:18,ErrCount) ;

Write (' HaaiMnre ENTER') ; ReadLn;

end.


ми между программами. Аббревиатуру RIFF можно прочитать в самых первых байтах файла, просматривая его как текст. Через четыре байта после нее находится слово WAVE. Именно эти символы, а не расширение имени файла, служат для безошибочного определения его формата. Подобный прием для фирмы Microsoft обычен. Вспомним, что командные процессоры созданных ею операционных систем фактически распознают исполняемые файлы не по расширению *.ехе, а по аббревиатуре MZ в первых двух байтах.

Структура WAV-файла представлена в табл. 2. Он состоит из разделов, каждый из которых начинается с заголовка - четырехсимвольного идентификатора и 32-разрядного числа, равного длине данного раздела в байтах за вычетом восьми, занятых заголовком. Байты этого и всех других чисел следуют в обычном для IBM-совместимых компьютеров порядке: первым - младший, последним - старший. Возможные исключения из этого правила обязательно оговариваются.

Собственно аудиоданные находятся в разделе data, однако ему предшествуют один или несколько разделов, в которых приведены значения параметров, необходимых для правильного воспроизведения; адреса контрольных точек, делящих данные на фрагменты; порядок воспроизведения фрагментов, число повторений каждого из них, символьные строки - названия записи в целом и ее фрагментов, а также комментарии. Из всех вспомогательных разделов обязателен только fmt_ (последний символ - пробел).

Признак формата записи - первое из полей раздела fmt_ - определяет стратегию всех дальнейших действий. Если он равен 1, коды в разделе data представляют собой простую последовательность отсчетов мгновенных значений сигнала. В технике связи ее называют РСМ (Pulse Code Modulation - импульсно-кодовая модуляция, ИКМ). Другие значения признака зарезервированы за различными форматами, зарегистрированными фирмой Microsoft и разрешенными ею для применения в WAV-файлах. Например, 2 обозначает ADPCM (адаптивная дифференциальная ИКМ), 50Н - MPEG. Все они предусматривают сжатие аудиоданных, при

Таблица 2

Байты

Содержание

Значение

0-3

Строка -RIFP

Идентификатор формата файла

4-7

32-разрядное число

Длина файла-в, байт

8-11

Строка "WAVE'

Идентификатор типа ресурса

12-15

Строка fmt '

Идентификатор раздела формата данных

16-19

32-разрядное число

Длина раздела-8, байт

20,21

16-разрядное число

Признак формата записи

22,23

16-разрядное число

Число каналов (1 - моно, 2 - стерео и т. д.)

24-27

32-разрядное число

Частота квантования, Гц

28-31

32-разрядное число

Средняя скорость потока данных, байт/с

32,33

16-разрядное число

Размер блока данных, байт

34,35

16-разрядное число

Разрядность данных (8 или 16 бит)

36-(n-1)

Другие разделы (не обязательные)

n-(n+3)

Строка 'data'

Идентификатор раздела данных

(n+4(-(n+7)

32-разрядное число

Длина раздела-8, байт

(п+8>-...

Звуковые данные


чем алгоритмы кодирования и декодирования далеко не всегда опубликованы. "Изобретателям" новых форматов рекомендуется до официальной регистрации записывать в поле признака формата значение OFFFFH.

Для непосредственной подачи на ЦАП пригодна только РСМ. Преобразования, необходимые при записи/воспроизведении данных других форматов, обычно выполняют программно (если позволяет производительность процессора) или аппаратно. Для кодирования и декодирования данных наиболее распространенных форматов выпускаются специализированные микросхемы. Все дальнейшие сведения, приводимые в настоящей статье, относятся только к РСМ.

Число каналов записи особых пояснений не требует. Одновременные отсчеты сигналов каждого канала (если их более одного) записаны в разделе data последовательно в порядке номеров каналов. Например, при стереозаписи вначале следует первый отсчет левого канала, затем - первый отсчет правого канала, второй отсчет левого канала,

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

Частота Fкв (число выборок в секунду) - важнейший параметр. Чем она выше, тем большей может быть максимальная частота компонентов спектра записываемого сигнала, но тем длиннее получится файл. В этом поле большинства WAV-файлов вы найдете значения 11025, 22050 или 44100 Гц. Именно на них рассчитаны воспроизводящие устройства почти всех звуковых карт. Если в приборе, для которого предназначено ПЗУ, частота другая, первое, что необходимо сделать, - выяснить, нельзя ли добиться нужного значения, например, заменив кварцевый резонатор. Пропорционально частоте Fкв следует изменить и частоту среза ФНЧ на выходе ЦАП (она не должна быть выше половины Fкв). Если этого сделать не удается, придется прибегнуть

к довольно сложной процедуре изменения частоты FKB сигнала, представленного в цифровой форме. При этом следует помнить, что любое такое преобразование вносит в сигнал искажения, как правило, необратимые и трудно поддающиеся коррекции.

Средняя скорость потока данных служит лишь для оценки длительности записанного в файле звукового фрагмента и равна произведению значений предыдущего (Fкв) и следующего (размер блока) полей раздела.

Размер блока данных - число байт, которое необходимо обработать, чтобы получить значения одной выборки сигнала во всех каналах (для восьмиразрядной одноканальной записи - 1, для 16-разрядной стерео - 4).

Разрядность данных - число бит, отведенное одному отсчету сигнала одного канала. Значения, не равные 8 (один байт) или 16 (два байта), встречаются здесь только для форматов, отличающихся от РСМ.

За нулевой уровень при восьмиразрядном кодировании принято шестнадцатиричное число 80Н. Максимальному положительному уровню (127) соответствует код 0FFH, минимальному отрицательному (-128) - 00Н. Такие коды (их называют прямыми со смещением) пригодны для непосредственной подачи на восьмиразрядный ЦАП, на выходе которого постоянную составляющую сигнала нетрудно скомпенсировать или устранить с помощью разделительного конденсатора. Однако арифметические действия с представленными подобным образом числами дают неверные результаты (сложив два отрицательных значения, можно получить положительную сумму). Это необходимо учитывать, например, формируя монофонический сигнал из стереофонического. С особой осторожностью следует подходить к коду 7FH. Иногда вместо обычной "минус единицы" его трактуют как "минус ноль". Соответственно, вся шкала отрицательных значений оказывается смещенной на одну ступень.

Шестнадцатиразрядные отсчеты принято представлять иначе. Здесь нулевое значение - ОН, максимальное положительное (32767) - 7FFFH, а минимальное отрицательное (-32768) - 8000Н. Код 0FFFFH соответствует -1. Этот способ представления чисел со знаком называют дополнительным кодированием. Именно к нему приспособлены арифметические устройства абсолютно всех микропроцессоров.

Переход от одного способа кодирования к другому очень прост. Достаточно инвертировать значение старшего (восьмого или 16-го) двоичного разряда и удалить лишний либо добавить недостающий младший байт. Например:

0000 -> 8000 -> 80

PFFF->7FFF-> 7F

FF -> 7F -> 7FXX

01->81->81XX

Все числа в этом примере - шестнадцатиричные. Добавляемый младший байт XX может быть любым, но одинаковым во всем преобразуемом массиве. Обычно рекомендуют пользоваться значением 80Н.

Радио 4/2001, с.25-27.