Аудиоданные в программах: представление отсчётов, каналы, кадры и буферы

Что означает «буфер на 512 сэмплов» и сколько чисел в нём хранится?

Опубликовано 11 мин чтения

В треках:Программист, этап 3Звукорежиссёр, этап 3Диджей, этап 3

Аудиопрограмма получает числа, меняет их и передаёт дальше. В моно (mono)моно (mono) — Аудиоформат с одним каналом. Кадр несжатых монофонических данных содержит один отсчёт. каждому моменту дискретизации (sampling)дискретизация (sampling) — Измерение непрерывного сигнала через равные промежутки времени: вместо функции времени сохраняется последовательность отсчётов. соответствует одно число, в стерео (stereo)стерео (stereo) — Аудиоформат с двумя каналами, обычно левым и правым. Кадр несжатых стереоданных содержит два отсчёта одного момента времени. — два. Если аудиодрайвер передаёт «512 сэмплов», нужно уточнить, считает ли он значения одного канала или все числа вместе. От этого зависят границы массивов, объём памяти и расчёт времени.

Одно число: представление отсчёта

Отсчёт (sample)отсчёт (sample) — Значение сигнала в один момент дискретизации, одно число. В звукозаписи его также называют сэмплом. — значение одного канала в один момент дискретизации. В памяти его можно хранить целым числом или числом с плавающей точкой. Знак, байты и масштаб перевода в float подробно разобраны в статье о целочисленном PCM. Здесь сосредоточимся на том, как значения объединяют в каналы, кадры и буферы.

При представлении отсчётов с плавающей точкой (floating-point sample representation)представление отсчётов с плавающей точкой (floating-point sample representation) — Хранение значений отсчётов в числовом формате со знаком, значащей частью и показателем степени. Номинальную полную шкалу аудио обычно принимают за ±1, но сам формат позволяет хранить значения за её пределами. число хранится через знак, значащую часть и показатель степени (power)степень (power) — При положительном целом показателе m запись aᵐ означает произведение m одинаковых множителей a. Показатель 0 даёт 1 при ненулевом основании, отрицательный целый показатель — обратную величину соответствующей положительной степени.. В C++ на обычных настольных платформах float соответствует 32-битному формату IEEE 754, double — 64-битному. Это позволяет работать с дробными значениями и большим диапазоном.

Для аудио обычно принимают номинальную полную шкалу (full scale)полная шкала (full scale) — Опорная граница уровня выбранного аудиоформата или тракта. Для нормированных аудиоотсчётов обычно принята за ±1; числовой формат с плавающей точкой способен хранить значения за этой границей. от −1 до +1. Ноль соответствует нулевому значению сигнала, знак — направлению отклонения, а модуль — его величине. Эти значения безразмерны: без калибровки звуковой карты число 0,5 не задаёт напряжение в вольтах или давление в паскалях. Последовательность нулей даёт цифровую тишину.

Умножение отсчётов на 0,5 вдвое уменьшает амплитуду (amplitude)амплитуда (amplitude) — Наибольший модуль отклонения колеблющейся величины от равновесного значения. Для синусоиды обозначается A; единицы совпадают с единицами сигнала.. Например, последовательность 0.0f, 0.5f, -0.5f превращается в 0.0f, 0.25f, -0.25f. Суффикс f в C++ обозначает литерал типа float.

У float конечная точность: например, десятичное 0,1 в двоичной записи округляется. Переход с целочисленной записи в float не возвращает информацию, потерянную при квантовании (quantization)квантование (quantization) — Замена значения отсчёта одним из конечного набора представимых уровней. Разность между выбранным уровнем и исходным значением называется ошибкой квантования., а арифметика может добавлять собственное округление (rounding)округление (rounding) — Замена числа ближайшим допустимым значением, например ближайшим целым для round. Выбор при равном расстоянии до двух значений определяется отдельным соглашением. (Smith, глава 4.4).

Каналы и кадры

