Главная » Программирование звука » Синтез музыкальных инструментов

0

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

При разработке решения  данной проблемы  преследуются  две достаточно  раз-

ные цели.

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

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

Семплеры

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

Для выполнения подлинной операции тонального сдвига требуется много времени;  поэтому  в  некоторых  звуковых  проигрывателях  принято  распространенное упрощение:  изменяется  скорость  воспроизведения,  пропуская  отсчеты,  чтобы  получить  аппроксимацию  частотной  вариации.  Однако  данный  метод  неидеален. Изменение  скорости  воспроизведения  меняет  не  только  частотный  спектр,  но и скорость изменения самого спектра. Ведь в обычных инструментах звучание различных нот затихает с одинаковыми скоростями. Ho, несмотря на несовершенство, этот метод остается лучшим из вариантов, доступных на многих системах.

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

Базовый   класс   SampledInstrument в   действительности   представляет   собой контейнер   для   выборок   данных.   Значительная   часть   обработки   производится в  классе   SampleNote.  Переменные   _basePitch и  _baseSampleRate содержат принципиально  важную информацию о семпле слышимую высоту тона при воспроизведении  прототипа на  определенной  скорости. B классе  SampleNote эта  информация  используется  для  определения  преобразования  семпла  при  формировании звуков различной высоты тона.

Листинг 21.1. Программа sampled.h

#ifndef SAMPLED_H_INCLUDED

#define SAMPLED_H_INCLUDED

#include "audio.h"

#include "instrumt.h"

class SampledNote : public AbstractNote {

friend class SampledInstrument;

};

class SampledInstrument : public AbstractInstrument {

private:

AudioSample *_samples;

int _sampleLength; int _repeatStart; int _repeatEnd; float _basePitch;

float _baseSampleRate;

public: SampledInstrument();

SampledInstrument(AudioSample * samples, int length, int repeatStart, int repeatEnd);

virtual ~SampledInstrument() ;

void BasePitch(float basePitch, float baseSampleRate);

friend class SampledNote;

AbstractNote * NewNote(float pitch, float volume) {

return new SampledNote(this, pitch, volume);

};

};

#endif

При   создании   объекта   класса   SampledInstrument ему   передается   запись для   использования   в   качестве   шаблона.   Важная   характеристика   этой   записи – высота тона на некоторой частоте дискретизации. По сути дела, объект

SampledInstrument будет изменять частоту дискретизации для корректиров-

ки высоты тона.

Листинг 21.2. Программа sampled.cpp

#include <cmath>

#include "instrumt.h"

#include "sampled.h" SampledInstrument::SampledInstrument() {

_basePitch = 440;

_baseSampleRate = 8000;

_samples = 0;

_sampleLength = 0;

_repeatStart = _repeatEnd = 0;

}

SampledInstrument::SampledInstrument(AudioSample * samples, int length, int repeatStart, int repeatEnd) {

_basePitch = 440;

_baseSampleRate = 8000;

_samples = 0;

if (length > 0) {     // Копируем отсчеты  в  локальный буфер.

_samples = new AudioSample[length];

for(int i=0; i<length; i++)

_samples[i] = samples[i];

}

_repeatStart = repeatStart;

_repeatEnd = repeatEnd;

_sampleLength = length;

}

SampledInstrument::~SampledInstrument() {

if (_samples) delete [] _samples;

}

void SampledInstrument::BasePitch(float basePitch, float baseSampleRate) {

_basePitch=basePitch; _baseSampleRate=baseSampleRate;

}

Класс  SampledInstrument на  практике  является  контейнером  для  записи  инструмента. Когда мы запрашиваем у него ноту, он создает объект SampledNote, который и выполняет основную работу.

Программе  SampledNote необходимо  небольшое  количество  приватных  переменных.  Ясно,  что  ноте  нужен  указатель  на  объект  инструмента.  Также  ей  потребуются  указатели  на  необработанные  выборки  и  на  некоторые  ключевые области внутри них.

Листинг 21.3. Члены класса SampledNote

private:

SampledInstrument *_instrument; AudioSample *_currentSample;

AudioSample *_endData; AudioSample *_startLoop; AudioSample *_endLoop;

bool _repeating;   // Зациклить?

public:

// Чтобы   прекратить  воспроизведение ноты,

