Главная » Программирование звука » Колеблющаяся струна

0

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

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

Эти  проблемы  можно  решить  при  помощи  алгоритма  колеблющейся  струны (plucked  string  algorithm),  который  первоначально  был  разработан  для  имитации эволюции звучании струны. Тем не менее, при внесении небольших изменений он может использоваться для синтеза многих музыкальных звуков. Сначала надо

ознакомиться с основными идеями, а затем рассмотреть простую реализацию это-

го алгоритма.

B основе алгоритма колеблющейся струны лежат три главные идеи:

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

?  если повторять короткий отрезок звука, то получится музыкальный тон. На-

пример, если взять отрезок длительностью в 1/100 секунды и циклически повторять его 100 раз в секунду, получится достаточно сильной тон в 100 Гц в дополнение к каким-либо иным частотам, присутствующим в звуке;

?  без особых проблем можно постепенно удалить высокие или низкие частоты.

Чтобы  представить  эти  идеи  в  действии,  предположим,  что  мы  работаем  на частоте  дискретизации  44100  Гц.  Заполним  таблицу  441  случайным  значением и  будем  воспроизводить  ее  в  бесконечном  цикле.  Возможно,  результат  будет  не очень музыкальным, но он имеет четкую высоту тона 100 Гц.

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

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

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

while(samplesRemaining >0) {

*buffer++ +=_buffer[_pos];

samplesRemaining–;

AudioSample thisSample = _buffer[_pos] buffer[pos] = (_buffer[_pos] + lastSample)/2; lastSample = thisSample;

if (++_pos >= _bufferSize) _pos = 0;

}

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

ляете ее, временно сохраняя предыдущее значение в переменной lastSample.

Базовый   алгоритм   требует   некоторых   усовершенствований.   Очевидная   проблема состоит в том, что при затухании ноты трудно определить, когда она заканчивается. Хотя все величины в буфере в результате усреднения в конечном счете

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

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

/* После обновления  _buffer[_pos] */

dcBias = (_dcBias * 32767 + _buffer [_pos]*32768)/32768;

_buffer [_pos] = dcBias/32768

Здесь   для   получения   точного   значения   _dcBias используются   вычисления  с  фиксированной  точкой  (15  бит  после  десятичной  точки).  Они  представляют  собой  простой  фильтр  высокихчастот  (high-pass  filter),  который  подавляет  низкие  частоты  сильнее,  чем  высокие.  B  частности,  он  подавляет  смещение постоянного  тока  нулевой  частоты,  результатом  чего  является  конечное  затухание сигнала до нуля.

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

При  эксперименте  с  одной  простой  реализацией  нота  в  100  Гц  звучала  более

15 секунд, в то время как нота в 1000 Гц затухала менее чем за секунду.

Один  из  вариантов  разрешения  этой  проблемы   изменить  способ  обновления  отсчетов.  Используя  взвешенное  усреднение  вместо   простого,  можно  заставить   содержимое   буфера   изменяться   медленнее,   препятствуя   быстрому   затуханию.  Однако  если  веса  будут  изменены  слишком  сильно,  то  и  звук  значительно изменит  свои  характеристики  (например,  вместо  мягкого  звука  гитары  получится  резкий,  металлический).  Корректная  настройка  весов  требует  понимания  математических   основ   проектирования   фильтров.   Более   подробное   описание   этого подхода  можно  найти  в  книге  Ф.  Ричарда  Мура  (F.  Richard  Moore)  "Elementsof Computer Music" (Prentice-Hall, 1990).

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

Реализация алгоритма струны

Как  и  во  всех  классах  инструментов,  здесь  используются  два  объекта.  Сначала  надо  создать  PluckedStringInstrument,  а  затем  просить  его  выдать  соответствующие объекты PluckedStringNote.

Листинг 21.6. Программа plucked.h

#ifndef PLUCKED_H_INCLUDED

#define PLUCKED_H_INCLUDED

#include "audio.h"

#include "instrumt.h"

#endif

Тем   не   менее,   в   отличие   от   класса   SampleInstrument на   класс   Plucked StringInstrument возлагается    немного    функций.    Кроме    создания    объектов PluckedStringNote,  он  только  сохраняет  текущую  частоту  дискретизации  (способность, которую он унаследовал от класса AbstractInstrument).

Листинг 21.7. Описание класса PluckedStringlnstrument

class PluckedStringlnstrument : public AbstractInstrument {

public: // Конструктор  и  деструктор, которые ничего  не   делают.

PluckedStringlnstrument() {};

virtual ~PluckedStringInstrument() {};

public:

AbstractNote * NewNote(float pitch, float volume) {

return new PluckedStringNote(this,pitch,volume);

};

};

Листинг 21.8. Программа plucked.cpp

#include "instrumt.h"

#include "plucked.h"

#include <cstdlib>    // Функции   rand, srand.

#include <ctime>      // time(), нужно   для  инициализации

// функции  srand.

#include <cmath>      // Функция  sqrt.

Листинг 21.9. Описание класса PluckedStringNote

class PluckedStringNote : public AbstractNote {

friend class PluckedStringlnstrument;

private:               // Объект класса  PluckedStringNote может

// создать  только дружественная функция.

PluckedStringlnstrument *_instr; PluckedStringNote(PluckedStringlnstrument *instr,

float pitch, float volume);

public:               // Ho удалить его  может   кто  угодно.  virtual ~PluckedStringNote();

private:

float _pitch;

float _volume;

public:

void Pitch(float pitch) { _pitch = pitch; };

float Pitch() { return _pitch; }

void Volume(float volume) { _volume = volume; };

float Volume() { return _volume; };

void Restart();

size_t AddSamples(AudioSample *buffer, size_t samples);

void EndNote(float rate) { _decayRate = rate; }

private:

float _decayRate;  // Коэффициент  затухания.

int _bufferSize;   // Размер буферов.

long *_buffer;     // Последние обработанные  данные.

long *_future;     // Следующая порция  отфильтрованных  данных.

int _pos;          // Текущее положение в  буфере.

int _iterations;   // Как   часто  требуется  фильтровать буфер.

int _remaining;    // Когда провести  очередную фильтрацию.

};

