Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
День добрый, камрады!
Я пока только начинающий музыкант, зато есть опыт в программировании. И почему бы не взять какие-нибудь данные и попробовать из аудиализировать (это как визуализировать, только… кэпъ)?
Тащемта, план таков:
- Найти данные
- Придумать, как сконвертировать их в звук
- Подправить параметры конвертера, чтобы было покрасивше
- Остались ещё силы? goto 1
Ловим космические лучи
Данные я взял с сайта: http://www.tien-shan.org/she/vardbaccess/index.html
Конкретно, я использовал данные “Вариационных измерений” космических лучей за дефолтный период — 31.03-10.04 — Tien-Shan и Tien-Shan Underground (чтобы можно было ещё проследить какие-то корреляции или гармонии). И для разнообразия данных я скачал измерения “Гамма-излучения”, тоже за дефолтный период — в этот раз 5.04-10.04.
Tien-Shan данные выглядят так:
31.03.2020 00:00:00 3813 5504 5187 5251 4637 4071 4568 4998 4922 4858 4956 4271 3997 4358 4715 4077 4160 3980
31.03.2020 00:01:00 3653 5308 5413 5371 4691 4090 4617 5139 5009 4762 5172 4309 4208 4387 4923 4248 4092 4108
31.03.2020 00:02:00 3763 5309 5292 5298 4588 4105 4608 5072 5070 4745 4834 4158 3918 4284 5115 4233 4011 3972
…
09.04.2020 23:57:00 3855 5308 5239 5190 4531 4063 4537 5035 5084 4863 5089 4261 4122 4395 5394 4186 4167 4078
09.04.2020 23:58:00 3955 5492 5416 5406 4458 4037 4474 5122 4942 4733 5168 4330 4026 4357 5283 4059 4174 3857
09.04.2020 23:59:00 3811 5378 5334 5121 4472 3955 4334 4992 4940 4646 4822 4378 4137 4195 4880 4049 4002 3817
График Tien-Shan:
Структура данных Tien-Shan Underground идентична структуре выше, но сами значения сильно ниже:
31.03.2020 00:00:00 20 32 29 26 20 20 17 14 16
31.03.2020 00:01:00 14 16 17 27 20 27 9 15 7
31.03.2020 00:02:00 13 22 16 22 15 18 12 15 11
…
09.04.2020 23:57:00 17 19 20 27 20 17 11 18 16
09.04.2020 23:58:00 14 33 19 19 18 16 15 16 9
09.04.2020 23:59:00 16 31 23 25 21 22 16 10 14
График Tien-Shan Underground:
И данные гамма-излучения выглядят так:
05.04.2020 00:00:02 20640 5345 4726 3532 2365 2118 1261 891 655 334 252 194
05.04.2020 00:00:12 21160 5295 4666 3526 2365 2138 1250 852 627 309 235 190
05.04.2020 00:00:22 19581 5189 4562 3536 2401 2130 1229 820 618 324 243 172
…
09.04.2020 23:59:37 22381 5134 4505 3429 2274 2044 1223 823 569 275 205 154
09.04.2020 23:59:47 21917 5186 4577 3448 2273 2044 1166 803 563 293 214 165
09.04.2020 23:59:58 20930 5275 4644 3561 2382 2148 1248 868 609 289 232 176
График Radio point Mu1:
Если я буду использовать какие-то другие данные — я обновлю декларацию константных исходников (абзац выше)
Первая попытка
Первая попытка у нас влоб. У нас есть числа. И, например, Audacity тоже может принимать на вход числа. Но! Эти числа разные, поэтому нам нужно привести наши космические лучи во что-то понятное программам.
В данном случае, Audacity принимает float значения амплитуды от -1.00000 до 1.00000, где -1/1 = 0dB — максимальная громкость, которая снижается при приближении к нулю с обеих сторон.
Нам на руку играет то, что этот csv, судя по хелпу, парсит значения по (\s\t\n)+
, так что мы даже сможем отформатировать наши данные, чтобы проще в них разбираться.
Немного о импорте данных в Audacity
Вот примеры raw данных для Audacity с их сайта:
Один цикл волны с частотой 1кГц:
;This is a comment and will be ignored.
0.07100 0.14056 0.20727 0.26978 0.32682 0.37724 0.42001 0.45428 0.47933 0.49468 0.50000 0.49518 0.48034 0.45575 0.42193 0.37957 0.32951 0.27277 0.21050 0.14397 0.07452 0.00356 -0.06747 -0.13713 -0.20402 -0.26677 -0.32411 -0.37489 -0.41807 -0.45278 -0.47831 -0.49415 -0.49997 -0.49566 -0.48131 -0.45721 -0.42384 -0.38188 -0.33218 -0.27575 -0.21373 -0.14738 -0.07804 -0.00712 0.06394 0.13370 0.20076 0.26375 0.32139 0.37252 0.41611 0.45125 0.47726 0.49359 0.49992 0.49612 0.48226 0.45864 0.42571 0.38416 0.33483 0.27871 0.21694 0.15078 0.08156 0.01068 -0.06040 -0.13027 -0.19749 -0.26072 -0.31866 -0.37014 -0.41412 -0.44971 -0.47618 -0.49301 -0.49984 -0.49655 -0.48319 -0.46004 -0.42757 -0.38643 -0.33747 -0.28166 -0.22015 -0.15417 -0.08507 -0.01425
Для стерео мы пишем два значения для каждого канала:
0.00000 -0.00000
0.10000 -0.10000
0.20000 -0.20000
0.30000 -0.30000
0.40000 -0.40000
0.50000 -0.50000
0.60000 -0.60000
0.70000 -0.70000
0.80000 -0.80000
0.90000 -0.90000
1.00000 -1.00000
Здесь у нас постепенное усиление громкости в обоих каналах, но правый относительно левого имеет обратную фазу (когда волна в левом идёт вверх, в правом — идёт вниз).
**Фазовая инверсия одного из каналов может вылиться в тишину, когда стерео совмещается в моно. Потому что равные значения с разным знаком аннигилируются в ноль, а ноль — это, как мы помним, тишина — 0dB.
PoC — Proof of Concept. ЧЧР — Что-то, что работает.
Начнём с обычного моно в один канал. Нам нужно проверить работоспособность подхода.
Для того, чтобы Audacity съел наши данные и не подавился, нам нужно сконвертировать данные в формат float [-1.00000-1.00000]. Для этого подведёт небольшую статистику по данным: минимум, максимум, медиана, среднеарифметическое и мода.
Берём первый попавшийся скриптовый язык (для меня это F12), и давайте напишем скрипт, который из массива нам посчитает все нужные данные.
Сначала разделим колонки. Для первого теста можно и вручную в саблайме с помощью Find & Replace и регулярки: (.+?\t){x}(.+?)\t.+?\n->
\2,`, где х — это номер нужного столбца + 1 (там два сдвига вначале).
Получим csv вида:
3813,3790,3801,3833,3674,3822,3639,3848,3866,3794,3747,3938,3823,3989,3963,3852,3836,3694,3883,3748,3802,3884,3790,3684,3895,3872,3885,4011,3844,3901,3713,3870,3868,3772,3866,3939,3856,3720,3640,3929,3905,...
Дальше пишем функцию, которая нам будет считать арифметические данные:
a = [1, 2, 3, 4, 5]
function parse_array(arr) {
console.log(“Min: “ + Math.min(...arr));
console.log(“Max: “ + Math.max(...arr));
console.log(“Avg: “ + arr.reduce((a, b) => a + b) / arr.length);
// console.log("Min: " + Math.min(...arr) + "; Max: " + Math.max(...arr) + "; Avg: " + arr.reduce((a, b) => a + b) / arr.length);
}
> parse_array(a)
1
5
3
Отлично, теперь прогоняем через реальные данные, и получаем такую табличку для всех колонок в файле Tien-Shan:
1: Min: 3514; Max: 4192; Avg: 3844.4135528815705
2: Min: 4934; Max: 5741; Avg: 5358.663125307817
3: Min: 4945; Max: 5703; Avg: 5313.77323577007
4: Min: 4882; Max: 5725; Avg: 5281.23640329276
5: Min: 4219; Max: 4953; Avg: 4598.3294167311615
6: Min: 3712; Max: 4426; Avg: 4075.323506648843
7: Min: 4168; Max: 4966; Avg: 4530.841131358616
8: Min: 4576; Max: 5344; Avg: 4973.967775979737
9: Min: 4608; Max: 5478; Avg: 4976.573559417435
10: Min: 4459; Max: 5261; Avg: 4795.212692605362
11: Min: 4574; Max: 5380; Avg: 4989.718215718005
12: Min: 3944; Max: 4757; Avg: 4331.33553788785
13: Min: 0 (3771); Max: 4428; Avg: 4095.4295363399706
14: Min: 0 (4084); Max: 4865; Avg: 4407.586716386407
15: Min: 0 (4384); Max: 5394; Avg: 4866.680292689791
16: Min: 0 (3837); Max: 4703; Avg: 4238.36325898825
17: Min: 0 (3786); Max: 4661; Avg: 4204.146837402378
18: Min: 0 (3753); Max: 4596; Avg: 4152.545697600788
*В скобках минимальное значение не считая нуля, duh.
Давайте теперь подправим нашу функцию, чтобы она сразу конвертировала наши данные в float [-1.00000-1.00000] формат:
a = [1, 2, 3, 4, 5]
function parse_array(arr) {
arr = arr.filter(a => a != 0); // to remove zeros
min = Math.min(...arr);
max = Math.max(...arr);
avg = arr.reduce((a, b) => a + b) / arr.length;
console.log("Min: " + min + "; Max: " + max + "; Avg: " + avg);
floatify = function (a) {
if (a == 0) return 0;
if (a > avg) {
return (a - avg) / (max - avg);
} else {
return (a - min) / (avg - min) - 1;
}
console.log(arr.map(a => floatify(a)));
}
> parse_array(a)
Min: 1; Max: 5; Avg: 3
[-1, -0.5, 0, 0.5, 1]
Получаем данные вида:
-0.09502086396735898
-0.579290635757401
-0.24635516765174703
-0.1646346436621775
-0.1313410968516121
-0.03448714249360363
Загружаем их в Audacity через Tools -> Sample Data Import…
И получаем что-то подобное (первый столбец Tien-Shan):
Звучит как шум. Максимально правдоподобно, но абслютно негармонично и как музыка не воспринимается.
Отбой.
Вторая попытка (неудачная)
Давайте пересмотрим исходные данные. Мне уже давно чесало мозг то, что столбцы в данных не подписаны. Поэтому я нашёл другую таблицу, где уже чуть больше понятно:
Timestamp FractionalDate UncorrectedCountRate[cts/min] CorrectedCountRate[cts/min] Pressure[mbar]
2020-03-11T00:00:00Z 71.0000000 7150 6702 991.00
2020-03-11T00:30:00Z 71.0208333 7205 6749 990.91
2020-03-11T01:00:00Z 71.0416667 7214 6750 990.75
2020-03-11T01:30:00Z 71.0625000 7250 6776 990.61
2020-03-11T02:00:00Z 71.0833333 7275 6792 990.45
И нужно поменять подход, потому что данные меняются слишком быстро и слишком хаотично.
Два варианта:
Использовать точки как значение амплитуды и добавить промежуточные колебания (данные->амплитуда)
Использовать точки как данные для косвенной информации: detune, частота итд
Overtone
Давайте, чтобы проще генерировать звуки и музыку, установим какое-нибудь ПО. Как раз я давно планировал познакомиться с языками программирования, на которых можно писать музыку. Так что я остановился на языке Overtone (только потому что вот: https://www.youtube.com/watch?v=imoWGsipe4k).
Ставим по инструкции: https://github.com/overtone/overtone
Запаситесь парой часов-дней в зависимости от опыта на установку и настройку Ovetone. Там нужны: Java, Clojure, Supercollider, Leiningen, JackD и обязательно будут проблемы между jackd, pulseaudio и alsa. Это же линукс!
На винде у меня не получилось установить Clojure и Leiningen.
Мак у меня есть, но на нём тоже линь, поэтому проверить не могу.
Clojure
Таки, добро пожаловать обратно в нашу статью. Надеюсь, прошло не слишком много времени и вы примерно помните, зачем мы всем этим занимаемся.
У нас есть Clojure, и данные.
Проверим первое. В проекте, из которого вы запускаете lein repl
(это в инструкциях по установке было), создадим файл с названием tone.clj, в нём у нас будет скрипт, который мы сможем запустить из repl-а.
Добавим в файл одну строку:
(demo 0.5 (sin-osc 440))
С тем, что это значит мы разберёмся позже. Сейчас нам нужно проверить, что у нас есть звук из звукоиздавателей.
Переходим в
lein repl
Импортим ovetone
user=> (use ‘overtone.live)
И открываем наш файл
user=> (load-file “tone.clj”)
Слышим звук? Чудесно!
Не слышим? Что-то произошло не по инструкциям установки. Извините, я не смогу помочь без конкретностей…
Данные
Теперь найдём, как мы будем скармливать наши данные нашему “синтезатору”.
Проще всего будет выгружать/загружать файлы прямо из кода, чтобы не писать сотню тысяч чисел вручную.
Делается это не шибко сложно:
user=> (def a [1,2,3])
#'user/a
user=> (prn-str a)
"[1 2 3]\n"
user=> (spit "stored-array.dat" (prn-str a))
nil
user=> (slurp "stored-array.dat")
"[1 2 3]\n"
*это уже не ЖС, это Clojure, на котором запущен overtone.live — см инструкции
Заодно мы проверили, в каком виде экспортируется массив в Clojure. Нам нужно будет убрать запятые из наших исходных данных, обрамить их квадратными скобками, и, вероятно, даже схлопнуть в одну строку — это мы проверим.
UPD: Да, кложура парсит и \n символы, поэтому добавим небольшую функцию для комфорта и услады глаз:
user=> (defn trim [input] (clojure.string/replace input #"\n" ""))
#'user/trim
Теперь, если у нас есть переносы строк в данных, мы их легко потрём (\n, а не данные…):
user=> (trim (slurp “stored-array.dat”))
“[1 2 3]”
На этом месте у меня опустились руки, видя как много нужно выучить, чтобы суметь сгенерировать кастомный звук в overtone. Я даже не мог представить общей картины, поэтому этот шаг резко обрывается даже не начавшись.
Третья попытка
После осознания того, что я слишком ленив и глуп, чтобы разобраться с overtone, я сделал паузу в несколько дней и вернулся к этой теме уже с новой идеей.
Имя этой идее — Shadertoy.com
Ожидали? Я сам не ожидал. А там можно генерировать не только картинку, но и звук!
И всего лишь одной строкой:
// Возвращает vec2() для правого и левого канала
vec2 mainSound(float time) {
// 6.2831 ~ 2pi
// exp() экспоненциально падает, создавая затухание звука
// Косинус для создания синусоиды с заданной частотой
return vec2(sin(6.2831 * 440.0 * time) * exp(-2.0 * time));
}
**(ну… почти одной… но самая важная — одна)
А ещё есть отличная особенность — shadertoy можно эмбеддить, поэтому вот вам нота ля:
Дальше научимся играть несколько нот:
// Возвращает vec2() для правого и левого канала
vec2 mainSound(float time) {
vec2 result = vec2(sin(6.2831 * 440.0 * time) * exp(-2.0 * time));
// One second later
if (time > 1.0) {
// (time - x) нужен, потому что нам нужно сбросить
// начальное число для exp() функции, иначе результат
// exp() будет глобальным и мы услышим только первую ноту
result = vec2(sin(6.2831 * 262.0 * time) * exp(-2.0 * (time - 1.0)));
}
return result;
}
В туториале на Shadertoy кода чуть больше, но и мелодия повеселее!
Ещё пара вещей, чтобы звук был красивее — это:
- Написать структуру для наших нот, чтобы было удобнее их создавать.
struct Note { // в Герцах float frequency; // Пауза - на какой секунде сыграть ноту float offset; // Длительность дробью (1, 2, 0.25, 1/16…) float duration; };
- Вынести вычисление амплитуды (конечного числа для
return
) в функцию:
float noteFreq(Note note, float time) { // 6.2831 = 2pi // exp() экспоненциально падает, создавая затухание звука // Косинус для создания синусоиды с заданной частотой return cos(6.2831 * note.frequency * time) * exp(-1.0/note.duration * (time - note.offset)); }
И подправлять частоту, полученную из “космического луча”, подгоняя её под существующую ноту (в западной системе; это где 12 полутонов и вся классика).
// Maps frequency to the nearest note from [scale] float nearestNote(float value) { // Найти последнюю ноту из набора, чья частота меньше заданной for (int i = 1; i < scale.length(); i++) { if (scale[i] > value) { return scale[i - 1]; } } // Проблемы с входящими данными return scale[0]; }
Оба блока надо добавить над vec2 mainSound(float time) {...}
функцией.
Отлично. Теперь, грубо говоря, у нас есть boilerplate для создания музыки.
Космические лучи
Пришла пора добавлять космические лучи в наш код. Я подготовил данные с того же Тянь-Шаня, благо смог придумать, как их оформить.
В задумке, чтобы не было слишком скучно, я взял три столбца из двух таблиц (два из надземной и один из подземной станции). Дополнительно, я изменил скорость и длительность “вызова” нот, получив мелодию с двумя ритмами: 2:1 (4:1) у высокой и бас партий и 3:1 у средней партии. Чтобы все партии закончились одновременно, я взял соответствующее количество значений для каждой из партий, пропорционально темпу. Получилось так:
const float leadTempoRatio = 2.0; // 2 ticks per second
const float midTempoRatio = 2.0/3.0; // 1.5 ticks/second
const float bassTempoRatio = 1.0; // 1 tick per second
// 200 items, see above
const float[] dataLead = float[] (5504.0, 5308.0, 5309.0, 5289.0, 5225.0, 5208.0, 5190.0, 5250.0, 5362.0, 5486.0, 5314.0, 5467.0, 5292.0, 5305.0, 5167.0, 5423.0, 5402.0, 5280.0, 5420.0, 5428.0, 5260.0, 5306.0, 5379.0, 5283.0, 5234.0, 5340.0, 5252.0, 5568.0, 5476.0, 5248.0, 5494.0, 5480.0, 5230.0, 5609.0, 5323.0, 5392.0, 5304.0, 5478.0, 5321.0, 5435.0, 5179.0, 5444.0, 5289.0, 5413.0, 5275.0, 5389.0, 5500.0, 5221.0, 5276.0, 5356.0, 5250.0, 5414.0, 5269.0, 5269.0, 5216.0, 5512.0, 5410.0, 5300.0, 5426.0, 5433.0, 5156.0, 5482.0, 5281.0, 5377.0, 5279.0, 5317.0, 5111.0, 5455.0, 5435.0, 5239.0, 5353.0, 5342.0, 5519.0, 5242.0, 5281.0, 5226.0, 5374.0, 5190.0, 5232.0, 5292.0, 5466.0, 5298.0, 5265.0, 5521.0, 5435.0, 5252.0, 5245.0, 5506.0, 5491.0, 5343.0, 5390.0, 5287.0, 5349.0, 5332.0, 5515.0, 5358.0, 5369.0, 5396.0, 5187.0, 5308.0, 5322.0, 5207.0, 5355.0, 5388.0, 5265.0, 5217.0, 5254.0, 5494.0, 5306.0, 5380.0, 5352.0, 5297.0, 5395.0, 5387.0, 5410.0, 5448.0, 5301.0, 5182.0, 5465.0, 5327.0, 5617.0, 5362.0, 5417.0, 5470.0, 5549.0, 5283.0, 5425.0, 5419.0, 5307.0, 5405.0, 5286.0, 5228.0, 5400.0, 5426.0, 5378.0, 5396.0, 5514.0, 5393.0, 5314.0, 5318.0, 5431.0, 5236.0, 5257.0, 5239.0, 5447.0, 5439.0, 5399.0, 5484.0, 5455.0, 5226.0, 5586.0, 5491.0, 5338.0, 5390.0, 5275.0, 5278.0, 5474.0, 5332.0, 5320.0, 5355.0, 5387.0, 5435.0, 5406.0, 5196.0, 5363.0, 5500.0, 5466.0, 5443.0, 5248.0, 5510.0, 5342.0, 5270.0, 5123.0, 5485.0, 5318.0, 5469.0, 5249.0, 5330.0, 5406.0, 5543.0, 5203.0, 5281.0, 5395.0, 5416.0, 5249.0, 5252.0, 5372.0, 5397.0, 5327.0, 5260.0, 5430.0, 5334.0, 5309.0, 5435.0, 5381.0, 5324.0, 5399.0, 5504.0, 5320.0, 5458.0);
// 150 items
const float[] dataMid = float[] (3813.0, 3653.0, 3763.0, 3790.0, 3801.0, 3833.0, 3674.0, 3822.0, 3639.0, 3848.0, 3866.0, 3794.0, 3747.0, 3938.0, 3823.0, 3989.0, 3963.0, 3852.0, 3836.0, 3694.0, 3883.0, 3748.0, 3802.0, 3884.0, 3790.0, 3684.0, 3895.0, 3872.0, 3885.0, 4011.0, 3844.0, 3901.0, 3713.0, 3870.0, 3868.0, 3772.0, 3866.0, 3939.0, 3856.0, 3720.0, 3640.0, 3929.0, 3905.0, 3811.0, 3811.0, 3899.0, 3699.0, 3868.0, 3892.0, 3746.0, 3878.0, 3778.0, 3894.0, 3740.0, 3709.0, 3710.0, 3812.0, 3856.0, 3811.0, 3935.0, 3850.0, 3859.0, 3800.0, 3748.0, 3725.0, 3814.0, 3897.0, 3745.0, 3763.0, 3833.0, 3964.0, 3770.0, 3846.0, 3776.0, 3945.0, 3791.0, 3799.0, 3709.0, 3922.0, 3825.0, 3804.0, 3869.0, 3829.0, 3770.0, 3838.0, 3820.0, 3734.0, 3979.0, 3765.0, 3764.0, 3857.0, 3861.0, 3869.0, 3787.0, 3963.0, 3780.0, 3847.0, 3759.0, 3857.0, 3782.0, 3711.0, 3843.0, 3909.0, 3839.0, 3811.0, 3874.0, 3849.0, 3883.0, 3925.0, 3752.0, 3847.0, 3731.0, 3824.0, 3905.0, 3901.0, 3926.0, 3897.0, 3751.0, 3896.0, 3752.0, 3854.0, 3936.0, 3767.0, 3812.0, 3933.0, 3889.0, 3808.0, 3703.0, 3948.0, 3883.0, 3872.0, 3762.0, 3870.0, 3899.0, 3818.0, 3900.0, 3774.0, 3951.0, 3818.0, 3893.0, 3821.0, 3823.0, 3801.0, 3833.0, 3744.0, 3769.0, 3864.0, 3923.0, 3974.0, 3810.0);
// 100 items
const float[] dataBass = float[] (20.0, 14.0, 13.0, 14.0, 10.0, 23.0, 16.0, 18.0, 6.0, 16.0, 18.0, 7.0, 15.0, 13.0, 14.0, 22.0, 12.0, 14.0, 12.0, 14.0, 19.0, 18.0, 8.0, 16.0, 13.0, 12.0, 21.0, 17.0, 22.0, 19.0, 11.0, 16.0, 16.0, 24.0, 16.0, 20.0, 18.0, 20.0, 17.0, 22.0, 23.0, 14.0, 17.0, 16.0, 23.0, 14.0, 15.0, 10.0, 12.0, 14.0, 20.0, 22.0, 14.0, 14.0, 20.0, 24.0, 14.0, 23.0, 20.0, 25.0, 14.0, 17.0, 21.0, 21.0, 18.0, 18.0, 13.0, 11.0, 21.0, 22.0, 16.0, 13.0, 24.0, 26.0, 29.0, 27.0, 18.0, 23.0, 15.0, 18.0, 8.0, 17.0, 24.0, 14.0, 12.0, 18.0, 16.0, 15.0, 25.0, 12.0, 19.0, 11.0, 18.0, 25.0, 21.0, 16.0, 19.0, 24.0, 19.0, 14.0);
Значения не самые красивые. А что важнее — они не несут никакого смысла в плане музыки (не в плане физики звука, а именно в плане музыки). Поэтому напишем конвертеры для каждой из партий (максимально субъективный код):
// Конвертирует значение высокой партии в “правильную” высокую частоту
// Можно не аккуратничать, потому что nearestNote()
// поправит все отклонения от “правильных” нот
float fixLead(float value) {
return nearestNote(value / 8.0);
}
// Конвертирует значение средней партии в “правильную” среднюю частоту
float fixMid(float value) {
return nearestNote(value / 10.0);
}
// Конвертирует значение бас партии в “правильную” бас частоту
float fixBass(float value) {
return nearestNote(value * 7.0);
}
И последний штрих — это добавить нашу гамму — наш набор нот, которые будут звучать.
Один из главных секретов музыки — внутри музыкальной системы (в нашем случае это классический темперированный строй), чем больше следуешь правилам — тем приятнее получаются звуки.
В нашем случае правилом будет создание гамм. Точнее, поиск. Точнее, просто copy-paste, потому что гаммы уже давно придуманы :)
//"Правильные" ноты. Здесь - C Major гамма от C0 до B8
const float[] scale = float[] (16.35, 18.35, 20.60, 21.83, 24.50, 27.50, 30.87, 32.70, 36.71, 41.20, 43.65, 49.00, 55.00, 61.74, 65.41, 73.42, 82.41, 87.31, 98.00, 110.00, 123.47, 130.81, 146.83, 164.81, 174.61, 196.00, 220.00, 246.94, 261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25, 587.33, 659.25, 698.46, 783.99, 880.00, 987.77, 1046.50, 1174.66, 1318.51, 1396.91, 1567.98, 1760.00, 1975.53, 2093.00, 2349.32, 2637.02, 2793.83, 3135.96, 3520.00, 3951.07, 4186.01, 4698.63, 5274.04, 5587.65, 6271.93, 7040.00, 7902.13);
Сегодня это гамма До Мажор от C0 до B8, что значит, что у нас в распоряжении есть 8 октав по 7 нот. И все они будут звучать хорошо друг с другом (если не перестараться, конечно).
Именно к этим частотам будут “подтягиваться” наши космические звуки после того как мы прогоним их через fix*()
функцию.
Важно: Нужно помнить, что сначала мы приводим число к частоте, а потом уже эту частоту приводим к конкретной ноте.
В итоге, в mainSound(float time) {...}
функции нам нужно только пройти по всем нашим космическим данным, перевести их в частоту звука и вычислить амплитуду по уже давно известной функции noteFreq()
.
float result = 0.0;
// Высокая партия
for (int i = 0; i < dataLead.length(); i++) {
// Высокая партия; Скорость - 2.0x, все ноты - четвертные (1/4), старт со 2 секунды
Note noteLead = Note(fixLead(dataLead[i]), float(i) + 2.0, 1.0/4.0);
// result = звук
// += потому что мы накладываем звуки новых и ещё звучащих нот
if (time > noteLead.offset) {
float amplitude = noteFreq(noteLead, time);
result += amplitude;
}
}
// Возвращает vec2() для правого и левого канала
return vec2(result);
То же и для двух других партий. Это есть по ссылке в конце. Сначала бонус.
Бонус
У Shadertoy есть специфика — он сначала рендерит аудио, а потом играет аудио поверх реал-тайм рендера картинки. Поэтому извне в музыку не залезть. И сами данные музыки там тоже извне не извлечь. Поэтому пойдём на хитрость.
К этому моменту мы писали весь звуковой код во вкладке Sound
. Он там чудесно звучал и никому не мешал. Нам нужно это сохранить. А ещё нельзя забывать про DRY
, если мы хотим сделать эквалайзер на нашу сгенерированную музыку, или какую визуализацию. В общем, чтобы наша картинка реагировала на звуки.
Для этого, перенесём весь код из вкладки Sound
во вкладку Common
, оставив только главную функцию:
// Звук рендерится перед запуском шейдера
// Поэтому нет возможности передать данные извне
// или достать данные из аудио
vec2 mainSound(float time) {
// Генерируем шаг в аудио
return _mainSound(time);
}
И создадим буфер для нашей новой текстуры (вкладка Buf A
):
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
// Генерируем шаг в аудио
_mainSound(iTime);
// Рендерим наш пиксель в текстуру
fragColor = vec4(FRAG_COLOR);
}
Это позволяет нам пользоваться нашей генерацией в главном рендере дважды — в пререндере для музыки, и во время рендера для картинки.
И музыка и картинка имеют доступ к глобальной переменной времени (iTime
или time
). Поэтому мы можем сделать визуализацию “под фонограмму”. Для простого отображения наших звуков, добавим ещё переменную для цвета каждой “ноты” в каждый момент времени (высокие ноты — синий; средние ноты — зеленый; низкие ноты — красный):
float result = 0.0;
vec4 frag_color = vec4(0.0);
frag_color.a = 1.0;
// High voice
for (int i = 0; i < dataLead.length(); i++) {
// Высокая партия; Скорость - 2.0x, все ноты - четвертные (1/4), старт со 2 секунды
Note noteLead = Note(fixLead(dataLead[i]), float(i) + 2.0, 1.0/4.0);
// result = звук
// frag_color = цвет пикселя
// += потому что мы накладываем звуки новых и ещё звучащих нот
if (time > noteLead.offset) {
float amplitude = noteFreq(noteLead, time);
result += amplitude;
frag_color.b += amplitude;
}
}
// Сохраняем значение пикселя
// Это используется во вкладке Buf A
FRAG_COLOR = frag_color;
// Возвращает vec2() для правого и левого канала
return vec2(result);
В итоге мы получаем мигание, отвечающее амплитуде волны нашего звука. То есть, мы не просто видим вспышку, а видим затухающее мерцание, вместе с затуханием звука.
Уборка
Дальше, для красоты, я скачал текстуру звёзд, и сделал псевдо-маску из нашей цветной музыки, чтобы звёзды немного мерцали под разные ноты.
**Для использования кастомной текстуры нужно установить специальное расширение для Shadertoy для браузера. Поэтому для бекапа я добавил обычную текстуру с шумом, чтобы было на что посмотреть.
Но я нашёл небольшой шейдер по созданию звёзд в космосе (https://www.shadertoy.com/view/XlfGRj) и добавил его в код. Выглядит потрясно!
Вот конечный вариант нашей космической музыки (с тем чужим шейдером):
Видео с выбранным мной звёздным небом я записал и выложил на ютуб: https://www.youtube.com/watch?v=r-4RJfCpepE
Если вдруг кому нужно, вот сгенерированный wav файл: https://soundcloud.com/arsenii-lisunov/cosmic-rays
**можно скачать...
Всем спасибо за ваш интерес!
Я не могу ручаться, что текст написан понятно, но могу обещать разжевать все непонятные моменты (которые я сам понимаю) в комментариях.
Всем ️
UPD: В shadertoy Sound вкладка может напрямую вызывать mainSound()
из Common
вкладки. Поэтому Sound
может быть пустым, а в Common
переименуем _mainSound()
в mainSound()
.
Также нам не нужен bufA
, потому что это лишний слой и мы можем вызывать mainSound()
напрямую из Image
вкладки, и в ней уже работать с полученным пикселем, без рендера в текстуру-посредник.
Всё по той же ссылке.