// просто  прерываем цикл.

void EndNote(float) { _repeating = false; }

Конструктор  этого  класса  минимален.  Так  как  нам  необходимо  переключать ноту и устанавливать ее высоту и громкость, определим эти более сложные операции как отдельные функции-члены.

Листинг 21.3. Члены класса SampledNote (продолжение)

protected:

SampledNote(SampledInstrument *instr); SampledNote(SampledInstrument *instr, float pitch, float

volume);

Листинг21.2. Программа sampled.cpp (продолжение)

SampledNote::SampledNote<SampledInstrument *instr) {

_instrument = instr;

_requestPitch = instr->_basePitch;

_requestSampleRate = instr->_baseSampleRate;

};

SampledNote::SampledNote(SampledInstrument *instr, float pitch, float volume) {

_instrument = instr;

_requestPitch = instr->_basePitch;

_requestSampleRate = instr->_baseSampleRate; Pitch(pitch);

Volume(volume); Restart();

};

Самой  простой  из  этих  операций  является  повторный  запуск  (рестарт)  воспроизведения  ноты,  который  требует  повторной  инициализации  различных  указателей. Позже будет объяснено значение переменной _fraction.

Листинг 21.3. Члены класса SampledNote (продолжение)

public:

void Restart();

Листинг 21.2. Программа sampled.cpp (продолжение)

void SampledNote::Restart() {

_repeating = true;

_currentSample = _instrument->_samples;

_endData = _instrument->_samples + _instrument->_sampleLength;

_startLoop = _instrument->_samples + _instrument->_repeatStart;

_endLoop = _instrument->_samples + _instrument->_repeatEnd;

_fraction = 0;

}

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

Листинг 21.3. Члены класса SampledNote (продолжение)

private:

int _volume;

enum {volumeBits = 13}; // _volume равно 1/8192

public:

void Volume(float volume) {

_volume = int(volume * (1<<volumeBits));

}

float Volume() {

return static_cast<float>(_volume) / (1<<volumeBits);

}

Теперь  надо  описать  фактическую  логику  процедуры  воспроизведения  зву-

ка, а затем обратиться к более сложным вычислениям значения Pitch.

Функция  AddSamples варьирует  скорость  воспроизведения  путем  изменения скорости шагов функции по буферу отсчетов. Смена высоты тона требует изменения величины приращения.

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

_increment к  _fraction,  а  затем  переместить  указатель  на  величину,  равную целой   части   _fraction.   Оставшаяся   часть   метода   AddSamples представляет собой простую «бухгалтерию».

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

Листинг 21.3. Члены класса SampledNote (продолжение)

private:

enum {fractionBits = 10};

public:

size_t AddSamples(AudioSample *buffer, size_t samples);

Листинг 21.2. Программа sampled.cpp (продолжение)

size_t SampledNote::AddSamples(AudioSample *buffer, size_t samplesRequested) {

int samplesRemaining = samplesRequested;

if (!_currentSample) return 0;      // Нет   данных?

while(samplesRemaining) {

if (_repeating && (_currentSample >= _endLoop)) {

if (_startLoop == _endLoop)   // Нет   цикла.

_repeating = false;        // He повторять.

else

_currentSample = _endLoop _startLoop;

}

if (!_repeating && (_currentSample >= _endData))

return samplesRequested samplesRemaining;

// Допустим, что "long"

// больше, чем   наибольшая

// выборка  * (1<<volumeBits)

long newSample = (*_currentSample) *

static_cast<long>(_volume);

newSample >>= volumeBits;

*buffer++ += newSample;

_fraction += _increment;

_currentSample += _fraction >> fractionBits;

// 8-битная  порция.

_fraction &= (1<<fractionBits)-1;

samplesRemaining–;

}

return samplesRequested samplesRemaining;

};

Для   вычисления   высоты   тона   большую   роль   играет   значение   переменной

_increment,   которая   управляет   тем,   насколько   быстро   указатель   _currentSample перемещается  по  данным.  Требуемая  величина  приращения  зависит  от  необходимой высоты тона и частоты дискретизации выходного звука, а также высоты тона,   получаемой   при   использовании   единичного   приращения   на   некоторой   заданной   частоте   дискретизации.   Переменные   _basePitch и   _baseSampleRate в    объекте    SampledInstrument содержат    стандартные    (задаваемые    по    умолчанию)  значения  высоты  тона  и  частоты  дискретизации.  Таким  образом,  чтобы корректно  задать  приращение,  необходимо  вычислить  отношение  желаемой  высоты  тона  к  базовой  и,  для  получения  значения  с  фиксированной  точкой,  провести масштабирование.