Конструктор просто устанавливает высоту и громкость, затем вызывает Restart

для общей инициализации. Деструктор очищает два буфера.

Листинг 21.10. Реализация класса PluckedStringNote

PluckedStringNote::PluckedStringNote(PluckedStringInstrument

*instr,

float pitch, float volume) {

_instr = instr; Pitch(pitch); Volume(volume); Restart ();

} PluckedStringNote::~PluckedStringNote(){

delete [] _buffer;

delete [] _future;

}

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

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

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

Листинг 21.10. Реализация класса PluckedStringNote (продолжение)

// Однократная активизация  srand(). static bool randomInitialized = false;

void PluckedStringNote::Restart() {

if (!randomInitialized) {

srand(time(0));           // Установка начального  числа

// генератора  случайных чисел.

randomInitialized = true; // Повторно этого  делать  не   надо.

}

_bufferSize = static_cast<long>(_instr->SamplingRate() /

_pitch);

if (_bufferSize < 2) {

_bufferSize = 1;

_iterations = 1;

} else {

// Можем воспроизводить вплоть

// до   половины от  частоты  дискретизации!

// Первая  аппроксимация: обновляем

// значения  буфера 100 раз  в секунду.

_iterations = _instr->SamplingRate()/100/_bufferSize;

// Вторая  аппроксимация: возводим

// в квадрат.

_iterations *= _iterations;

}

if (_iterations < 1) _iterations = 1;

_remaining = 1;

_pos = 0;

_decayRate = 0.0;

}

Важной особенностью генераторов случайных чисел является то, что старшие биты  случайный  характер  имеют  чаще,  чем  младшие.  Константа  RAND_MAX стандарта ANSI C определяет диапазон значений функции rand().

Листинг 21.11. Создание буфера и заполнение его случайными числами

{ // Создание  буфера  и  заполнение  его  случайными числами.

_buffer = new long[_bufferSize];

for(int i = 0;i<_bufferSize; i ++) { AudioSample s = (rand() (RAND_MAX/2) 1)

>> (sizeof(RAND_MAX)*8-sizeof(AudioSample)*8);

_buffer[i] = s;

}

}

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

Листинг 21.12. Предварительное фильтрование буфера

long maxSample = 0;

{                 // Для  предварительной  фильтрации используем

// значение громкости.

float s1 = 0.5 + _volume/2.0;

float s2 = 0.5 _volume/2.0;

long lastSample = _buffer[_bufferSize-1];

for(int i=0;i<_bufferSize;i++) {

long thisSample = _buffer[i];

_buffer[i] = static_cast<long>(thisSample * s1 + lastSample

* s2 );

lastSample = thisSample;

if (labs(_buffer[i])>maxSample) maxSample =

labs(_buffer[i]);

}

}

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

Листинг 21.13. Масштабирование содержимого буфера

long average = 0;

{

float volumeScale = _volume * ((1<<(sizeof(AudioSample)*8-1))-

1)

/maxSample;

for(int i=0;i<_bufferSize;i++) {

_buffer[i] = static_cast<long>(_buffer[i] * volumeScale);

average += _buffer[i];

}

average /= _bufferSize;

}

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

Листинг 21.14. Нормализация содержимого буфера

{

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

_buffer[i] = average;

}

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

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

Листинг 21.15. Инициализация будущих выборок

{

_future = new long[_bufferSize];

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

_future[i] = _buffer[i];

}

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

Листинг 21.10. Реализация класса PluckedStringNote (продолжение)

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

int samplesRemaining = samplesRequested;

while(samplesRemaining > 0) {

// Плавное смешение значений

// _buffer и  _future.

long blendedSample = (_buffer[_pos] * _remaining

+ _future[_pos] * (_iterations _remaining)

)/_iterations;

*buffer++ += blendedSample; // Воспроизведение  выборки.

samplesRemaining–;

if(++_pos>=_bufferSize) {   // Достигнут  конец  _buffer.

_pos = 0;                // Возврат  к  началу.

if (–_remaining == 0) { // He пора ли   повторить

// обработку данных?

long *t = _buffer;    // Смена   буферов.

_buffer = _future;

_future = t;

// Фильтрация  _buffer в _future. long lastSample = _buffer[_bufferSize-1];

long average = 0;

// Увеличиваем  делитель

// для того, чтобы  затухание

// шло  быстрее.

long divisor = 1024 * (1 << static_cast<int>(10 *

_decayRate));

int i;

for (i=0;i<_bufferSize;i++) {

_future[i] = (_buffer[i]*512 + lastSample*512)/

divisor;

lastSample = _buffer[i];

average += _future[i];

}

// Ре-нормализация _future и  проверка, не  затухла ли

// нота.

average /= _bufferSize; long total = 0; for(i=0;i<_bufferSize;i++) {

_future[i] = average;

total += labs(_future[i]);

// Суммируем  общую  амплитуду.

}

// Если   ничего не   осталось,

// возвращаемся.

if (total == 0) return (samplesRequested –

samplesRemaining);

_remaining = _iterations;  // Сбрасываем задержку

// перед  следующим

// обновлением.

}

}

}

return (samplesRequested samplesRemaining);

}

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

По теме:

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