Картинки со звуком

Никита Дубко, HR-Tech@Яндекс

Картинки со звуком

Никита Дубко, HR-Tech@Яндекс

Кто я?

Слышу глазами 👁

Я вижу, где смеётся Маша.
Вадим Макеев

Пятиминутка
истории

✏️

Neume
Золотые хиты Средневековья

🪕 Гвидо из Ареццо,
XI век

1877

Charles Cro
Phonograph
Emile Berliner
Vinyl Put Under Microscope In Slow Motion Video

Пластинки изменили
мировую экономику 📈

1904

Рисованный звук: из прошлого в будущее
Синтезированный «бумажный звук»
Евгений Мурзин показывает свое детище. ВДНХ. 1962

Оптическая реализация алгоритма преобразования Фурье 🤯

Что такое звук?

Дисклеймер

Автор не имеет докторской степени по физике звука. Дальнейшие объяснения основаны на собственном опыте и активном гуглении. Если вы считаете, что автор не прав, не стесняйтесь вступать в дискуссии после доклада. Если это кто-то читает, то вот вам интересный факт: шум моря, который мы слышим через морскую раковину, на самом всего лишь звук крови, протекающей по нашим сосудам.

Звук

〰 Упругие волны
механических колебаний

Волна

16–20 000 Гц

Инфразвук < 16 Гц

Ультразвук > 20 000 Гц

Звуковое давление

1933

📖 Теорема Котельникова
(Найквиста-Шеннона)

Любую функцию F(t), состоящую из частот от 0 до f1, можно непрерывно передавать с любой точностью при помощи чисел, следующих друг за другом через 1/(2f1) секунд

частота дискретизации

44,1 кГц

Не пустой звук

разрядность квантования

16 бит

Не пустой звук

16 бит

65 536

PCM
Pulse Code Modulation

ИКМ
Импульсно-Кодовая Модуляция

АЦП
Аналогово-Цифровой Преобразователь

Теория звука. Что нужно знать о звуке, чтобы с ним работать

Время,
частота,
амплитуда

Время X,
частота Y,
амплитуда Z

Спектрограмма 🎨

Web Audio API

Web Audio API, Boris Smus
Web Audio API
Web Audio API, Михаил Силаев

Не про файлы ☐

Про сигналы 〰

Web Audio API, Boris Smus

Доклад не про это 🤷‍♂️

Демо 1.
Осцилограмма

const audioCtx = new (AudioContext || webkitAudioContext)();

const buffer = await audioCtx.decodeAudioData(file);

const source = audioCtx.createBufferSource();
source.buffer = buffer;

source.connect(audioCtx.destination);

source.start(0);
buffer: AudioBuffer
    duration: 91.53199546485261
    length: 4036561
    numberOfChannels: 2
buffer.getChannelData(0);

