SineMusitвид изнутри

Следующий шаг - создание вторичных буферов. Но прежде надо понять, сколько потребуется буферов, каких, и что в них будет находится. А для этого нужно хорошо представить себе, как будет работать программа. Как и положено генератору, SineMusit должен звучать сколько угодно, пока пользователь его не выключит. Значит воспроизведение вторичных буферов должно быть зациклено. Первое, что приходит в голову, поместить в буфер образец сигнала длительностью в один период и запустить циклическое воспроизведение.

При таком подходе, размер буфера оказывается в зависимости от длительности периода, то есть от частоты сигнала. В буфере может быть больше одного периода, но сколько бы их там ни находилось, все они должны быть только в целом виде. Бессмысленно помещать в буфер, скажем, три и одну десятую периода. Другими словами, сумма периодов сигнала, находящихся в буфере, должна быть СТРОГО равна сумме периодов частоты дискретизации, необходимых для их представления.

Отсюда вывод: данный способ получения сигнала позволяет ТОЧНО представить только те частоты, которые, так или иначе, кратны частоте дискретизации. Остальные придется представлять приближенно, причем размер буфера будет разным не только для разных частот, но и для разной точности представления частоты. Поскольку SineMusit должен служить, в том числе, источником образцовых частот для музыкальных инструментов, то надо обесечить возможность установки частоты с точностью до сотой доли герца. Меньше, для музыкальных инструментов, нельзя. В итоге, подход на первый вгляд простой и очевидный, на самом деле, для такой программы как SineMusit, оказывается чересчур громоздким и сложным.

поточный буфер

Поточный буфер.

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

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

DirectSound дает все необходимое для реализации такой схемы. В нем есть, как возможность циклического проигрывания вторичных буферов, так и возможность задать позиции уведомления, о которых уже шла речь выше. Возможность занесения в буфер данных, без его остановки, тоже есть. Кроме того, операционная система позволяет создать отдельный поток, в котором можно ожидать событий уведомлений и подгружать в буфер новые данные.

Итак, общий подход определен. В SineMusit один вторичный буфер с двумя позициями уведомления: самой первой и точно посередине, чтобы получились две одинаковые половины. Опытным путем установлено, что оптимальный размер буфера примерно 10000 минимальных блоков данных для частоты дискретизации 44100 Гц. Минимальный блок данных - это совокупность выборок сигнала всех звуковых каналов в один и тот же момент времени. Например, для стерео это две выборки. При размере буфера примерно вдвое меньше указанного, нормального воспроизведения уже не получается, а больший размер приводит к заметным задержкам в реакции на действия пользователя. Для частоты дискретизации большей чем указанная, размер буфера надо пропорционально увеличить. Например, при частоте 96000 Гц он должен быть вдвое больше.

Создается вторичный буфер тем же самым методом CreateSoundBuffer объекта DirectSound, который перед этим использовался для создания объекта первичного буфера. Только теперь, передаваемая в первом параметре, переменная типа TDSBufferDesc полностью заполнена. В поле флагов - комбинация флагов, определяющая, нужные для SineMusit, возможности буфера. Переменная WFX та же самая, что и при установке формата первичного буфера.

const
  NSamp = 10000;    //  размер буфера в минимальных блоках данных
var
  //  формат буфера
  WFX : TWaveFormatEx =
    ( wFormatTag      : WAVE_FORMAT_PCM;
      nChannels       : 2;
      nSamplesPerSec  : 44100;
      nAvgBytesPerSec : 44100 * 2 * (16 div 8);
      nBlockAlign     : 2 * (16 div 8);
      wBitsPerSample  : 16;
      cbSize          : 0 );
  //  описание свойств буфера
  BufDesc : TDSBufferDesc =
    ( dwSize          : SizeOf(TDSBufferDesc);
      dwFlags         : DSBCAPS_CTRLPOSITIONNOTIFY or   //  получение уведомлений
                        DSBCAPS_CTRLPAN or              //  управление стерео балансом
                        DSBCAPS_GLOBALFOCUS or          //  вывод звука при отсутствии фокуса
                        DSBCAPS_GETCURRENTPOSITION2 or  //  получение текущей позиции воспроизведения
                        DSBCAPS_CTRLVOLUME;             //  управление громкостью
      dwBufferBytes   : NSamp * 2 * 2;  //  размер буфера в байтах = NSamp * кол-во каналов * байт на выборку
      dwReserved      : 0;
      lpwfxFormat     : @WFX;
      guid3DAlgorithm : '{00000000-0000-0000-0000-000000000000}' );
  hr   : HRESULT;
  FDS  : IDirectSound;                                  //  объект DirectSound
  FDSB : IDirectSoundBuffer;                            //  вторичный буфер
  FDSN : IDirectSoundNotify;                            //  интерфейс уведомлений
  PlayEvents    : array[0..1] of THandle;               //  массив событий буфера
  PlayEventsPos : array[0..1] of TDSBPositionNotify;    //  массив позиций уведомлений
  ...
  //  создание вторичного звукового буфера
  hr := FDS.CreateSoundBuffer(BufDesc, FDSB, nil);
  if failed(hr) then
    ...
  //  создание интерфейса уведомлений
  hr := FDSB.QueryInterface(IID_IDirectSoundNotify, FDSN);
  if failed(hr) then
    ...
  //  создание событий звукового буфера и задание позиций уведомлений
  for i:=0 to High(PlayEvents) do begin
    PlayEvents[i] := CreateEvent(nil, false, false, nil);
    PlayEventsPos[i].hEventNotify := PlayEvents[i];
    PlayEventsPos[i].dwOffset := i*(BufDesc.dwBufferBytes div 2);
  end;
  //  Включение уведомлений
  hr := FDSN.SetNotificationPositions(2, @PlayEventsPos[0]);
  if failed(hr) then
    ...

