Как я писал музыку из космических лучей

    День добрый, камрады! Я пока только начинающий музыкант, зато есть опыт в программировании. И почему бы не взять какие-нибудь данные и попробовать из аудиализировать (это как визуализировать, только… кэпъ)?


    Тащемта, план таков:


    • Найти данные
    • Придумать, как сконвертировать их в звук
    • Подправить параметры конвертера, чтобы было покрасивше
    • Остались ещё силы? goto 1

    Ловим космические лучи


    Данные я взял с сайта. Конкретно, я использовал данные “Вариационных измерений” космических лучей за дефолтный период — 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 (только потому что вот).


    Ставим по инструкции:


    Запаситесь парой часов-дней в зависимости от опыта на установку и настройку 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 для браузера. Поэтому для бекапа я добавил обычную текстуру с шумом, чтобы было на что посмотреть.


    Но я нашёл небольшой шейдер по созданию звёзд в космосе (ссылка) и добавил его в код. Выглядит потрясно!


    Вот конечный вариант нашей космической музыки (с тем чужим шейдером):



    Видео с выбранным мной звёздным небом я записал и выложил на ютуб.


    Если вдруг кому нужно, вот сгенерированный wav-файл.
    **можно скачать...


    Всем спасибо за ваш интерес! Я не могу ручаться, что текст написан понятно, но могу обещать разжевать все непонятные моменты (которые я сам понимаю) в комментариях.


    Всем ️


    UPD: В shadertoy Sound вкладка может напрямую вызывать mainSound() из Common вкладки. Поэтому Sound может быть пустым, а в Common переименуем _mainSound() в mainSound().


    Также нам не нужен bufA, потому что это лишний слой и мы можем вызывать mainSound() напрямую из Image вкладки, и в ней уже работать с полученным пикселем, без рендера в текстуру-посредник.
    Всё по той же ссылке.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +1

      А эта музыка будет отличаться от написанной по последовательностям случайных чисел? В том плане, что это не белый шум? :)

        0
        В первом шаге — никак не будет отличаться. Разница между числами вносят слишком сильный хаос в float значения.

        С другой стороны, третий шаг (через шейдеры) как раз старается как можно меньше менять «высотное» отношение между количеством приходящих частиц каждую минуту.
        И, как мне кажется, это уже не случайные числа, и какие-то погодные или сезонные изменения тоже есть.
        Не буду отстаивать, что мои данные — не случайны. Это, всё же, значения за каждую минуту.

        Хотя, в них прослеживается своего рода стабильность (особенно в высоких нотах).
          +1

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

            0

            Дык, да, не спорю. В этом плане космос не сильно отличается от статистики на селе.


            Если брать более конкретные и интересные данные — получится лучше. И из уже достаточно будет добавить в нужные массивы и все будет играть

              0

              Главное чтоб звуки какие-то таинственные, а если гормошку или балалаечку использовать, то совсем другой настрой выйдет. Короче, до сих пор в этом плане меня зацепила только положенная на звук запись радиопомех от Юпитера или Сатурна. Но теперь я уж подозреваю, что там тоже изрядный привкус топорища в каше.

        0

        Поставьте себе VCV Rack, там есть модули позволяющие играть csv. Лучше, конечно, перегнать в миди или osc — задача сильно упростится, заодно в проводочках покопаетесь.

          0
          Для проводочков у меня есть ардуино и сделанный наполовину синтезатор (простейший).

          Но я гляну, что это за VCV зверь такой и что с его помощью можно делать. Спасибо!
            0

            А вы не посматривали в сторону Bluetooth MIDI? Было бы прикольно, просто и элегантно. А звучал бы ноут или смартфон через любые нормальные колонки.

              0

              MIDI — это отдельная большая тема. Я бы в ней покопался с обычной музыкой сначала, без процедурки, шейдеров и космоса :)


              Тем более, вышел/выходит MIDI 2.0, и, например, его интересно было бы имплементировать на Ардуино как кастомный MIDI контроллер.

                0

                Имплементируйте лучше на ESP32. Там nRF модули есть, может Blutooth потом можно будет юзать для подключения.

          0
          Я делал похожую задачу — генерировал музыку из фрактальных множеств.
          С частотами не заморачивался — сразу делал миди-файлы.
          С гаммами тоже всё было просто — клавиатура с полутонами и запрещет определённых нот, таким образом моделировались различные строи и лады: мажор, минор, пентатоника и т.д. — музыка получается вполне гармоничной.
            0
            Остались ли какие-нибудь исходники или аудио? Было бы интересно послушать и сравнить
              0
              Извиняюсь, ниже ответил, не нашёл как перенести или удалить.
            0
            Сейчас поищу.
            Программа выглядела так:
            image
              0
              Спасибо! Выглядит интересно. Я правильно понимаю, что это ваша или чья-то кастомная программа? Не то, что можно скачать где-нибудь в интернетах
                0
                Я её делал для заказчика ещё на рентакодере.
                Смысл — каждая дорожка проигрывает определённый цвет картинки определённым инструментом и в определённом строе. Одновременно звучат несколько дорожек. Цвет проигрывается слева направо, высота границы этого цвета — проигрываемая величина, скорректированная на выбранный строй.
                Финальным этапом была интерактивная система, позволяющая вмешиваться в процесс с помощью миди-клавиатуры — переключать настройки памяти — строи, аккорды, включать арпеджио и т.д.
                Делал давно, сейчас не могу даже включить проигрывание миди на своём компе ))) Как разберусь — пришлю примеры звука.
              0
              Где-то был сайт показывающий и озвучивающий транзакции в биткойн.
              Вот он
                0

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

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

              Самое читаемое