Аудиоканал (audio channel)аудиоканал (audio channel) — Отдельная последовательность отсчётов аудиосигнала. Моно содержит один канал, стерео — два, обычно левый и правый. — отдельная последовательность отсчётов. Мономоно (mono) — Аудиоформат с одним каналом. Кадр несжатых монофонических данных содержит один отсчёт. содержит один канал, стереостерео (stereo) — Аудиоформат с двумя каналами, обычно левым и правым. Кадр несжатых стереоданных содержит два отсчёта одного момента времени. — два, обычно левый L и правый R. Многоканальные форматы содержат больше каналов; их назначение и порядок задаёт согласованная конфигурация. Два одинаковых канала всё равно хранятся как два канала, хотя различий между левым и правым сигналами в них нет.

Кадр аудиоданных (audio frame)кадр аудиоданных (audio frame) — Набор из одного отсчёта каждого канала в общий момент дискретизации. В моно кадр содержит одно число, в стерео — два. Определение относится к несжатым аудиоданным. объединяет по одному отсчёту каждого канала, относящихся к одному моменту дискретизации. В моно кадр содержит одно число, в стерео — пару (L[n],R[n])(L[n], R[n]) с общим номером nn. Здесь речь о кадрах несжатых аудиоданных; слово «кадр» в описании аудиокодека может обозначать более длинный фрагмент. Такое определение кадра используется, например, в документации ALSA.

Пусть каналов CC, а кадров NN. Тогда общее число отдельных отсчётов SS равно формуле (1).

S=NCS = N C
SS
число отдельных отсчётов во всех каналах;
NN
число кадров, то же число отсчётов в каждом канале;
CC
число каналов. Все три величины — целые счётчики без физических единиц.

Например, три кадра стерео содержат шесть чисел: L0L_0, R0R_0, L1L_1, R1R_1, L2L_2, R2R_2. Номера кадров начинаются с нуля, поэтому последний номер равен N−1N-1.

Частота дискретизации (sample rate)частота дискретизации (sample rate) — Число отсчётов в каждом канале за одну секунду, то же число кадров аудиоданных в секунду. Измеряется в герцах: 44 100 Гц означает 44 100 отсчётов на канал в секунду. fsf_s задаёт число отсчётов в каждом канале за секунду, то есть число кадров за секунду. При 48 000 Гц одна секунда моно содержит 48 000 кадров и 48 000 чисел; секунда стерео — те же 48 000 кадров, но 96 000 чисел. Добавление канала увеличивает объём данных, сохраняя временной шаг между кадрами.

Как каналы расположены в памяти

Одни и те же кадры можно хранить с чередованием каналов (interleaved)чередование каналов (interleaved) — Хранение аудиоданных в одном массиве: подряд идут отсчёты всех каналов одного кадра, затем следующего. Для стерео: L₀, R₀, L₁, R₁.: L₀, R₀, L₁, R₁, L₂, R₂. При раздельном хранении каналов (planar, non-interleaved)раздельное хранение каналов (planar, non-interleaved) — Хранение каждого аудиоканала в отдельном массиве. Отсчёты с одинаковым индексом в разных массивах образуют один кадр. каждый канал имеет свой массив: левый L₀, L₁, L₂, правый R₀, R₁, R₂. Оба варианта встречаются в аудио API (PortAudio, API Overview). Рисунок 1 показывает их для одного фрагмента.

Три кадра стерео: чередование и отдельные массивыСверху один массив: L0, R0, L1, R1, L2, R2; каждая пара заключена в рамку с номером кадра 0, 1 или 2. Снизу два отдельных массива: L0, L1, L2 и R0, R1, R2. Вертикальные направляющие связывают одинаковые индексы в кадры. Значения и порядок времени сохраняются.Чередование каналовОтдельные массивыКадр 0Кадр 1Кадр 2Кадр 0Кадр 1Кадр 2L₀R₀L₁R₁L₂R₂L₀L₁L₂R₀R₁R₂
Рис. 1. Один фрагмент из трёх стереокадров содержит шесть отсчётов при обеих раскладках. Сверху кадр объединяет соседние ячейки, снизу — значения с одинаковым индексом в двух массивах. Левый канал обозначен сплошной рамкой, правый — штриховой; L и R подписаны в каждой ячейке.

