Коливається струна

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

Друга проблема в тому, що звук семпла НЕ еволюціонує безперервно на Протягом усього звучання ноти Для економії місця більша частина нот програється в результаті циклічного повторення одного короткого відрізка звуку Ha багатьох реальних інструментах істинний звук продовжує змінюватися весь час, поки утримується нота

Ці проблеми можна вирішити за допомогоюалгоритму коливається струни (Plucked string algorithm), який спочатку був розроблений для імітації еволюції звучанні струни Проте, при внесенні невеликих змін він може використовуватися для синтезу багатьох музичних звуків Спочатку треба

ознайомитися з основними ідеями, а потім розглянути просту реалізацію це-

го алгоритму

B основі алгоритму коливається струни лежать три головні ідеї:

 ‰  Ñ, Ð ° Ð º Ð º Ð ° Ð º ÑуР¼ Ñ Ð ¾ ÐÐμÑ € Ð ¶ Ð ¸ Ñ, ÑÐ ¸ Ñ € Ð ¾ Ð º Ð ¸ Ð ¹ ÐÐ ¸ Ð ° Ð ¿Ð ° Ð · Ð ¾ Ð ½ Ñ ‡ Ð ° Ñ Ñ, Ð ¾ Ñ,, Ñ Ð ¾ Ñ € Ð ¼ Ð ¸ Ñ € Ð ¾ Ð ² Ð ° Ð ½ Ð ¸ Ðμ Ð · Ð ² уРº Ð °  Р¼ Ð ¾ Ð ¶ Ð ½ Ð ¾ Ð ½ Ð ° Ñ ‡ Ð ¸ Ð ½ Ð ° Ñ, ÑŒ Ñ Ð ¾ Ñ Ð» ÑƒÑ ‡ Ð ° Ð ¹ Ð ½ Ñ <Ñ ... Ð · Ð ½ Ð ° Ñ ‡ ÐμÐ ½ Ð ¸ Ð ¹ Ð ¸ Ð ¼ ÐμÐ'Ð »ÐμÐ ½ Ð ½ Ð ¾ Ð º Ð ¾ Ñ € Ñ € ÐμÐ º Ñ, Ð ¸ Ñ € Ð ¾ Ð ² Ð ° Ñ, ÑŒ Ð ¸ Ñ ... Ð'л Ñ Â Ð ¿Ñ € Ð ¸ Ð'Ð ° Ð ½ Ð ¸ Ñ Ð · Ð ² уРº у Ñ Ð ² Ð ¾ Ð »ÑŽÑ † Ð ¸ Ð ¾ Ð ½ Ð ¸ Ñ € ÑƒÑŽÑ ‰ ÐμÐ ³ Ð ¾ Ñ ... Ð ° Ñ € Ð ° Ð º Ñ, ÐμÑ € Ð °;

 ‰  ÐμÑ Ð »Ð ¸ Ð ¿Ð ¾ Ð ² Ñ, Ð ¾ Ñ € Ñ Ñ, ÑŒ Ð º Ð ¾ Ñ € Ð ¾ Ñ, Ð º Ð ¸ Ð ¹ Ð ¾ Ñ, Ñ € ÐμÐ · Ð ¾ Ð º Ð · Ð ² уРº Ð °, Ñ, Ð ¾ Ð ¿Ð ¾ л ÑƒÑ ‡ Ð ¸ Ñ, Ñ ​​Ñ Ð ¼ уР· Ñ <Ð º Ð ° Ð »ÑŒÐ ½ Ñ <Ð ¹ Ñ, Ð ¾ Ð ½. Ð Ð ° -

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

 ‰  Р± ÐμÐ · Ð ¾ Ñ Ð ¾ Ð ± Ñ <Ñ ... Ð ¿Ñ € Ð ¾ Ð ± Ð »ÐμÐ ¼ Ð ¼ Ð ¾ Ð ¶ Ð ½ Ð ¾ Ð ¿Ð ¾ Ñ Ñ, ÐμÐ ¿ÐμÐ ½ Ð ½ Ð ¾ уÐ'Ð ° л Ð ¸ Ñ, ÑŒ Ð ² Ñ <Ñ Ð ¾ Ð º Ð ¸ Ðμ Ð ¸ Ð »Ð ¸ Ð ½ Ð ¸ Ð · Ð º Ð ¸ Ðμ Ñ ‡ Ð ° Ñ Ñ, Ð ¾ Ñ, Ñ <.