Float32Array(6506070) [0.0014954069629311562, 0.0019531846046447754,
0.0016785180196166039, 0.0018616290763020515, 0.0017700735479593277,
0.0017700735479593277, 0.0018005920574069023, 0.0017700735479593277,
0.0017700735479593277, 0.001831110566854477, 0.001831110566854477,
0.0019226660951972008, 0.0020142216235399246, 0.0020447401329874992,
0.0021057771518826485, 0.002227851189672947, 0.0023194067180156708,
0.002471999265253544, 0.002533036284148693, 0.002624591812491417,
0.0027161473408341408, 0.0027161473408341408, 0.00277718435972929,
0.00277718435972929, 0.0028382213786244392, 0.00277718435972929,
0.0028077028691768646, 0.0028382213786244392, 0.0028382213786244392,
0.0028077028691768646, 0.00277718435972929, 0.0028382213786244392,
0.0028077028691768646, 0.002868739888072014, 0.0028077028691768646,
0.0028077028691768646, 0.0028077028691768646, 0.0028382213786244392, ...
        
const dotsCount = buffer.length;
const dotsInPixel = Math.floor(dotsCount / canvas.width);
const values = [];

for (let i = 0; i < canvas.width; i++) {
    let sum = 0;
    for (let j = i * dotsInPixel; j < (i + 1) * dotsInPixel; j++) {
        sum += Math.pow(buffer[j], 2);
    }
    values.push(Math.sqrt(sum));
}
const maxValue = Math.ceil(Math.max(...values));
const yCenter = Math.floor(h / 2);
const h = canvas.height;

for (let i = 0; i < canvas.width; i++) {
    const currentValue = Math.round(
        (values[i] / maxValue) * h * 0.8
    );
    canvas.fillRect(
        i,
        y + yCenter - currentValue / 2,
        1,
        currentValue
    );
}

Demo

Демо 2.
Спектрограмма

const offlineCtx = new OfflineAudioContext(
    audioBuffer.numberOfChannels,
    audioBuffer.length,
    audioBuffer.sampleRate
);

const offlineSource = offlineCtx.createBufferSource();
offlineSource.buffer = audioBuffer;
offlineSource.channelCount = audioBuffer.numberOfChannels;

Не про файлы ☐

OfflineAudioContext 💡

const config = {
    fftResolution: 4096,
    smoothingTimeConstant: 0.02,
    processorBufferSize: 2048,
};

const analyzer = offlineCtx.createAnalyser();
analyzer.fftSize = config.fftResolution;
analyzer.smoothingTimeConstant = config.smoothingTimeConstant;
AnalyserNode

FFT?

Быстрое
преобразование
Фурье

Применение преобразования Фурье в цифровой обработке звука
Применение преобразования Фурье в цифровой обработке звука
offlineSource.connect(analyzer);

offlineCtx.createScriptProcessor =
    offlineCtx.createScriptProcessor || offlineCtx.createJavaScriptNode;

const processor = offlineCtx.createScriptProcessor(
    config.processorBufferSize,
    1,
    1
);

const channelFFtDataBuffer = new Uint8Array(
    (audioBuffer.length / config.processorBufferSize) *
        (config.fftResolution / 2)
);
let offset = 0;
processor.onaudioprocess = (e) => {
    const freqData = new Uint8Array(
        channelFFtDataBuffer.buffer,
        offset,
        analyzer.frequencyBinCount
    );
    analyzer.getByteFrequencyData(freqData);
    offset += analyzer.frequencyBinCount;
};

offlineSource.connect(processor);
processor.connect(offlineCtx.destination);
offlineSource.start(0);

getByteFrequencyData

const stride = generalAnalyzer.frequencyBinCount;

for (let i = 0; i < w; i++) {
    const startIndex = i * ticksPerLine * stride;

    for (let j = 0; j < stride; j++) {
        const index = startIndex + j;
        const db = data.channel[index] / 255;
        const hColor = Math.round((db * 120 + 280) % 360);
        const lColor = 10 + 70 * db + '%';
        spectreCanvasCtx.fillStyle =
            `hsl(${hColor}, 100%, ${lColor})`;
        spectreCanvasCtx.fillRect(
            i * cellWidth, h - cellHeight * j,
            cellWidth, cellHeight
        );
    }
}

Demo

Демо 3.
Spectrodrawing

Спектрограмма

Звук

Generate Sound from Image Using Inverse Spectrogram

Потери ☹️

Ну и ладно 😁

const imgData = ctx.getImageData(0, 0, w, h).data;
const durationSeconds = 10;
const sampleRate = 44100;
const channelsCount = 1;
const samplesCount = Math.round(sampleRate * durationSeconds);
const maxPossibleFrequency = 20000; // Hz
const coeff = maxPossibleFrequency / canvas.height;
const samplesPerLineX = Math.floor(samplesCount / canvas.width);
const samples = [];

let maxFreq = -Infinity;
let yFactor = 2;
for (let i = 0; i < samplesCount; i++) {
    const x = Math.floor(i / samplesPerLineX);
    let reX = 0;

    for (let y = 0; y < h; y += yFactor) {
        const j = (y * w + x) * 4;
        const sum = imgData[j] + imgData[j + 1] + imgData[j + 2];
        const volume = Math.pow((sum / (255 * 3)) * 100, 2);
        const freq = Math.round(coeff * (h - y + 1));
        reX += Math.floor(
            volume * Math.cos((freq * 2 * Math.PI * i) / sampleRate)
        );
    }

    samples.push(reX);
}
volume * Math.cos((freq * 2 * Math.PI * i) / sampleRate)

4 часа дебага спустя...

Demo

img-encode
Virtual ANS

Зачем? 😶

Визуальный поиск
паттернов 👀

Распознавание речи по картинке

Стеганограмма 🥷

Подпись файлов ✍️

Демо 4.
Музыка в картинку

амплитуда

число

картинка

пиксели

пиксель

R G B

R G B

числа

числа

числа

const buffers = [];
const buffersCount = source.buffer.numberOfChannels;

for (let i = 0; i < buffersCount; i++) {
    buffers.push(source.buffer.getChannelData(i));
}

const size = Math.ceil(Math.sqrt(buffers[0].length / 3));
const cellSize = 1;
const h = (canvas.height = size * cellSize * buffersCount);
const w = (canvas.width = size * cellSize);
for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
        const index = (y * size + x) * 3;
        const r = Math.floor((255 * (buffer[index] + 1)) / 2);
        const g = Math.floor((255 * (buffer[index + 1] + 1)) / 2);
        const b = Math.floor((255 * (buffer[index + 2] + 1)) / 2);
        const fillStyle = `rgb(${r}, ${g}, ${b})`;
        canvasCtx.fillStyle = fillStyle;
        canvasCtx.fillRect(
            x * cellSize,
            y * cellSize + i * w,
            cellSize,
            cellSize
        );
    }
}

Demo

Демо 5.
Картинка в музыку

const data = ctx.getImageData(0, 0, w, h).data;

const buffersCount = Math.round(h / w);
const bufferLength = (data.length * 3) / 4 / buffersCount;
const audioBuffer = new AudioBuffer({
    length: bufferLength,
    sampleRate: 44100,
    numberOfChannels: buffersCount,
});
for (let j = 0; j < buffersCount; j++) {
    const pixels = [];
    const dotsCount = data.length / 4 / buffersCount;
    for (let i = 0; i < dotsCount; i++) {
        const index = j * dotsCount * 4 + i * 4;
        const r = (data[index] * 2) / 255 - 1;
        const g = (data[index + 1] * 2) / 255 - 1;
        const b = (data[index + 2] * 2) / 255 - 1;
        pixels.push(r);
        pixels.push(g);
        pixels.push(b);
    }
    const buffer = Float32Array.from(pixels);
    audioBuffer.copyToChannel(buffer, j, 0);
}
const objectUrl = URL.createObjectURL(
    bufferToWave(abuffer, total_samples)
);

audio.src = objectUrl;
audio.title = filename + '.wav';

function bufferToWave(abuffer, len) {
    // ...
}

Demo

Squoosh 😈

Физика —
это интересно 🙃

Have Fun!

mefody.dev/talks/img-with-sound/
@dark_mefody
n.a.dubko@gmail.com QR-код со ссылкой на доклад