[Перевод] Воссоздание звука Deep Note от THX
Если вы когда-нибудь посещали кинотеатр, то наверняка слышали Deep Note — звуковой трейдмарк компании THX. Это один из первых звуков, который раздаётся в начале трейлеров в залах, сертифицированных THX. Мне всегда нравилось его узнаваемое крещендо, начиная с жуткого смешения нот и заканчивая ярким и грандиозным финалом (звук). Какая услада для уха!
Вчера (вероятно) без всяких причин меня заинтересовало происхождение этого звука, и я провёл небольшое исследование. Меня глубоко тронула его история, которой хочу поделиться с вами. Затем мы продолжим — и сами создадим этот звук, готовьте ножницы и клей!
Лучший источник информации о звуке, который я смог найти — по-моему, это его полная электроакустическая композиция, опубликованная в отличном блоге Music Thing Blog в 2005 году. Вот ссылка на сообщение.
Некоторые факты о звуке:
- Его создал д-р Джеймс Энди Мурер в 1982 году.
- В один из дней в истории его проигрывали 4000 раз в день, почти каждые 20 секунд! Цитата доктора Мурера:
«Хотел бы сказать, что звук THX — самое популярное произведение компьютерной музыки в мире. Это может быть правдой или нет, но звучит круто!»
- Он создан на компьютере ASP (Audio Signal Processor), способном синтезировать звуки в реальном времени.
- Программа из 20 000 строк кода на C генерировала данные для воспроизведения на ASP. Сгенерированные данные состояли из 250 000 строк, которые обрабатывались на ASP.
- Осцилляторы голосов используют в качестве сигнала оцифрованный виолончельный тон. Доктор Мурер вспоминает, что в сэмпле было около 12 гармоник. ASP мог запустить 30 таких осцилляторов в реальном времени (для сравнения, мой ноутбук прямо сейчас может обрабатывать без сбоев более 1000 таких).
- Сам звук защищён авторским правом, но вот проблема: код доктора Мурера полагается на генераторы случайных чисел (генеративный процесс) и каждый раз звук несколько отличается. Поэтому я не думаю, что можно с уверенностью сказать, что сам процесс является или может быть «защищён авторским правом». Сам звук, да, конкретный сэмпл защищён.
- Звук дебютировал в трейлере THX «Возвращение джедая» перед его премьерой в 1983 году.
- Генеративные характеристики процесса в какой-то момент стали проблематичными. После выхода «Возвращения джедая» оригинальная запись Deep Note была утеряна. Доктор Мурер воссоздал произведение для компании, но те постоянно жаловались, что оно звучит не так, как оригинал. В конце концов, оригинальную запись нашли и сохранили в безопасном месте.
- Dr. Dre попросил разрешения на использование сэмпла в своей музыке, но ему отказали. Он всё равно использовал его и получил судебный иск.
- В произведении Metastaseis Яниса Ксенакиса (1954) есть очень похожее вступительное крещендо (как и в других произведениях различных композиторов). Но оно начинается с одного тона и завершается полустройным тональным кластером вместо полностью созвучного, как в Deep Note. Звукозапись из патентной заявки можно прослушать здесь.
Обязательно послушайте звук, потому что при воссоздании Deep Note мы будем обращаться к этой конкретной записи.
Вот некоторые технические/теоретические факты, прежде чем приступить к синтезу звука:
- Моё наблюдение: на оригинальной записи с сайта патентного ведомства основной тон находтся между D и Eb, а в более новых вариантах фундаментальное значение между E и F. Мы будем использовать оригинальную константу D/Eb. Новые варианты обычно короче, если не ошибаюсь. Очевидно, что мне больше нравится тот вариант, что подавали в патентное ведомство.
- По словам доктора Мурера (и также подтверждено моими ушами), фрагмент начинается с осцилляторов, настроенных на случайные частоты между 200 Гц и 400 Гц. Но осцилляторы не просто гудят — их частоты модулируются случайным образом, и они используют сглаживающие фильтры для сглаживания случайных переходов тонов. Это продолжается до начала крещендо.
- Внутри крещендо и в конце звукового фрагмента рандомизаторы всё ещё модулируют частоты осцилляторов, поэтому ни один из них не является стабильным в какой-то момент времени. Но диапазон случайной развёртки настолько узкий, что просто добавляет естественное/хоровое звучание.
- Доктор Мурер вспоминает, что в спектре оцифрованного звука виолончели было около 12 внятных гармоник.
- Насколько мне известно, значения для генератора (которые использовались для получения авторских прав) в письменном виде так никогда и не публиковались. Д-р Мурер говорит, что может записать их, если мы получим разрешение от THX. Но я думаю, что это необязательно для воссоздания звука.
- Звук в финале (технически не аккорд) — на мой слух, просто сложение октав основного тона. Так что при воссоздании начнём со случайно настроенных (между 200 и 400 Гц) осцилляторов, сделаем более-менее сложную развёртку и завершим наложением октав на основной тон между низкими D/Eb.
Итак, приступим. Здесь мой рабочий инструмент — SuperCollider. Начнём с простого сэмпла. В качестве источника хочу использовать пилообразную волну, у неё богатый и гармонический спектр из чётных и нечётных компонентов. Позже я планирую отфильтровать вершины. Вот фрагмент из начальной части кода:
//30 осцилляторов, распределённых по стереополю
(
{
var numVoices = 30;
//generating initial random fundamentals:
var fundamentals = {rrand(200.0, 400.0)}!numVoices;
Mix
({|numTone|
var freq = fundamentals[numTone];
Pan2.ar
(
Saw.ar(freq),
rrand(-0.5, 0.5), //stereo placement of voices
numVoices.reciprocal //scale the amplitude of each voice
)
}!numVoices);
}.play;
)
Я выбрал 30 осцилляторов для генерации звука, в соответствии с возможностями компьютера ASP, как рассказывал д-р Мурер. Создал массив из 30 случайных частот между 200 и 400 Гц, распределил их случайным образом по стереополю с помощью Pan2.ar с аргументом rrand (-0.5, 0.5), назначил частоты пилообразным осцилляторам (30 экземпляров). Вот как это звучит.
Если изучить информацию от д-ра Мурера и/или внимательно прислушаться к оригинальному фрагменту, то можно услышать, что частоты осцилляторов случайным образом смещаются вверх и вниз. Хочется добавить этот эффект для более органичного звучания. Частотная шкала логарифмическая, поэтому на низких частотах должны быть более узкие диапазоны колебаний, чем на более высоких. Такое можно реализовать сортировкой наших случайно сгенерированных частот с присвоением LFNoise2 (который генерирует квадратично интерполированные случайные значения) аргументов mul по порядку внутри нашего макроса Mix. И я ещё добавил для осцилляторов фильтр нижних частот с частотой среза по пятикратной частоте осциллятора и умеренным 1/q:
//добавление к частотам случайных колебаний, сортировка, фильтр низких частот
(
{
var numVoices = 30;
//sorting to get high freqs at top
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
Mix
({|numTone|
//fundamentals are sorted, so higher frequencies drift more.
var freq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
Pan2.ar
(
BLowPass.ar(Saw.ar(freq), freq * 5, 0.5),
rrand(-0.5, 0.5),
numVoices.reciprocal
)
}!numVoices);
}.play;
)
Вот как звучит образец с последними правками.
Это уже выглядит хорошей отправной точкой, поэтому приступим к реализации развёртки, сначала очень грубо. Чтобы реализовать развёртку, сначала нужно определить окончательные частоты для каждого осциллятора. Это не очень просто, но и не очень сложно. Основной тон должен находиться между низкими D и Eb, так что средней частотой для этого тона будет 14,5 (0 — это C, отсчитывая хроматически, без первой октавы). Так что для 30 осцилляторов переводим случайные частоты между 200 и 400 Гц в значение 14,5 и соответствующие октавы. На слух я выбрал первые 6 октав. Итак, конечный массив частот получается таким:
(numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
Будем использовать развёртку от 0 до 1. Случайные частоты умножаются на значение (1 − развёртка)
, а целевые частоты умножаются на саму развёртку. Поэтому когда развёртка равна 0 (начало), то частота будет случайной. Когда развёртка 0,5, то получается ((рандом + целевая частота) / 2)
, а когда равна 1, то частота и будет конечным значением. Вот модифицированный код:
//создаём начальную развёртку (грубо), создаём финальные частоты
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var sweepEnv = EnvGen.kr(Env([0, 1], [13]));
Mix
({|numTone|
var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
var destinationFreq = finalPitches[numTone];
var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
Pan2.ar
(
BLowPass.ar(Saw.ar(freq), freq * 5, 0.5),
rrand(-0.5, 0.5),
numVoices.reciprocal //scale the amplitude of each voice
)
}!numVoices);
}.play;
)
Звук здесь.
Как я уже говорил, это очень грубая развёртка. Она линейно повышается от 0 до 1, что не согласуется с оригинальной композицией. Также вы могли заметить, что последние октавы звучат ужасно, потому что они настроены на идеальные октавы и сливаются друг с другом как базовые тона и овертоны. Мы это исправим, добавив случайное колебание на финальном этапе — так же, как делалось вначале, и это будет звучать гораздо более органично.
Сначала нужно исправить общую формулу частотной развёртки. Предыдущая была просто для пробы. Если посмотрим на оригинал, то заметим, что в первые 5−6 секунд в звучании очень мало изменений. После этого происходит быстрая и экспоненциальная развёртка, которая уводит осцилляторы в конечные октавные интервалы. Вот вариант, который я выбрал:
sweepEnv = EnvGen.kr(Env([0, 0.1, 1], [5, 8], [2, 5]));
Здесь переход от 0 до 0,1 занимает 5 секунд, а переход от 0,1 до 1 занимает 8 секунд. Курватуры для этих сегментов установлены в 2 и 5. Позже послушаем, что получилось, но сначала надо исправить ещё окончательные интервалы. Как и раньше, добавим случайные колебания с LFNoise2, диапазон которого пропорционален конечной частоте осциллятора. Это сделает финал более органичным. Вот изменённый код:
//настройка развёртки и финального аккорда
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var sweepEnv = EnvGen.kr(Env([0, 0.1, 1], [5, 8], [2, 5]));
Mix
({|numTone|
var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
var destinationFreq = finalPitches[numTone] + LFNoise2.kr(0.1, (numTone / 4));
var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
Pan2.ar
(
BLowPass.ar(Saw.ar(freq), freq * 8, 0.5),
rrand(-0.5, 0.5),
numVoices.reciprocal
)
}!numVoices);
}.play;
)
Здесь я ещё подкорректировал частоту среза фильтра нижних частот на свой вкус. Мне нравится подправлять вещи, если результат не ухудшается… В любом случае, вот что получилось.
Мне не очень нравится эта схема развёртки. Нужно растянуть начало и ускорить финиш. Или подождите… разве обязательно реализовать одинаковую схему для всех осцилляторов? Совершенно нет! Каждый осциллятор должен иметь собственную схему с немного разными значениями времени и курватуры — я уверен, что это будет интереснее. Ещё немного раздражают высокочастотные обертоны случайного пилообразного кластера, поэтому добавляем к общему результату фильтр нижних частот, отсечение которых контролируется глобальным «внешним» значением, не имеющим ничего общего со схемами осцилляторов. Вот измененный код:
//кастомные схемы. Фильтр нижних частот в конце
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var outerEnv = EnvGen.kr(Env([0, 0.1, 1], [8, 4], [2, 4]));
var snd = Mix
({|numTone|
var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
var destinationFreq = finalPitches[numTone] + LFNoise2.kr(0.1, (numTone / 4));
var sweepEnv =
EnvGen.kr(
Env([0, rrand(0.1, 0.2), 1], [rrand(5.0, 6), rrand(8.0, 9)],
[rrand(2.0, 3.0), rrand(4.0, 5.0)]));
var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
Pan2.ar
(
BLowPass.ar(Saw.ar(freq), freq * 8, 0.5),
rrand(-0.5, 0.5),
numVoices.reciprocal
)
}!numVoices);
BLowPass.ar(snd, 2000 + (outerEnv * 18000), 0.5);
}.play;
)
Небольшое изменение сделало развёртку немного более интересной. Фильтр нижних частот на 2000 Гц помогает укротить начальный кластер. Вот как это звучит.
Осталось ещё одно, что сделает процесс интереснее. Помните, мы сортировали случайные осцилляторы в начале? Ну, теперь мы можем отсортировать их в обратном порядке и убедиться, что осцилляторы на более высоких случайных частотах в конечном итоге попадают в нижние голоса после крещендо, и наоборот. Это добавит в крещендо больше «движения» и вполне согласуется с тем, как структурирован оригинальный фрагмент. Не уверен, что доктор Мурер запрограммировал его именно так, но на записи есть этот процесс, и она звучит круто, будь то случайный продукт генеративного процесса или специальный выбор. (О, я это сказал? Если процесс предусматривает такой вариант, то это и есть выбор… или нет?). Таким образом, изменим порядок сортировки и структуру кода, чтобы зубья пилы с более высокими частотами попадали в более низкие голоса в финале, и наоборот.
Ещё одно: нужен более громкий бас. Сейчас у всех голосов одинаковая амплитуда. Я хочу, чтобы низкие звуки звучали чуть громче и затухали пропорционально увеличению частоты. Поэтому соответствующим образом изменим аргумент mul для Pan2. Повторно настроим частоты среза фильтров нижних частот для отдельных осцилляторов. И я собираюсь добавить схему масштабирования амплитуды, которая будет плавно вступать в действие и исчезать к финалу, и освободиться от scserver. Еще несколько численных настроек там и тут — и вот окончательный код:
//инвертирование init sort, более громкий бас, финальная схема громкости, несколько маленьких подкруток
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort.reverse;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var outerEnv = EnvGen.kr(Env([0, 0.1, 1], [8, 4], [2, 4]));
var ampEnvelope = EnvGen.kr(Env([0, 1, 1, 0], [3, 21, 3], [2, 0, -4]), doneAction: 2);
var snd = Mix
({|numTone|
var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 6 * (numVoices - (numTone + 1)));
var destinationFreq = finalPitches[numTone] + LFNoise2.kr(0.1, (numTone / 3));
var sweepEnv =
EnvGen.kr(
Env([0, rrand(0.1, 0.2), 1], [rrand(5.5, 6), rrand(8.5, 9)],
[rrand(2.0, 3.0), rrand(4.0, 5.0)]));
var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
Pan2.ar
(
BLowPass.ar(Saw.ar(freq), freq * 6, 0.6),
rrand(-0.5, 0.5),
(1 - (1/(numTone + 1))) * 1.5
) / numVoices
}!numVoices);
Limiter.ar(BLowPass.ar(snd, 2000 + (outerEnv * 18000), 0.5, (2 + outerEnv) * ampEnvelope));
}.play;
)
А вот и окончательная запись произведения.
Можете сравнить с оригиналом.
Да, это моя интерпретация. И конечно, её можно оптимизировать до смерти, меняя схемы, частоты, распределение, что угодно… тем не менее, я думаю, что это достойная попытка сохранить звуковое наследие. Хотелось бы услышать ваши комментарии и/или собственные попытки синтеза этого крещендо.
Да, и вот ещё одна вещь, которую я сделал ради удовольствия. Помните, я рассказывал, что для генерации оригинала понадобилось 20 000 строк кода на С. Я почти уверен, что доктору Муреру пришлось всё писать вручную, так что такая цифра неудивительна. Но вы знаете, в связи с популярностью твиттера мы пытаемся всё втиснуть в 140 символов кода. Для удовольствия я попытался воспроизвести основные элементы композиции в 140 символах кода. Думаю, что сэмпл ещё звучит круто, вот код (здесь с основным тоном F/E):
play{Mix({|k|k=k+1/2;2/k*Mix({|i|i=i+1;Blip.ar(i*XLine.kr(rand(2e2,4e2),87+LFNoise2.kr(2)*k,15),2,1/(i/a=XLine.kr(0.3,1,9))/9)}!9)}!40)!2*a}
И вот звук, который генерирует эта версия.
В одном документе — весь код с этой страницы для ваших экспериментов.
Удачного крещендо, друзья!