В чередующемся массиве индекс отсчёта канала cc в кадре nn равен nC+cnC+c. Номера каналов начинаются с нуля и заканчиваются на C−1C-1. Для стерео с порядком L, R правый отсчёт кадра 2 лежит по индексу 2⋅2+1=52 \cdot 2+1=5. При раздельном хранении сначала выбирают массив канала cc, затем его элемент nn. Кадр остаётся общей временной позицией, даже если его значения находятся в разных областях памяти.

В JUCE AudioBuffer<float> предоставляет отдельный массив каждого канала: указатель на него получают через getReadPointer() или getWritePointer(). Нельзя считать, что правый канал начинается сразу после левого; буфер может ссылаться на внешние массивы.

Блок и буфер

Блок аудиоданных (audio block)блок аудиоданных (audio block) — Фрагмент из нескольких последовательных кадров аудиоданных, обрабатываемый за один вызов функции. — фрагмент из нескольких последовательных кадров, который обрабатывают за один вызов функции. Аудиобуфер (audio buffer)аудиобуфер (audio buffer) — Область памяти для хранения аудиоданных. Может повторно использоваться для очередных блоков; вместимость памяти и длина текущего блока могут различаться. — память, в которой находятся эти данные. Слова часто используют рядом: «получить буфер» обычно означает получить доступ к очередному блоку.

Программа может повторно использовать ту же память: сначала записать в неё кадры 0–479, затем 480–959. Номер 0 внутри нового блока означает его начало; абсолютная позиция в записи уже другая. Между блоками могут сохраняться состояния обработки, например память фильтра или фаза (phase)фаза (phase) — Положение внутри периода колебания, выраженное углом: полный период соответствует 360°, или 2π радиан. Начальная фаза — её значение в момент t = 0. генератора.

Поток обрабатывается порциями, чтобы за один вызов выполнить работу над многими кадрами. При воспроизведении результат должен быть готов вовремя: если новые данные не поступили к моменту, когда они нужны устройству, возникает опустошение буфера (buffer underrun)опустошение буфера (buffer underrun) — Ситуация при воспроизведении, когда устройству уже нужны следующие аудиоданные, а программа ещё не подготовила их. Может вызвать щелчок или прерывание звука., которое может слышаться как щелчок или прерывание. Для записи противоположный случай — переполнение буфера (buffer overrun)переполнение буфера (buffer overrun) — Ситуация при записи, когда новые аудиоданные поступают быстрее, чем программа забирает их из буфера. Приводит к потере данных.: новые данные поступают быстрее, чем программа их забирает (ALSA, Error codes).

Вместимость выделенной памяти и длина текущего блока могут различаться. Если подготовлена память на 512 кадров, а сейчас передано 128, обрабатывают только эти 128. В JUCE getNumSamples() сообщает текущую длину доступного буфера в каждом канале, а не сумму по каналам.

Размер: кадры, время и байты

Под размером буфера (buffer size)размер буфера (buffer size) — Длина или вместимость аудиобуфера в единицах конкретного API. В настройках аудиодрайвера обычно означает число кадров, то же число отсчётов на канал. Объём в байтах зависит также от числа каналов и формата отсчёта. в настройках аудиодрайвера обычно понимают число кадров NN, то же число отсчётов на канал. При чтении API нужно проверить его единицы: некоторые функции ждут число байтов или отдельных значений.

Длительность блока TblockT_\text{block} при известной частоте дискретизации получается по формуле (2).

Tblock=NfsT_\text{block} = \frac{N}{f_s}
TblockT_\text{block}
длительность блока, с;
fsf_s
частота дискретизации, Гц, или кадров в секунду.

При 48 000 Гц блок из 480 кадров занимает 480/48 000=0,01480/48\,000=0{,}01 с, то есть 10 мс, в моно и стерео. Это длительность всей порции: последний отсчёт имеет номер 479 и находится на один период дискретизации (sampling period)период дискретизации (sampling period) — Интервал времени между соседними отсчётами, обратный частоте дискретизации. Измеряется в секундах. раньше начала следующего блока.

Если каждый отсчёт занимает bb байтов, объём самих аудиоданных MM вычисляется по формуле (3).