Щоб уявити ці ідеї в дії, припустимо, що ми працюємо на частоті дискретизації 44100 Гц Заповнимо таблицю 441 випадковим значенням і будемо відтворювати її в нескінченному циклі Можливо, результат буде не дуже музичним, але він має чітку висоту тону 100 Гц

Тепер нам потрібно модифікувати таблицю в міру відтворення Звичайна версія даного алгоритму просто замінює кожну вибірку на середнє значення поточної і попередньої вибірок Ця проста модифікація спрямована на згладжування грубих модуляцій Як наслідок, високі частоти будуть ослаблені, в той час як низькі практично не зміняться (Це усереднення є прикладом фільтра низьких частот, названого так тому, що він залишає низькі частоти щодо незміненими, тоді як високі послаблюються)

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

Обєднавши розглянуті вище ідеї, можна отримати базовий алгоритм коливається струни

while(samplesRemaining &gt0) {

*buffer++ +=_buffer[_pos]

samplesRemaining–

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

if (++_pos &gt= _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

Лістинг 216 Програма pluckedh

#ifndef PLUCKED_H_INCLUDED

#define PLUCKED_H_INCLUDED

#include &quotaudioh&quot

#include &quotinstrumth&quot

#endif

Проте, на відміну від класу SampleInstrument на клас Plucked StringInstrument покладається трохи функцій Крім створення обєктів PluckedStringNote, він тільки зберігає поточну частоту дискретизації (здатність, яку він успадкував від класу AbstractInstrument)

Лістинг 217 Опис класу PluckedStringlnstrument

class PluckedStringlnstrument : public AbstractInstrument {

public: / / Конструктор і деструктор, які нічого не роблять

PluckedStringlnstrument() {}

virtual ~PluckedStringInstrument() {}

public:

AbstractNote * NewNote(float pitch, float volume) {

return new PluckedStringNote(this,pitch,volume)

}

}

Лістинг 218 Програма pluckedcpp

#include &quotinstrumth&quot

#include &quotpluckedh&quot

# Include / / Функції rand, srand

# Include / / Time (), потрібно для ініціалізації

/ / Функції srand

# Include / / Функція sqrt

Лістинг 219 Опис класу 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

для загальної ініціалізації Деструктор очищає два буфера

Лістинг 2110 Реалізація класу 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 в одиницю призводить до звичайного варіанту поведінки цього алгоритму, при якому вміст буфера оновлюється так само часто, як і відтворюється

Лістинг 2110 Реалізація класу PluckedStringNote (продовження)

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

void PluckedStringNote::Restart() {

if (randomInitialized) {

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

/ / Генератора випадкових чисел

randomInitialized = true / / Повторно цього робити не треба

}

_bufferSize = static_cast&ltlong&gt(_instr-&gtSamplingRate() /

_pitch)

if (_bufferSize &lt 2) {

_bufferSize = 1

_iterations = 1

} else {

/ / Можемо відтворювати аж

/ / До половини від частоти дискретизації

/ / Перша апроксимація: оновлюємо

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

_iterations = _instr-&gtSamplingRate()/100/_bufferSize

/ / Друга апроксимація: зводимо

/ / В квадрат

_iterations *= _iterations

}

if (_iterations &lt 1) _iterations = 1

_remaining = 1

_pos = 0

_decayRate = 00

}

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

Лістинг 2111 Створення буфера і заповнення його випадковими числами

{/ / Створення буфера і заповнення його випадковими числами

_buffer = new long[_bufferSize]

for(int i = 0i&lt_bufferSize i ++) { AudioSample s = (rand() (RAND_MAX/2) 1)

&gt&gt (sizeof(RAND_MAX)*8-sizeof(AudioSample)*8)

_buffer[i] = s

}

}

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

Лістинг 2112 Попереднє фільтрування буфера

long maxSample = 0

{/ / Для попередньої фільтрації використовуємо

/ / Значення гучності

float s1 = 05 + _volume/20

float s2 = 05 _volume/20

long lastSample = _buffer[_bufferSize-1]

for(int i=0i&lt_bufferSizei++) {

long thisSample = _buffer[i]

_buffer[i] = static_cast&ltlong&gt(thisSample * s1 + lastSample

* s2 )

lastSample = thisSample

if (labs(_buffer[i])&gtmaxSample) maxSample =

labs(_buffer[i])

}

}

B класі SampledInstrument гучність регулювалася шляхом масштабування кожної вибірки під час відтворення звуку Цього можна уникнути, якщо зробити попереднє масштабування всього вмісту буфера Треба просто відкоригувати вміст буфера таким чином, щоб максимальна вибірка мала задану амплітуду

Лістинг 2113 Масштабування вмісту буфера

long average = 0

{

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

1)

/maxSample

for(int i=0i&lt_bufferSizei++) {

_buffer[i] = static_cast&ltlong&gt(_buffer[i] * volumeScale)

average += _buffer[i]

}

average /= _bufferSize

}

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

Лістинг 2114 Нормалізація вмісту буфера

{

for(int i = 0i&lt_bufferSize i++)

_buffer[i] = average

}

B базовому алгоритмі використовується тільки один буфер, і значення модифікуються на місці B цій зміненій версії значення оновлюються лише зрідка Щоб поліпшити виходнСй сигнал, слід злегка змішати значення до і після кожного оновлення, щоб зміна звуку відбувалося плавно, незважаючи на те що за 16 або більше циклів буфер може фільтруватися тільки один раз

Щоб реалізувати таке змішування, необхідно знати вміст буфера до і після кожної фільтрації Далі слід зберегти вміст буфера послефільтраціі в масиві _future Коли приходить час оновлювати вміст буфера, треба перемістити _future в _buffer, а потім обновити _future

Лістинг 2115 Ініціалізація майбутніх вибірок

{

_future = new long[_bufferSize]

for(int i=0i&lt_bufferSizei++)

_future[i] = _buffer[i]

}

Ще одна перевага даного підходу в порівнянні з традиційним швидкість Для більшості відліків обчислюється тільки одне середнє зважене значення (дві операції множення і одна операція ділення цілих чисел) Складніша фільтрація проводиться рідше, так як вона повинна також провести повторну нормалізацію буфера (усунути зміщення постійного струму, яке могла накопичитися) і визначити, чи закінчилася нота Ha повільних машинах можна було б прибрати операцію змішування і зробити звичайний варіант ще швидше

Лістинг 2110 Реалізація класу PluckedStringNote (продовження)

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

int samplesRemaining = samplesRequested

while(samplesRemaining &gt 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 &lt&lt static_cast&ltint&gt(10 *

_decayRate))

int i

for (i=0i&lt_bufferSizei++) {

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

divisor

lastSample = _buffer[i]

average += _future[i]

}

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

/ / Нота

average /= _bufferSize long total = 0 for(i=0i&lt_bufferSizei++) {

_future[i] = average

total += labs(_future[i])

/ / Підсумовуємо загальну амплітуду

}

/ / Якщо нічого не залишилося,

/ / Повертаємося

if (total == 0) return (samplesRequested –

samplesRemaining)

_remaining = _iterations / / Скидаємо затримку

/ / Перед наступним

/ / Оновленням

}

}

}

return (samplesRequested samplesRemaining)

}

Джерело: Кінтцель Т Керівництво програміста по роботі зі звуком = A Programmers Guide to Sound: Пер з англ М: ДМК Пресс, 2000 432 с, іл (Серія «Для програмістів»)

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*