Листинг 21.3. Члены класса SampledNote (продолжение)

private:

int _increment, _fraction;

float _requestPitch;

float _requestSampleRate;

void SetIncrement();

public:

void Pitch(float pitch) {

_requestPitch  =  pitch; SetIncrement();

};

float Pitch() { return _requestPitch; };

Листинг 21.2. Программа sampled.cpp (продолжение)

void SampledNote::SetIncrement() {

_increment = int(_requestPitch/_instrument->_basePitch

* _instrument->_baseSampleRate/_instrument-

>SamplingRate()

* (1<<fractionBits));

}

Некоторым клиентам этого класса может понадобиться более точное управление  воспроизведением.  B  частности,  необходимо  заставить  звучать  инструмент с определенного места записи в семпле.

Листинг 21.3. Члены класса SampledNote (продолжение)

public:

void SetSampleOffset(int offset);

Листинг 21.2. Программа sampled.cpp (продолжение)

void SampledNote::SetSampleOffset(int offset) {

_currentSample = _instrument->_samples + offset;

while (_currentSample >= _endData) {

if (_startLoop == _endLoop) {

_currentSample = 0;

return;

}

_currentSample = _startLoop + (_currentSample _endData);

_endData = _endLoop;

}

};

Генератор синусоидального сигнала

Существует    множество    способов    использования    вышеописанного    класса SampledInstrument. Один способ считать семплы из внешнего файла. Другой создать    новые    классы    инструментов,    использующих    SampledInstrument. B  качестве  примера  второго  подхода  рассмотрим  инструмент,  который  просто проигрывает   синусоидальную   волну.   Он   создает   короткий   отрывок   синусоидальной   волны,   а   затем   использует   SampledInstrument для   воспроизведения этого семпла.

B   некоторых   объектно-ориентированных   языках   можно   организовать   подкласс   SampledInstrument,   переопределив   его   конструктор   другим,   который давал бы дискретное представление синусоиды, а затем вызывал бы обычный конструктор  SampledInstrument.  Однако  такой  подход  не  работает  в  C++.  Вместо этого  получен  новый  класс  AbstractInstrument,  из  которого  возник  управляемый им класс SampledInstrument.

Листинг 21.4. Объявление класса SineWavelnstrument

class SineWavelnstrument: public AbstractInstrument {

private:

SampledInstrument *_sampledInstrument;

void CreateInstrument();

public:

SineWaveInstrument() { _sampledInstrument = 0; };

~SineWaveInstrument() {

if (_sampledInstrument)

delete _sampledInstrument;

}

AbstractNote *NewNote(float pitch, float volume) { CreateInstrument();

return _sampledInstrument->NewNote(pitch,volume);

}

void SamplingRate(long samplingRate) { AbstractInstrument::SamplingRate(samplingRate); CreateInstrument();

_sampledInstrument->SamplingRate(samplingRate);

}

long SamplingRate() {

return AbstractInstrument::SamplingRate();

}

};

Программисты,  пишущие  на  объектно-ориентированных  языках,  называют  этот тип объектов объектами-агентами (proxy object). B сущности, он не более чем заместитель  другого:  всю  работу  выполняет  базовый  объект  SampledInstrument. Особый интерес представляет та часть данного класса, в которой создается шаблон инструмента  и  на  его  основе  инициализируется  объект  SampledInstrument. Основная часть реализации будет рассмотрена в следующем разделе, а пока надо разобраться в некоторых модификациях, которые делают звук более естественным.

Листинг 21.2. Программа sampled.cpp (продолжение)

void SineWaveInstrument::CreateInstrument() {

if(_sampledInstrument) return;

}

Источник: Кинтцель Т.  Руководство программиста по работе со звуком = A Programmer’s Guide to Sound: Пер. с англ. М.: ДМК Пресс, 2000. 432 с, ил. (Серия «Для программистов»).

По теме:

  • Комментарии