M=NCbM = N C b
MM
объём данных всех каналов, байт;
bb
число байтов на один отсчёт.

У 32-битного float один отсчёт занимает 4 байта, у 64-битного double — 8. Формула считает только значения отсчётов; объект буфера, указатели и выравнивание памяти могут занимать дополнительное место. Таблица 1 сравнивает одинаковые по времени блоки.

Таблица 1. 480 кадров при 48 000 Гц; 32-битные отсчёты float. Моно содержит один канал, стерео — два. Отсчёты посчитаны по всем каналам; объём удваивается, длительность остаётся 10 мс.
ФорматКадрыОтсчётыБайтыВремя, мс
Моно4804801 92010
Стерео4809603 84010

В AudioBuffer частота дискретизации не хранится. Один и тот же буфер на 480 кадров соответствует 10 мс при 48 кГц и 5 мс при 96 кГц. Само изменение числа в переменной частоты не выполняет передискретизацию: для сохранения длительности и высоты звука нужны новые значения отсчётов.

Примеры на C++ и JUCE

Примеры используют JUCE 9.0.3, последний стабильный релиз на 2 октября 2026 года. Они рассчитаны на C++17 или новее в проекте с подключённым модулем juce_audio_basics. Два первых фрагмента размещаются внутри функции, последний определяет отдельную функцию. Для прямых импортов нужен заголовок <juce_audio_basics/juce_audio_basics.h>, для std::size_t — <cstddef>.

Посчитать длину и объём

Создадим стереобуфер и повторим расчёт таблицы 1. Содержимое нового буфера не инициализировано, поэтому перед чтением заполняем его нулями через clear() (JUCE, AudioBuffer). Выделение памяти выполняется при подготовке, вне функции обработки звука в реальном времени.

C++
juce::AudioBuffer<float> buffer (2, 480);
buffer.clear();

const int channels = buffer.getNumChannels(); // 2
const int frames = buffer.getNumSamples();    // 480 на канал
const std::size_t values =
    static_cast<std::size_t> (frames) * channels; // 960
const std::size_t dataBytes = values * sizeof (float); // 3840

const double sampleRate = 48000.0;
const double durationMs = 1000.0 * frames / sampleRate; // 10

Для моно заменяем первый аргумент конструктора на 1. Получаем 480 кадров, 480 отдельных значений, 1920 байт при 4-байтовом float и те же 10 мс. Приведение к std::size_t сделано до умножения: счётчик объёма вычисляется в типе, предназначенном для размеров объектов. Для любых расчётов времени частота должна быть положительной.

Записать три кадра стерео

Первый аргумент setSample() — индекс канала, второй — индекс кадра внутри него. В этом примере канал 0 принят за левый, канал 1 — за правый; сами номера не назначают каналам роли.

C++
juce::AudioBuffer<float> example (2, 3);
example.clear();

example.setSample (0, 0,  0.0f);  // L0
example.setSample (1, 0,  0.25f); // R0
example.setSample (0, 1,  0.5f);  // L1
example.setSample (1, 1,  0.0f);  // R1
example.setSample (0, 2, -0.5f);  // L2
example.setSample (1, 2, -0.25f); // R2

В левом массиве теперь 0.0f, 0.5f, -0.5f, в правом — 0.25f, 0.0f, -0.25f. getNumSamples() вернёт 3. Пара значений с индексом 2 образует последний кадр; индекс 3 уже выходит за границу.

Обработать каждый отсчёт

Функция ниже уменьшает амплитуду всех каналов вдвое. Предполагается, что вызывающий код разрешает запись во все каналы переданного буфера. Внутренний цикл проходит по кадрам одного канала; внешний выбирает каналы.

C++
void attenuateBlock (juce::AudioBuffer<float>& buffer)
{
    const int frames = buffer.getNumSamples();
    if (frames == 0)
        return;

    for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
    {
        float* samples = buffer.getWritePointer (channel);
        for (int frame = 0; frame < frames; ++frame)
            samples[frame] *= 0.5f;
    }
}

