В электронной музыке есть интересное направление — музыка для осциллоскопов, которая рисует интересные картинки, если выход аудиокарты подключить к осциллоскопу в режиме XY.
К примеру, Youscope, Oscillofun и Khr?ng.

Все красивые видео, генерируемые такой музыкой созданы с помощью записи работы настоящего осциллоскопа на видеокамеру. Когда я поискал в сети эмуляторы осциллоскопов, мне не удалось найти такие, которые рисуют мягкие линии, как в настоящем осциллоскопе.

Это сподвигло меня на создацие своего эмулятора осциллоскопа на WebGL: woscope.

В этом посте я расскажу о том как именно происходит рисование линий осциллоскопа в woscope.

Постановка задачи


Есть стерео аудио файл. Каждый сэмпл интерпретируется как координаты точки на плоскости.
Мы хотим получить линию, которая выглядит как линия на экране осциллоскопа, когда тот подключен в режиме XY.

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

image

Яркость всех сегментов будет собираться с помощью gl.blendFunc(gl.SRC_ALPHA, gl.ONE);.

Генерация вершин


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

image

Две первых точки находятся ближе к началу сегмента, и две последних — к концу сегмента.
Четные точки смещены «налево» от сегмента, а нечетные — «направо».

Такое преобразование довольно просто написать в vertex shader:

#define EPS 1E-6
uniform float uInvert;
uniform float uSize;
attribute vec2 aStart, aEnd;
attribute float aIdx;
// uvl.xy is used later in fragment shader
varying vec4 uvl;
varying float vLen;
void main () {
    float tang;
    vec2 current;
    // All points in quad contain the same data:
    // segment start point and segment end point.
    // We determine point position using its index.
    float idx = mod(aIdx,4.0);

    // `dir` vector is storing the normalized difference
    // between end and start
    vec2 dir = aEnd-aStart;
    uvl.z = length(dir);

    if (uvl.z > EPS) {
        dir = dir / uvl.z;
    } else {
    // If the segment is too short, just draw a square
        dir = vec2(1.0, 0.0);
    }
    // norm stores direction normal to the segment difference
    vec2 norm = vec2(-dir.y, dir.x);

    // `tang` corresponds to shift "forward" or "backward"
    if (idx >= 2.0) {
        current = aEnd;
        tang = 1.0;
        uvl.x = -uSize;
    } else {
        current = aStart;
        tang = -1.0;
        uvl.x = uvl.z + uSize;
    }
    // `side` corresponds to shift to the "right" or "left"
    float side = (mod(idx, 2.0)-0.5)*2.0;
    uvl.y = side * uSize;
    uvl.w = floor(aIdx / 4.0 + 0.5);

    gl_Position = vec4((current+(tang*dir+norm*side)*uSize)*uInvert,0.0,1.0);
}


Рассчитываем яркость в точке


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

В моей модели, интенсивность пучка описана нормальным распределением, что довольно распространено в реальном мире.

Где ? — разброс пучка.

Для того чтобы рассчитать общую интенсивность в точке, я интегрирую интенсивность пучка по времени, когда пучек движется от начала к концу сегмента.


image


Если использовать систему отсчета в которой начало сегмента имеет координаты (0,0) а конец — (length,0), можно записать distance(t) как:


Теперь,


Поскольку является константой, можно вынести за знак интегрирования:


Немного упростим интеграл, заменив t на u/l:


Интеграл нормального распределения — функция ошибок.


Наконец,


Зная аппроксимацию функции ошибок, несложно записать эту формулу в fragment shader'е

Fragment shader


Параметр uvl, сгенерированный в vertex shader содержит координаты точки в системе отсчета где начало сегмента имеет координаты (0,0) а конец — (length,0).
Этот параметр будет линейно интерполироваться между вершинами треугольников, что нам и нужно.

#define EPS 1E-6
#define TAU 6.283185307179586
#define TAUR 2.5066282746310002
#define SQRT2 1.4142135623730951
uniform float uSize;
uniform float uIntensity;
precision highp float;
varying vec4 uvl;

float gaussian(float x, float sigma) {
    return exp(-(x * x) / (2.0 * sigma * sigma)) / (TAUR * sigma);
}

float erf(float x) {
    float s = sign(x), a = abs(x);
    x = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
    x *= x;
    return s - s / (x * x);
}

void main (void)
{
    float len = uvl.z;
    vec2 xy = uvl.xy;
    float alpha;

    float sigma = uSize/4.0;
    if (len < EPS) {
    // If the beam segment is too short, just calculate intensity at the position.
        alpha = exp(-pow(length(xy),2.0)/(2.0*sigma*sigma))/2.0/sqrt(uSize);
    } else {
    // Otherwise, use analytical integral for accumulated intensity.
        alpha = erf(xy.x/SQRT2/sigma) - erf((xy.x-len)/SQRT2/sigma);
        alpha *= exp(-xy.y*xy.y/(2.0*sigma*sigma))/2.0/len*uSize;
    }

    float afterglow = smoothstep(0.0, 0.33, uvl.w/2048.0);
    alpha *= afterglow * uIntensity;
    gl_FragColor = vec4(1./32., 1.0, 1./32., alpha);
}


