Три байта 00 00 80 могут обозначать отсчёт (sample)отсчёт (sample) — Значение сигнала в один момент дискретизации, одно число. В звукозаписи его также называют сэмплом. −8388608. Если собрать их как положительное число и сразу поделить на масштаб, получится +1 вместо −1. Чтобы прочитать звук правильно, нужно знать разрядность (bit depth)разрядность (bit depth) — Число бит для представления одного квантованного отсчёта. B бит дают 2ᴮ кодов; при равномерном квантовании шаг зависит также от полного диапазона сигнала., способ кодирования знака и порядок байтов. При записи добавляются ещё два решения: как округлять и что делать с выходом за диапазон.
Здесь разбираем отдельный отсчёт линейной импульсно-кодовой модуляции (pulse-code modulation, PCM)импульсно-кодовая модуляция (pulse-code modulation, PCM) — Представление сигнала последовательностью квантованных отсчётов, взятых через равные промежутки времени. В WAV код формата 1 означает PCM с целыми отсчётами.. В примерах используется соглашение обычного PCM в WAV (WAVE file)WAV-файл (WAVE file) — Звуковой файл со структурой RIFF и типом формы WAVE. Чанк fmt описывает параметры звука, чанк data содержит отсчёты; обычно это несжатый PCM.: 8 бит без знака со смещением, 16, 24 и 32 бита со знаком в дополнительном коде (two’s complement)дополнительный код (two’s complement) — Представление знакового целого в N битах: при установленном старшем бите беззнаковое значение этих битов уменьшают на 2ᴺ. Диапазон — от −2ᴺ⁻¹ до 2ᴺ⁻¹−1; у нуля одна запись., многобайтовые числа — little-endianпорядок байтов little-endian (little-endian) — Запись многобайтового числа, при которой младший байт идёт первым. Число 44 100 = 0xAC44 в четырёх байтах хранится как 44 AC 00 00.. Это не универсальное правило для всех аудио API. Заголовок файла и раскладка каналов (channel layout)раскладка каналов (channel layout) — Назначения и порядок аудиоканалов, например левый, центральный, правый и каналы окружения в многоканальной записи. разобраны в статье об устройстве WAV; здесь предполагаем, что байты одного отсчёта уже найдены.
Биты и диапазон целого числа
Один бит принимает значение 0 или 1. В битах есть различных комбинаций: у 8 бит их 256, у 16 — 65536. У целого без знака (unsigned integer) эти комбинации означают числа от 0 до . Старшая позиция имеет наибольший вес: у байта веса справа налево равны 1, 2, 4, 8, 16, 32, 64 и 128. Поэтому 00000101 означает . Двоичная и шестнадцатеричная запись подробно объяснены в математической справке.
Целое со знаком (signed integer) должно вместить отрицательные значения в тот же набор комбинаций. При дополнительном кодедополнительный код (two’s complement) — Представление знакового целого в N битах: при установленном старшем бите беззнаковое значение этих битов уменьшают на 2ᴺ. Диапазон — от −2ᴺ⁻¹ до 2ᴺ⁻¹−1; у нуля одна запись. диапазон равен (таблица 1). Все границы включены.
| Биты | Без знака | Со знаком |
|---|---|---|
| 8 | 0…255 | −128…127 |
| 16 | 0…65535 | −32768…32767 |
| 24 | 0…16777215 | −8388608…8388607 |
| 32 | 0…4294967295 | −2147483648…2147483647 |
Разрядностьразрядность (bit depth) — Число бит для представления одного квантованного отсчёта. B бит дают 2ᴮ кодов; при равномерном квантовании шаг зависит также от полного диапазона сигнала. задаёт число битов одного отсчёта одного канала. Она сама по себе не задаёт напряжение и не сообщает, знаковое ли это число. Для декодирования нужны все свойства формата; частота дискретизации (sample rate)частота дискретизации (sample rate) — Число отсчётов в каждом канале за одну секунду, то же число кадров аудиоданных в секунду. Измеряется в герцах: 44 100 Гц означает 44 100 отсчётов на канал в секунду. при этом определяет время между отсчётами, а не диапазон их значений.
Дополнительный код и знаковый бит
У знакового числа старший бит — знаковый бит (sign bit)знаковый бит (sign bit) — Старший бит целого в дополнительном коде: 0 означает неотрицательное значение, 1 — отрицательное. Его положение зависит от разрядности исходного числа.. Если он равен 0, число неотрицательно; если 1 — отрицательно. Но оставшиеся биты не являются модулем отрицательного числа. Например, 11111111 в 8-битном дополнительном коде — −1, а не −127.
Пусть — значение всех битов, прочитанных без знака. Тогда при старшем бите 0 знаковое значение равно , а при старшем бите 1 — . Для 11111111 получается , для 10000000 — . То же правило для 16 бит даёт 0x8000 → и 0xFFFF → −1.
Представление числа −5 в восьми битах можно получить из +5: 00000101 → инвертировать биты → 11111010 → прибавить 1 → 11111011. Проверка: . Эти операции выполняются в выбранных восьми битах; лишний перенос за их пределы отбрасывается.
Почему используют такой код? Двоичное сложение работает и для отрицательных значений. В восьми битах 11111111 + 00000001 даёт 00000000 с лишним переносом, то есть −1 + 1 = 0. Ноль имеет одну запись. Из 256 комбинаций 128 отведены отрицательным числам, остальные — нулю и 127 положительным. Отсюда асимметрия: отрицательная граница на единицу дальше от нуля. Это свойство представления; переполнение знакового целого в C++ не следует считать разрешённым способом такой арифметики.
8-битный WAV: тишина равна 128
Обычный 8-битный PCM в WAV использует двоичный код со смещением (offset binary)двоичный код со смещением (offset binary) — Кодирование центрированного значения неотрицательным целым с добавленным смещением. В 8-битном WAV PCM смещение равно 128: байт 128 означает ноль сигнала.: байт читается без знака, а центрированный отсчёт равен . Поэтому 00 означает −128, 80 — 0, FF — +127. Здесь старший бит не является знаком: байт 80 обозначает тишину. Соглашение формата подтверждено в описании WAVE.
Нормированное значение получаем как : отрицательная граница — −1, положительная — . Последовательность байтов 80 80 80… — цифровая тишина для этого формата. Если заполнить её нулями, получим постоянный отрицательный уровень полной шкалы (full scale)полная шкала (full scale) — Опорная граница уровня выбранного аудиоформата или тракта. Для нормированных аудиоотсчётов обычно принята за ±1; числовой формат с плавающей точкой способен хранить значения за этой границей..
Сначала вычитают 128 в достаточно широком знаковом типе. Если сохранить результат в беззнаковом байте, отрицательные значения снова превратятся в положительные коды.
16 бит: собрать два байта
При порядке байтов little-endianпорядок байтов little-endian (little-endian) — Запись многобайтового числа, при которой младший байт идёт первым. Число 44 100 = 0xAC44 в четырёх байтах хранится как 44 AC 00 00. младший байт идёт первым. Два байта 00 FF образуют беззнаковое , то есть 0xFF00. Знаковый бит установлен, поэтому отсчёт .
Порядок байтов и знак решают разные задачи: little-endian сообщает порядок частей числа, дополнительный код — смысл собранных битов. Если ошибиться только порядком и прочитать 00 FF как 0x00FF, получится +255.
Следующие самостоятельные функции рассчитаны на C++17 или новее, восьмибитный байт и платформу с типами std::uint8_t, std::uint32_t, std::int32_t. Указатель p должен указывать минимум на два доступных байта. Сборка идёт через беззнаковый тип, затем знак учитывается вычитанием: все промежуточные значения помещаются в типы.
#include <cstdint>
std::int32_t readI16LE (const std::uint8_t* p)
{
const std::uint32_t u = std::uint32_t (p[0])
| (std::uint32_t (p[1]) << 8);
return u < 0x8000u ? std::int32_t (u)
: std::int32_t (u) - 65536;
}Оператор << 8 переносит второй байт на восемь позиций влево, | объединяет непересекающиеся позиции. Их смысл с примером разобран в математической справке. Приведение к широкому беззнаковому типу выполнено до сдвига. Чтение через reinterpret_cast к указателю на int16_t не заменяет этот код: нужно учитывать выравнивание, правила доступа к памяти и порядок байтов машины.
24 бита: расширение знака
Три байта собираются аналогично: . Стандартного типа int24_t в C++ нет; значение можно хранить в int32_t, но его нужно прочитать как 24-битное. Для FF FF FF беззнаковое , а отсчёт .
Расширение знака (sign extension)расширение знака (sign extension) — Заполнение новых старших битов копиями знакового бита при увеличении разрядности целого в дополнительном коде. Сохраняет числовое значение, например 24-битное −1 становится 32-битным −1. заполняет новые старшие биты копиями исходного знакового бита. Число −1 переходит из FF FF FF в FF FF FF FF; минимум −8388608 — из 80 00 00 в FF 80 00 00. Здесь числа показаны старшими байтами слева. Дополнение нулями вместо единиц изменило бы их значения на +16777215 и +8388608.
Следующая функция вычисляет правильное знаковое значение вычитанием . Результат в int32_t уже имеет нужное расширенное представление. У p должны быть три доступных байта.
std::int32_t readI24LE (const std::uint8_t* p)
{
const std::uint32_t u = std::uint32_t (p[0])
| (std::uint32_t (p[1]) << 8)
| (std::uint32_t (p[2]) << 16);
return (u & 0x800000u) == 0 ? std::int32_t (u)
: std::int32_t (u) - 16777216;
}Маска 0x800000 оставляет только бит 23. Если результат & ненулевой, этот бит установлен. Для проверки знака старшего байта то же условие записывается (p[2] & 0x80u) != 0. Скобки нужны: сравнение и побитовая операция имеют разные приоритеты.
Вариант через сдвиги в C++20
Часто знак расширяют сдвигами: сначала перемещают исходный знаковый бит в позицию 31, затем возвращают число вправо арифметическим сдвигом. Ниже — вариант для C++20 с <bit>: u уже собран как в функции выше и не превышает 0xFFFFFF.
#include <bit>
std::int32_t signExtend24 (std::uint32_t u)
{
const auto shifted = std::bit_cast<std::int32_t> (u << 8);
return shifted >> 8;
}Левый сдвиг выполнен над беззнаковым числом; bit_cast сохраняет биты при смене типа. В C++20 правый сдвиг отрицательного знакового числа расширяет знак. В C++17 его результат зависел от реализации; поэтому первый вариант с вычитанием удобнее для переносимого примера. Условия сдвига описаны в рабочем тексте стандарта C++.
Посмотреть число по байтам
В рисунке 1 введите целое в диапазоне 24 бит или три hex-байта в порядке файла. Нажмите соответствующую кнопку или Enter в поле. Попробуйте 00 00 80, FF FF 7F и FF FF FF: меняются биты, знак, расширенный код и float. При неверном вводе сохраняется последний корректный разбор; «Сбросить» возвращает −256.
- Байты в файле (LE)
00 FF FF- 24 бита: старшие слева
11111111 11111111 00000000- Знаковый бит
- 1 — отрицательное
- Чтение со знаком
- 16776960 − 16777216 = -256
- Расширение до 32 бит
FF FF FF 00- Рабочий float
- -256 / 8388608 = -0.000030517578125
Подчёркнутый байт добавлен при расширении знака. Байты в строке расширения показаны старшими слева, а в файле младший идёт первым.
Для ввода нужен JavaScript. Показан отсчёт −256.
| Формат | Байты | q | x |
|---|---|---|---|
| 8 бит без знака | 00 | −128 | −1 |
| 8 бит без знака | 7F | −1 | −0,0078125 |
| 8 бит без знака | 80 | 0 | 0 |
| 8 бит без знака | FF | 127 | 0,9921875 |
| 16 бит со знаком | 00 80 | −32768 | −1 |
| 16 бит со знаком | FF FF | −1 | −0,000030517578125 |
| 16 бит со знаком | 00 00 | 0 | 0 |
| 16 бит со знаком | FF 7F | 32767 | 0,999969482421875 |
| 24 бита со знаком | 00 00 80 | −8388608 | −1 |
| 24 бита со знаком | FF FF FF | −1 | −0,00000011920928955078125 |
| 24 бита со знаком | 00 00 00 | 0 | 0 |
| 24 бита со знаком | FF FF 7F | 8388607 | 0,99999988079071044921875 |
Из целого в рабочий float: выбрать масштаб
Представление отсчётов с плавающей точкой (floating-point sample representation)представление отсчётов с плавающей точкой (floating-point sample representation) — Хранение значений отсчётов в числовом формате со знаком, значащей частью и показателем степени. Номинальную полную шкалу аудио обычно принимают за ±1, но сам формат позволяет хранить значения за её пределами. позволяет выполнять обработку с дробными значениями. Обычно номинальной полной шкале сопоставляют −1…+1. Для знакового PCM удобен масштаб и перевод : у 16 бит , у 24 — 8388608. Ноль сохраняется, отрицательная граница становится ровно −1, положительная равна и не достигает +1.
Это выбранное соглашение, а не свойство типа float. При 16 битах существуют по меньшей мере три варианта:
| Перевод | −32768 | 32767 | Следствие |
|---|---|---|---|
| q / 32768 | −1 | 1 − 1/32768 | Один равномерный шаг, +1 не достигается |
| q / 32767 | −1 − 1/32767 | +1 | Отрицательная граница выходит ниже −1 |
| q / 32768 при q < 0; q / 32767 иначе | −1 | +1 | Шаг положительной половины немного больше |
Второй вариант не помещает весь диапазон в −1…+1. Если ограничить его снизу значением −1, коды −32768 и −32767 сольются. Третий помещает обе границы в шкалу, но задаёт разные масштабы для двух знаков; обратный перевод тоже должен выбирать масштаб по знаку. Единственного обязательного соглашения здесь нет. Важно выбрать его явно и использовать согласованную пару преобразований.
В FAQ libsndfile, Q10 описан пример несовпадающих масштабов чтения и записи: 20000 / 32768 = 0,6103515625, затем умножение на 32767 даёт 19999,3896484375 и после округления (rounding)округление (rounding) — Замена числа ближайшим допустимым значением, например ближайшим целым для round. Выбор при равном расстоянии до двух значений определяется отдельным соглашением. — 19999. Даже без обработки значение изменилось на один код. Поэтому обратимость проверяют на конкретной версии и настройках библиотеки.
Что помещается в binary32
На рассматриваемых платформах C++ float — IEEE 754 binary32 с 24 значащими двоичными разрядами, double — binary64 с 53. Деление на степень (power)степень (power) — При положительном целом показателе m запись aᵐ означает произведение m одинаковых множителей a. Показатель 0 даёт 1 при ненулевом основании, отрицательный целый показатель — обратную величину соответствующей положительной степени. двойки меняет двоичный масштаб. Все знаковые 16- и 24-битные целые и их значения точно представимы в binary32. При переводе исходных целых 32 бит в такой float часть информации теряется: значащих разрядов уже недостаточно.
Например, при 32 битах математически меньше 1, но при округлении в binary32 становится ровно 1. Соседние большие положительные коды могут дать одинаковый float, и обратный перевод не сможет различить их. Для точного пути без обработки всех 32-битных целых нужен double с согласованным масштабом либо сохранение исходных целых. Для float обещать такой путь нельзя.
Исходный код и тесты ниже рассчитаны на обычные режимы IEEE 754, без -ffast-math. Формат C++ проверяется через std::numeric_limits, а не предполагается для любой существующей платформы.
Из float обратно: округлить и ограничить
Для принятого масштаба сначала возвращаемся к целой шкале: . Затем выбираем ближайшее целое и ограничиваем его диапазоном . Ограничение диапазона (clamp)ограничение диапазона (clamp) — Замена значений за пределами заданного интервала ближайшей границей; значения внутри интервала сохраняются. Перед записью PCM предотвращает выход за диапазон целого типа. оставляет допустимые значения и заменяет превышение ближайшей границей. Результат уже можно приводить к целому типу.
Округлениеокругление (rounding) — Замена числа ближайшим допустимым значением, например ближайшим целым для round. Выбор при равном расстоянии до двух значений определяется отдельным соглашением. отличается от усечения (truncation): для 1,8 ближайшее целое — 2, усечение даёт 1; для −1,8 — соответственно −2 и −1. В наших функциях половины округляются от нуля: 0,5 → 1, −0,5 → −1. std::round использует именно это правило.
Для входа +1 при 16 битах получается 32768, хотя допустимый максимум — 32767. Одного ограничения x до −1…+1 недостаточно: нужна также граница целой шкалы. Для входа 1,5 масштабирование дало бы 49152. В C++ прямое преобразование такого float в int16_t имеет неопределённое поведение; рассчитывать на насыщение или «заворачивание» нельзя (стандарт C++, Floating-integral conversions).
Безопасный кодер на C++
Эта функция возвращает центрированное целое для 8, 16, 24 или 32 бит. Перед умножением ограничиваем вход до −1…+1, чтобы большое конечное число не переполнило промежуточное вычисление. После округления ограничиваем целую шкалу. NaN («не число») и бесконечности отклоняются отдельно. Вычисления ведутся в double, чтобы точно представить максимум 32-битного PCM; всё помещается в int32_t к моменту приведения.
#include <algorithm>
#include <cmath>
#include <stdexcept>
std::int32_t floatToPcm (float x, int bits)
{
if (bits != 8 && bits != 16 && bits != 24 && bits != 32)
throw std::invalid_argument ("Недопустимая разрядность");
if (!std::isfinite (x))
throw std::invalid_argument ("Нужен конечный отсчёт");
const double scale = std::ldexp (1.0, bits - 1);
const double bounded = std::clamp (double (x), -1.0, 1.0);
const double rounded = std::round (bounded * scale);
return static_cast<std::int32_t> (
std::clamp (rounded, -scale, scale - 1.0));
}При 16 битах: floatToPcm (0.5f, 16) → 16384; (1.0f, 16) и (1.5f, 16) → 32767; (−1.5f, 16) → −32768. При 8 битах выход ещё центрированный: к нему нужно прибавить 128 для байта WAV. При отказе функция бросает исключение; такая политика подходит для учебного кодера вне аудиоколбэка (audio callback)аудиоколбэк (audio callback) — Функция, которую аудиосистема вызывает для передачи очередного блока входных данных и получения выходных. Обработчик должен завершиться до срока, когда нужны данные.. В обработчике реального времени проверку и обработку ошибок организуют заранее, без исключений в критичном пути.
Чтобы записать целое обратно в байты, выделим младшие части беззнакового представления. Ниже q должен быть допустимым центрированным кодом, p — указывать на доступные bits/8 байтов. Функция проверяет диапазон, но не может проверить длину памяти за указателем.
void writePcmLE (std::int32_t q, int bits, std::uint8_t* p)
{
if (bits != 8 && bits != 16 && bits != 24 && bits != 32)
throw std::invalid_argument ("Недопустимая разрядность");
const double scale = std::ldexp (1.0, bits - 1);
if (double (q) < -scale || double (q) > scale - 1.0)
throw std::out_of_range ("Код вне диапазона");
const std::uint32_t u = bits == 8
? std::uint32_t (q + 128) : std::uint32_t (q);
for (int i = 0; i < bits / 8; ++i)
p[i] = static_cast<std::uint8_t> ((u >> (8 * i)) & 0xFFu);
}Для отрицательного целого типа int32_t преобразование в uint32_t даёт исходное значение плюс ; для −1 получается 4294967295, то есть все единицы. В 24-битный файл уходят только три младших байта, в 16-битный — два. При q=0, bits=8 записывается 80. Например, вызов writePcmLE (floatToPcm (-1.0f, 24), 24, p) записывает 00 00 80. Выделение результата в байты не зависит от порядка байтов машины.
Ограничение перегрузки сохраняет корректность числа, но меняет сигнал: вершины могут срезаться. Это клиппинг (clipping)клиппинг (clipping) — Ограничение значений сигнала крайними допустимыми уровнями при выходе за диапазон. Срезает вершины волны и создаёт искажения., и его последствия нельзя устранить одним выбором способа округления.
Проверить путь i16 → float → i16
Здесь i16 обозначает знаковое 16-битное целое; в C++ его тип — std::int16_t. Функции чтения выше возвращают то же значение в более широком int32_t.
При декодировании в binary32 без обработки и кодировании функцией выше каждый допустимый i16 должен вернуться без изменений. Исходное целое представимо точно, деление на степень двойки тоже; обратное умножение возвращает исходное число. Проверить можно все 65536 кодов, а не только несколько примеров. Следующий фрагмент — тело main(); к предыдущим заголовкам добавьте <cassert> и <limits>.
static_assert (std::numeric_limits<float>::is_iec559
&& std::numeric_limits<float>::digits == 24);
static_assert (std::numeric_limits<double>::is_iec559
&& std::numeric_limits<double>::digits == 53);
for (std::int32_t q = -32768; q <= 32767; ++q)
{
const float x = static_cast<float> (q) / 32768.0f;
assert (floatToPcm (x, 16) == q);
}Счётчик шире 16 бит: после 32767 цикл должен безопасно перейти к 32768 и закончиться. Не используйте int16_t как такой счётчик.
Для декодера отдельно проверяют ноль, −1, +1 и обе границы из таблицы 2, а также значение вроде −256, которое выявляет перестановку байтов. Для кодера нужны ±0,5, ±1, ±1,5, значения чуть ниже и выше половины шага, ровно ±0,5/32768, отказ на NaN и бесконечностях. Полезно проверить и цикл байты → целое → байты.
Та же точная обратимость возможна для исходных знаковых 24 бит и масштаба 8388608. Она не обещает сохранность после изменения усиления, фильтрации или многократной записи с уменьшением разрядности. Если в файл нужно вернуть исходную запись совершенно без обработки, надёжнее сохранить её байты.
Пример с juce::AudioData
juce::AudioData::Pointer описывает формат отсчёта, порядок байтов, раскладку каналов и возможность записи. Ниже функции для одного 24-битного отсчёта. Пример рассчитан на JUCE 9.0.3, C++17 или новее и модуль juce_audio_basics; нужны три доступных байта входа/выхода. NonInterleaved задаёт соседние отсчёты одного канала. Здесь преобразуется всего один, без разбора стереокадров.
#include <juce_audio_basics/juce_audio_basics.h>
using Pcm24In = juce::AudioData::Pointer<
juce::AudioData::Int24, juce::AudioData::LittleEndian,
juce::AudioData::NonInterleaved, juce::AudioData::Const>;
using Pcm24Out = juce::AudioData::Pointer<
juce::AudioData::Int24, juce::AudioData::LittleEndian,
juce::AudioData::NonInterleaved, juce::AudioData::NonConst>;
float readWithJuce (const std::uint8_t* p)
{
return Pcm24In (p).getAsFloat();
}
void writeWithJuce (float x, std::uint8_t* p)
{
if (!std::isfinite (x))
throw std::invalid_argument ("Нужен конечный отсчёт");
Pcm24Out (p).setAsFloat (std::clamp (x, -1.0f, 1.0f));
}Чтение Int24 делит на 8388608. Запись в этой версии использует тот же масштаб, но ограничивает целую шкалу до −8388607…8388607. Поэтому чтение 00 00 80 даёт −1, а обратная запись — 01 00 80: минимум изменился на один код. У Int16::setAsFloat аналогично нижняя граница −32767. Собственная функция выше сохраняет полный асимметричный диапазон.
В JUCE правило округления задаёт roundToInt, поэтому его не следует автоматически приравнивать к выбранному здесь std::round на половинах. Поведение конкретных методов сверено по исходникам 9.0.3; контракт указателя описан в документации AudioData::Pointer. При обновлении библиотеки границы и округление стоит перепроверить.
Для нескольких отсчётов можно использовать convertSamples(), задав подходящие типы источника и назначения. Однако совпадение формата чтения не гарантирует совпадения политики обратной записи. Если задача требует точного сохранения целых кодов, проверьте именно используемый путь преобразования или оставьте данные целыми.
Чего перевод не исправляет
Перевод 16-битного PCM в float сохраняет уже записанные уровни, но не восстанавливает дробные значения, потерянные при исходном квантовании (quantization)квантование (quantization) — Замена значения отсчёта одним из конечного набора представимых уровней. Разность между выбранным уровнем и исходным значением называется ошибкой квантования.. У значения 0,5, полученного из 16384, может быть много представимых соседей в float; это запас для вычислений, а не новая информация об исходном сигнале.
При уменьшении разрядности или записи обработанного вещественного сигнала возникает новое квантование. Для ослабления зависимости его ошибки от сигнала применяют дизеринг (dithering)дизеринг (dithering) — Добавление специально выбранного слабого случайного шума перед квантованием, чтобы ослабить зависимость ошибки квантования от сигнала. Не восстанавливает потерянную точность и меняет отдельные коды.: перед окончательным округлением добавляют специально выбранный слабый случайный шум. Он не делает результат точнее в каждом отсчёте и нарушает точное совпадение кодов, зато может уменьшить слышимые регулярные искажения. Здесь кодер намеренно без дизеринга, чтобы отдельно проверить знак, масштаб и округление; отличие TPDF-дизеринга (triangular probability density function dither, TPDF dither)TPDF-дизеринг (triangular probability density function dither, TPDF dither) — Шум с треугольным распределением амплитуд, добавляемый перед квантованием. В обычной схеме его размах равен двум шагам квантования. от шумового источника синтезатора разобрано в статье о шуме.
Самопроверка
Почему FF FF FF — −1 для 24 бит, но положительное число после добавления нулевого старшего байта?
В 24 битах установлен знаковый бит 23: . В 32 битах с нулевым добавленным байтом знаковый бит 31 равен 0, поэтому значение +16777215. Для сохранения −1 нужны единицы в новом старшем байте.
Почему clamp до −1…+1 перед умножением на 32768 не позволяет сразу привести результат к int16_t?
Вход +1 даёт 32768, на единицу больше допустимого максимума. Нужно ограничить результат целой шкалой −32768…32767 до приведения. Так +1 и +1,5 дадут 32767.
Обязаны ли все 32-битные целые PCM точно вернуться после перевода в float и обратно?
Нет. У binary32 только 24 значащих двоичных разряда; разные большие целые могут дать одно вещественное значение. Для точного пути без обработки используйте binary64 double с согласованным масштабом либо исходные целые.
Источники
- Smith S. W. The Scientist and Engineer’s Guide to Digital Signal Processing. Гл. 4.2: Fixed Point (Integers). в списке литературы → — представления целых, дополнительный код и асимметричный диапазон. Примеры байтов и расчёты в статье собственные.
- Smith S. W. The Scientist and Engineer’s Guide to Digital Signal Processing. Гл. 4.3: Floating Point (Real Numbers). в списке литературы → — значащая часть и показатель степени; 24 значащих разряда одинарной точности.
- Smith S. W. The Scientist and Engineer’s Guide to Digital Signal Processing. Гл. 3.1: Quantization. в списке литературы → — ошибка квантования и добавление случайного шума перед квантованием.
- Kabal P. Wave File Specifications. в списке литературы → — кодирование PCM, 8 бит со смещением и более широкие знаковые отсчёты.
- libsndfile. FAQ, Q10 — различие масштабов чтения и записи нормированных отсчётов.
- Рабочий текст стандарта C++, Floating-integral conversions, Shift operators — условия преобразования вещественного числа в целое и правила сдвига. Для варианта с
bit_castв статье требуется C++20. - JUCE. AudioData::Pointer и исходники JUCE 9.0.3, AudioDataConverters — форматы указателя, масштаб, границы записи и методы преобразования.