После того как буфер создан, методом QueryInterface для него создается интерфейс уведомлений. Первым параметром метода передается идентификатор создаваемого интерфейса. Второй параметр - сама переменная создаваемого объекта. Далее, с помощью системной функции CreateEvent, создаются события буфера. В SineMusit события имеют начальное состояние "не установлено" и автоматически сбрасываются при выходе из ожидающей функции WaitForMultipleObjects. Дескрипторы событий заносятся в массив, который потребуется для ожидающей функции. Одновременно заполняется массив позиций уведомлений. Позиция уведомления имеет тип TDSBPositionNotify, представляющий собой запись:

type
  TDSBPositionNotify = packed record
    dwOffset: DWORD;        // смещение позиции от начала буфера (в байтах)
    hEventNotify: THandle;  // дескриптор события, которое будет возникать при достижении позиции
  end;

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

Коротко об используемых системных функциях (объявлены в модуле Windows.pas). Функция CreateEvent создает событие и возвращает его дескриптор, который можно использовать в дальнейшем.

function CreateEvent(
             lpEventAttributes: PSecurityAttributes;  //  указатель на атрибуты безопасности
             bManualReset     : BOOL;                 //  ручной сброс события или автоматический
             bInitialState    : BOOL;                 //  начальное состояние события
             lpName           : PChar;                //  имя события
         ): THandle;  //  дескриптор события

Функция WaitForMultipleObjects не возвращает управление до тех пор, пока хотя бы одно событие (или, при желании, обязательно все события) не перейдет (перейдут) в состояние "установлено". Если в четвертом параметре передается константа INFINITE, то возврат из функции произойдет только при наступлении событий. Индекс произошедшего события в массиве, переданном во втором параметре, можно узнать путем вычитания из возвращаемого функцией значения константы WAIT_OBJECT_0.

function WaitForMultipleObjects(
             nCount        : DWORD;           //  количество ожидаемых событий
             lpHandles     : PWOHandleArray;  //  массив дескрипторов событий
             bWaitAll      : BOOL;            //  ожидать все события или для возврата управления достаточно одного
             dwMilliseconds: DWORD;           //  время ожидания в милисекундах
         ): DWORD;  //  номер события

Функция CreateThread создает и запускает поток. Возвращает дескриптор и идентификатор потока. В третьем параметре передается адрес функции (процедуры), которую поток будет выполнять.

function CreateThread(
             lpThreadAttributes: Pointer;                //  указатель на атрибуты безопасности
             dwStackSize       : DWORD;                  //  размер стека, если 0, то размер будет как у вызывающего потока
             lpStartAddress    : TFNThreadStartRoutine;  //  пользовательская функция, которую поток будет выполнять
             lpParameter       : Pointer;                //  указатель на параметр для пользовательской функции
             dwCreationFlags   : DWORD;                  //  флаги управляющие создаием процесса
             var lpThreadId    : DWORD;                  //  идентификатор потока
         ): THandle;  //  дескриптор потока

Осталось последнее действие - создание и запуск потока, в котором будут перехватываться события буфера. Это можно сделать двумя способами, с помощью системной функции CreateThread или с помощью, имеющегося в Delphi класса TThread. В SineMusit использован первый способ.

var
  PlayThreadHandle : THandle;
  PlayThreadID     : DWORD;
  ...
  PlayThreadHandle := CreateThread(nil, 0, TFNThreadStartRoutine(@PlayExecute), nil, 0, PlayThreadID);

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


Назад: к общему описанию программы
Голос народа: отзывы, пожелания, мнения
1
2
3 4 5

Если Вы хотите поддержать разработку бесплатных программ и в частности этот сайт, то сделать это можно не только "монетами". Компьютерное оборудование, или как сейчас говорят "железо", которое Вы, по тем или иным причинам сочли уже неподходящим для себя, возможно сможет еще поработать. Правда, не во всех видах оборудования есть необходимость ...

Москва, 2013-2020 гг., © ZHarNS58
386