Что можно улучшить


  • В этом эмуляторе точка движется по прямой линии в каждом сегменте, что иногда приводит к видимо ломанным линиям, чтобы этого избежать можно использовать интерполяцию sinc, увеличив число семплов в несколько раз
  • Насыщение пикселов происходит слишком быстро, этого можно было бы избежать, используя Float-текстуры, но есть проблемы с их поддержкой в WebGL. На текущий момент в луче есть маленькие значение красного и синего цвета, что «переполняет» значение в белые пикселы
  • Не учитывается гамма-коррекция монитора
  • Нет блума, но он может быть и не нужен, учитывая метод генерации линий
  • Сделать нативную программу с этим функционалом?


Итоги


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

Код шейдеров отдается в общественное достояние. Полный код woscope доступен на github

Комментарии (22)


  1. seokirill
    14.10.2015 12:57

    Плюсую. Но я не понял, разве в статье приведён js код оО?!


    1. m1el
      14.10.2015 13:01

      Технически — да. «gl.blendFunc(gl.SRC_ALPHA, gl.ONE);»

      Но я включил JS в хабы потому что woscope написан с использованием JS. Интересующиеся могут почитать на гитхабе.
      Да и WebGL использовать без JS не получится :)
      Код на JS не включен в статью потому что он, в основном, является бойлерплейтом для загрузки данных и работы с WebGL.


    1. degorov
      14.10.2015 13:03
      +2

      Это GLSL — язык для написания шейдеров. На С похож, но без указателей.


  1. deniskreshikhin
    14.10.2015 13:25

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


    1. m1el
      14.10.2015 13:33
      +1

      >эти сегменты накладываются и получаются светящиеся точки в местах сцепления.
      Как раз, нет. Благодаря тому, что я расчитываю интеграл интенсивности, два смежных отрезка будут выглядеть хорошо.
      Именно эта «проблема» с яркими точками на соединении отрезков отлично решается с помощью математики :)


      1. deniskreshikhin
        14.10.2015 14:00

        Какова тогда природа этих точек?
        Они выглядят немного странно, которые образуют «пилу».
        Может конечно это связано с музыкой. Просто интересно.




        1. m1el
          14.10.2015 14:03

          Конкретно эти точки так и должны выглядеть — сигнал «замирает» на месте.
          i.imgur.com/rg7x1dI.png


          1. deniskreshikhin
            14.10.2015 14:18

            Ок, вопрос снят)


  1. lockywolf
    14.10.2015 13:36
    +7

    Может, всё-таки «осциллограф»? Как-то это слово распространённее в русском языке.


    1. m1el
      14.10.2015 14:11
      -3

      Для вас можно и «осциллограф», но в статье будет «осциллоскоп» :)


      1. mickvav
        15.10.2015 09:39
        +1

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


        1. m1el
          15.10.2015 09:43
          +1

          Ну, я бы начал с того, что «осциллоскоп» и «осциллограф» — равносильно слова позаимствованные.
          Оба слова есть в русском словаре. Не понимаю, в чем обвинение «кальки» с английского.


          1. mickvav
            15.10.2015 15:46
            +3

            Ну, в английском обычно этот прибор называют oscilloscope. В русском — осциллограф. Единственное исключение, которое я смог нагуглить — осциллоскоп САГА производства вильнюсского завода (в литовском он osciloscopas) и пару скопированных упоминаний в словарях ( прибор для наблюдения за процессами в электрических цепях, представляющий собою упрощённый осциллограф). Ключевое слово «собою» выдаёт, что это определение списано с какого-то довольно старого словаря — так не говорят и не пишут лет 30-40 уже, наверное. Так что в русском языке норма, всё-таки — осциллограф.


      1. gorbln
        16.10.2015 12:50

        Не по-джентльменски хамить на первое сообщение.
        Человек сказал правильно — по-русски будет «осциллограф». А что такое осциллоскоп — вообще неведомо.


        1. m1el
          16.10.2015 12:54
          +1

          Я не вижу хамства в своем комментарии и вижу слово «осциллоскоп» в словарях.


          1. gorbln
            16.10.2015 13:15

            Ну как бы фраза «для вас можно и...» автоматически причисляет человека к какому-то меньшинству, причём, фраза построена так, что вроде как большинство образовано — а человек — нет.

            Это я очень мягко попытался донести смысл своей мысли
            «у всех осциллоскоп, а ты, быдло, называй как хочешь»


            1. m1el
              16.10.2015 13:18
              +1

              Хорошо. Я понял почему мой комментарий можно было принять как оскорбление.
              Он не подразумервался как оскорбление.


  1. silvansky
    14.10.2015 15:18
    +1

    Ещё бы теперь «разоблачение»: как делать такую музыку? =)


    1. barkalov
      15.10.2015 00:33

      В левом канале X, в правом Y. Что тут разоблачать?


  1. kozyabka
    14.10.2015 16:37

    «если выход аудиокарты подключить к осциллоскопу в режиме XY» — то очевидно, что будет вовсе не то что рисуется на приведенных в пример видео с ютуба.


    1. m1el
      14.10.2015 16:46
      +2

      А что же, по-вашему, будет?
      Не считая youscope, в котором трек для музыки и для осциллоскопа — разный.
      Предлагаю скачать треки (которые используются в демо или oscillofun.flac, alpha_molecule.wav), подключить к осциллоскопу в режиме XY, два канала аудио и посмотреть что будет.


  1. kozyabka
    14.10.2015 16:55

    признаюсь, дальше youscope видео не смотрел. Благодарю, за разъяснения.