После attenuateBlock (example) левый канал содержит 0.0f, 0.25f, -0.25f, правый — 0.125f, 0.0f, -0.125f. Каналов по-прежнему два, кадров три, а отдельных отсчётов шесть. У моно функция обойдёт один массив. При нулевой длине она сразу завершится.

Указатель samples даёт доступ к данным канала. Его не освобождают вручную и не сохраняют для обработки будущего блока: при смене памяти он может стать недействительным. Для чтения без изменения нужен getReadPointer().

В плагине (plug-in)плагин (plug-in) — Программный модуль эффекта или инструмента, который загружается в DAW или другую программу-хост и обрабатывает переданные ему аудиоблоки. JUCE аудио приходит в аудиоколбэк (audio callback)аудиоколбэк (audio callback) — Функция, которую аудиосистема вызывает для передачи очередного блока входных данных и получения выходных. Обработчик должен завершиться до срока, когда нужны данные. processBlock() класса AudioProcessor. Он получает входные данные и должен записать выходные. Число кадров может меняться от вызова к вызову, включая нулевые блоки, поэтому длину берут из текущего буфера. Каналы для записи выбирают по конфигурации входов и выходов: переданный буфер может содержать дополнительные входные каналы, менять которые нельзя. Функцию из примера применяют к буферу, где это условие уже проверено (JUCE, processBlock).

Размер блока и задержка

Меньший блок обычно сокращает ожидание следующей порции данных, но требует чаще вызывать обработчик. Например, при 48 000 Гц 128 кадров занимают примерно 2,67 мс, 512 — 10,67 мс. При фиксированном размере это соответственно 375 и примерно 93,75 блока в секунду. Вызовы имеют свои накладные расходы, а времени на подготовку очередного результата при малом блоке меньше.

Длительность блока не равна полной задержке (latency)задержка (latency) — Время прохождения сигнала через тракт от входа до выхода. Включает буферизацию, драйвер, преобразователи и алгоритмы; длительность одного блока не равна полной задержке. от входа до выхода. Задержка включает очереди входа и выхода, работу драйвера, преобразователей и алгоритмов; некоторые алгоритмы специально накапливают данные. Поэтому значение 10,67 мс для 512 кадров описывает одну порцию, а не гарантированное время прохождения звука через программу. PortAudio, например, отдельно сообщает фактические входную и выходную задержки потока (Retrieving Stream Information).

В обработчике реального времени избегают операций с непредсказуемым ожиданием: выделения памяти, чтения файлов и блокировок. Буферы для собственной работы готовят заранее. Функция attenuateBlock() только проходит по уже имеющимся данным; создание AudioBuffer в первых двух примерах относится к подготовке, а не к каждому аудиовызову (PortAudio, Callback I/O Method).

Самопроверка

В стереоблоке 512 кадров при 48 000 Гц; каждый отсчёт — 32-битный float. Сколько чисел, байтов и миллисекунд в блоке?

Чисел 512⋅2=1024512 \cdot 2=1024, данных 1024⋅4=40961024 \cdot 4=4096 байт. Длительность 512/48 000≈0,01067512/48\,000 \approx 0{,}01067 с, или 10,67 мс. Это объём отсчётов без служебной памяти и длительность блока без остальных составляющих задержки.

Моно и стерео имеют по 480 кадров при 48 кГц. Что изменится, если вместо float хранить double?

Число кадров и длительность останутся прежними: 480 и 10 мс. При 8-байтовом double объём аудиоданных удвоится: моно — 3840 байт, стерео — 7680 байт. Сам переход на double не меняет временной шаг и не добавляет каналов.

AudioBuffer содержит два канала по три отсчёта. Верно ли обходить каждый канал циклом до getNumSamples() * getNumChannels()?

Нет: в каждом массиве допустимы только индексы 0, 1 и 2. Произведение даёт суммарное число значений в двух массивах, а не длину одного. По каждому каналу проходят до getNumSamples(), не включая эту границу.

Источники

Связи

Связи списком

Ссылаются сюда

Открыть в общем графе

Поиск

Ищем по названиям, тексту статей и английским терминам. Например: «частота дискретизации», «Nyquist», «аналоговый сигнал».