402 lines
14 KiB
C++
402 lines
14 KiB
C++
// mastering.cpp — Reference-based audio mastering CLI tool
|
|
//
|
|
// Usage: mastering --target input.wav --reference ref.wav --output mastered.wav
|
|
// [--no-limiter] [--pcm24]
|
|
//
|
|
// Implements the matchering algorithm: spectral + RMS matching against a reference track.
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "mastering.h"
|
|
|
|
// ─── Minimal WAV Reader/Writer ──────────────────────────────────────
|
|
|
|
#pragma pack(push, 1)
|
|
struct WavHeader {
|
|
char riff[4]; // "RIFF"
|
|
uint32_t file_size; // file size - 8
|
|
char wave[4]; // "WAVE"
|
|
};
|
|
struct WavChunkHdr {
|
|
char id[4];
|
|
uint32_t size;
|
|
};
|
|
struct WavFmt {
|
|
uint16_t format; // 1=PCM, 3=IEEE float
|
|
uint16_t channels;
|
|
uint32_t sample_rate;
|
|
uint32_t byte_rate;
|
|
uint16_t block_align;
|
|
uint16_t bits_per_sample;
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
struct WavData {
|
|
std::vector<float> L, R;
|
|
int sample_rate;
|
|
int channels;
|
|
};
|
|
|
|
static bool wav_read(const char * path, WavData & out) {
|
|
FILE * f = fopen(path, "rb");
|
|
if (!f) {
|
|
fprintf(stderr, "[WAV] Cannot open: %s\n", path);
|
|
return false;
|
|
}
|
|
|
|
WavHeader hdr;
|
|
if (fread(&hdr, sizeof(hdr), 1, f) != 1
|
|
|| memcmp(hdr.riff, "RIFF", 4) != 0
|
|
|| memcmp(hdr.wave, "WAVE", 4) != 0) {
|
|
fprintf(stderr, "[WAV] Invalid WAV header: %s\n", path);
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
|
|
WavFmt fmt = {};
|
|
bool found_fmt = false, found_data = false;
|
|
int data_size = 0;
|
|
|
|
while (!feof(f)) {
|
|
WavChunkHdr chunk;
|
|
if (fread(&chunk, sizeof(chunk), 1, f) != 1) break;
|
|
|
|
if (memcmp(chunk.id, "fmt ", 4) == 0) {
|
|
int to_read = std::min((uint32_t) sizeof(fmt), chunk.size);
|
|
if (fread(&fmt, to_read, 1, f) != 1) break;
|
|
// Skip extra fmt bytes
|
|
if (chunk.size > (uint32_t) to_read) {
|
|
fseek(f, chunk.size - to_read, SEEK_CUR);
|
|
}
|
|
found_fmt = true;
|
|
} else if (memcmp(chunk.id, "data", 4) == 0) {
|
|
data_size = chunk.size;
|
|
found_data = true;
|
|
break; // data follows
|
|
} else {
|
|
// Skip unknown chunk
|
|
fseek(f, chunk.size, SEEK_CUR);
|
|
}
|
|
}
|
|
|
|
if (!found_fmt || !found_data) {
|
|
fprintf(stderr, "[WAV] Missing fmt or data chunk: %s\n", path);
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
|
|
// WAVE_FORMAT_EXTENSIBLE (0xFFFE): the real format code is in the sub-format
|
|
// GUID at the end of the extended fmt chunk. The first 2 bytes of the GUID
|
|
// encode the actual format (1=PCM, 3=float). Common for 24/32-bit and
|
|
// multi-channel audio from DAWs.
|
|
if (fmt.format == 0xFFFE) {
|
|
// The extended fmt chunk has: cbSize(2) + validBitsPerSample(2) +
|
|
// channelMask(4) + subFormat GUID(16). We already read the base WavFmt
|
|
// (16 bytes), and we skipped any extra bytes. We need to re-read the
|
|
// extension. Seek back to re-read the extension portion.
|
|
// Actually, we skipped extra fmt bytes — we need to read them before skip.
|
|
// Let's fix: the fmt chunk was already fully consumed (read + skip).
|
|
// We need to handle this in the fmt reading section above. For now,
|
|
// the simplest fix: re-open and re-parse just the fmt extension.
|
|
fprintf(stderr, "[WAV] WAVE_FORMAT_EXTENSIBLE detected, attempting re-parse: %s\n", path);
|
|
fclose(f);
|
|
|
|
f = fopen(path, "rb");
|
|
if (!f) return false;
|
|
fseek(f, sizeof(WavHeader), SEEK_SET);
|
|
|
|
// Scan for fmt chunk again
|
|
while (!feof(f)) {
|
|
WavChunkHdr chunk2;
|
|
if (fread(&chunk2, sizeof(chunk2), 1, f) != 1) break;
|
|
if (memcmp(chunk2.id, "fmt ", 4) == 0) {
|
|
// Read base fmt (16 bytes)
|
|
WavFmt fmt2 = {};
|
|
int base = std::min((uint32_t)sizeof(fmt2), chunk2.size);
|
|
if (fread(&fmt2, base, 1, f) != 1) break;
|
|
|
|
// Read extension: cbSize(2), validBits(2), channelMask(4), subFormat(16)
|
|
if (chunk2.size >= 40) { // 16 base + 2 cbSize + 2 validBits + 4 mask + 16 GUID
|
|
uint16_t cb_size = 0;
|
|
uint16_t valid_bits = 0;
|
|
uint32_t channel_mask = 0;
|
|
uint16_t sub_format = 0;
|
|
|
|
fread(&cb_size, 2, 1, f);
|
|
fread(&valid_bits, 2, 1, f);
|
|
fread(&channel_mask, 4, 1, f);
|
|
fread(&sub_format, 2, 1, f); // first 2 bytes of GUID = real format
|
|
|
|
fmt.format = sub_format;
|
|
if (valid_bits > 0) {
|
|
fmt.bits_per_sample = valid_bits;
|
|
}
|
|
fprintf(stderr, "[WAV] EXTENSIBLE sub-format: %d (%s), valid bits: %d\n",
|
|
sub_format, sub_format == 1 ? "PCM" : sub_format == 3 ? "float" : "unknown",
|
|
valid_bits > 0 ? valid_bits : fmt.bits_per_sample);
|
|
}
|
|
break;
|
|
} else {
|
|
fseek(f, chunk2.size, SEEK_CUR);
|
|
}
|
|
}
|
|
fclose(f);
|
|
|
|
// Re-open and seek to data chunk
|
|
f = fopen(path, "rb");
|
|
if (!f) return false;
|
|
fseek(f, sizeof(WavHeader), SEEK_SET);
|
|
found_data = false;
|
|
while (!feof(f)) {
|
|
WavChunkHdr chunk2;
|
|
if (fread(&chunk2, sizeof(chunk2), 1, f) != 1) break;
|
|
if (memcmp(chunk2.id, "data", 4) == 0) {
|
|
data_size = chunk2.size;
|
|
found_data = true;
|
|
break;
|
|
} else {
|
|
fseek(f, chunk2.size, SEEK_CUR);
|
|
}
|
|
}
|
|
if (!found_data) {
|
|
fprintf(stderr, "[WAV] Cannot find data chunk on re-parse: %s\n", path);
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (fmt.format != 1 && fmt.format != 3) {
|
|
fprintf(stderr, "[WAV] Unsupported format %d (need PCM=1 or float=3): %s\n",
|
|
fmt.format, path);
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
|
|
if (fmt.channels < 1 || fmt.channels > 2) {
|
|
fprintf(stderr, "[WAV] Unsupported channel count %d: %s\n", fmt.channels, path);
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
|
|
int bytes_per_sample = fmt.bits_per_sample / 8;
|
|
int n_samples = data_size / (bytes_per_sample * fmt.channels);
|
|
|
|
out.sample_rate = fmt.sample_rate;
|
|
out.channels = fmt.channels;
|
|
out.L.resize(n_samples);
|
|
out.R.resize(n_samples);
|
|
|
|
// Read raw data
|
|
std::vector<uint8_t> raw(data_size);
|
|
if (fread(raw.data(), 1, data_size, f) != (size_t) data_size) {
|
|
fprintf(stderr, "[WAV] Truncated data: %s\n", path);
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
fclose(f);
|
|
|
|
// Convert to float
|
|
for (int i = 0; i < n_samples; i++) {
|
|
for (int ch = 0; ch < fmt.channels; ch++) {
|
|
int offset = (i * fmt.channels + ch) * bytes_per_sample;
|
|
float val = 0.0f;
|
|
|
|
if (fmt.format == 3) {
|
|
// IEEE float
|
|
if (bytes_per_sample == 4) {
|
|
memcpy(&val, raw.data() + offset, 4);
|
|
} else if (bytes_per_sample == 8) {
|
|
double dval;
|
|
memcpy(&dval, raw.data() + offset, 8);
|
|
val = (float) dval;
|
|
}
|
|
} else {
|
|
// PCM integer
|
|
if (bytes_per_sample == 2) {
|
|
int16_t ival;
|
|
memcpy(&ival, raw.data() + offset, 2);
|
|
val = ival / 32768.0f;
|
|
} else if (bytes_per_sample == 3) {
|
|
int32_t ival = 0;
|
|
memcpy(&ival, raw.data() + offset, 3);
|
|
if (ival & 0x800000) ival |= 0xFF000000; // sign extend
|
|
val = ival / 8388608.0f;
|
|
} else if (bytes_per_sample == 4) {
|
|
int32_t ival;
|
|
memcpy(&ival, raw.data() + offset, 4);
|
|
val = (float) ((double) ival / 2147483648.0);
|
|
}
|
|
}
|
|
|
|
if (ch == 0) out.L[i] = val;
|
|
else out.R[i] = val;
|
|
}
|
|
}
|
|
|
|
// Mono → stereo
|
|
if (fmt.channels == 1) {
|
|
out.R = out.L;
|
|
out.channels = 2;
|
|
}
|
|
|
|
fprintf(stderr, "[WAV] Read %s: %d samples, %d ch, %d Hz, %d-bit %s\n",
|
|
path, n_samples, fmt.channels, fmt.sample_rate,
|
|
fmt.bits_per_sample, fmt.format == 3 ? "float" : "PCM");
|
|
return true;
|
|
}
|
|
|
|
static bool wav_write(const char * path, const float * L, const float * R, int n,
|
|
int sample_rate, int bits = 16) {
|
|
FILE * f = fopen(path, "wb");
|
|
if (!f) {
|
|
fprintf(stderr, "[WAV] Cannot create: %s\n", path);
|
|
return false;
|
|
}
|
|
|
|
int channels = 2;
|
|
int bytes_per_sample = bits / 8;
|
|
int data_size = n * channels * bytes_per_sample;
|
|
|
|
WavHeader hdr;
|
|
memcpy(hdr.riff, "RIFF", 4);
|
|
hdr.file_size = 36 + data_size;
|
|
memcpy(hdr.wave, "WAVE", 4);
|
|
|
|
WavChunkHdr fmt_chunk;
|
|
memcpy(fmt_chunk.id, "fmt ", 4);
|
|
fmt_chunk.size = 16;
|
|
|
|
WavFmt fmt;
|
|
fmt.format = (bits == 32) ? 3 : 1; // float or PCM
|
|
fmt.channels = channels;
|
|
fmt.sample_rate = sample_rate;
|
|
fmt.bits_per_sample = bits;
|
|
fmt.block_align = channels * bytes_per_sample;
|
|
fmt.byte_rate = sample_rate * fmt.block_align;
|
|
|
|
WavChunkHdr data_chunk;
|
|
memcpy(data_chunk.id, "data", 4);
|
|
data_chunk.size = data_size;
|
|
|
|
fwrite(&hdr, sizeof(hdr), 1, f);
|
|
fwrite(&fmt_chunk, sizeof(fmt_chunk), 1, f);
|
|
fwrite(&fmt, sizeof(fmt), 1, f);
|
|
fwrite(&data_chunk, sizeof(data_chunk), 1, f);
|
|
|
|
// Write interleaved samples
|
|
for (int i = 0; i < n; i++) {
|
|
float l = std::clamp(L[i], -1.0f, 1.0f);
|
|
float r = std::clamp(R[i], -1.0f, 1.0f);
|
|
|
|
if (bits == 16) {
|
|
int16_t sl = (int16_t) (l * 32767.0f);
|
|
int16_t sr = (int16_t) (r * 32767.0f);
|
|
fwrite(&sl, 2, 1, f);
|
|
fwrite(&sr, 2, 1, f);
|
|
} else if (bits == 24) {
|
|
int32_t sl = (int32_t) (l * 8388607.0f);
|
|
int32_t sr_v = (int32_t) (r * 8388607.0f);
|
|
fwrite(&sl, 3, 1, f);
|
|
fwrite(&sr_v, 3, 1, f);
|
|
} else if (bits == 32) {
|
|
fwrite(&l, 4, 1, f);
|
|
fwrite(&r, 4, 1, f);
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
fprintf(stderr, "[WAV] Wrote %s: %d samples, %d Hz, %d-bit\n",
|
|
path, n, sample_rate, bits);
|
|
return true;
|
|
}
|
|
|
|
// ─── CLI ────────────────────────────────────────────────────────────
|
|
|
|
static void print_usage(const char * prog) {
|
|
fprintf(stderr,
|
|
"Usage: %s --target input.wav --reference ref.wav --output mastered.wav\n"
|
|
" [--pcm24] [--pcm32f]\n"
|
|
"\n"
|
|
"Reference-based audio mastering using the matchering algorithm.\n"
|
|
"Matches the RMS level, frequency spectrum, and dynamic range\n"
|
|
"of the target track to the reference track.\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" --target PATH Input audio file to master (WAV)\n"
|
|
" --reference PATH Reference track to match against (WAV)\n"
|
|
" --output PATH Output mastered file (WAV)\n"
|
|
" --pcm24 Write 24-bit PCM output (default: 16-bit)\n"
|
|
" --pcm32f Write 32-bit float output\n",
|
|
prog);
|
|
}
|
|
|
|
int main(int argc, char ** argv) {
|
|
const char * target_path = nullptr;
|
|
const char * ref_path = nullptr;
|
|
const char * output_path = nullptr;
|
|
int output_bits = 16;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--target") == 0 && i + 1 < argc) {
|
|
target_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--reference") == 0 && i + 1 < argc) {
|
|
ref_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--output") == 0 && i + 1 < argc) {
|
|
output_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--pcm24") == 0) {
|
|
output_bits = 24;
|
|
} else if (strcmp(argv[i], "--pcm32f") == 0) {
|
|
output_bits = 32;
|
|
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
|
print_usage(argv[0]);
|
|
return 0;
|
|
} else {
|
|
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (!target_path || !ref_path || !output_path) {
|
|
fprintf(stderr, "Error: --target, --reference, and --output are required\n\n");
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
auto t_start = std::chrono::high_resolution_clock::now();
|
|
|
|
// Read input files
|
|
WavData target, reference;
|
|
if (!wav_read(target_path, target)) return 1;
|
|
if (!wav_read(ref_path, reference)) return 1;
|
|
|
|
// Run mastering
|
|
auto result = mastering_process(
|
|
target.L.data(), target.R.data(), (int) target.L.size(), target.sample_rate,
|
|
reference.L.data(), reference.R.data(), (int) reference.L.size(), reference.sample_rate
|
|
);
|
|
|
|
if (!result.success) {
|
|
fprintf(stderr, "[Mastering] FAILED: %s\n", result.error ? result.error : "unknown");
|
|
return 1;
|
|
}
|
|
|
|
// Write output
|
|
if (!wav_write(output_path, result.L.data(), result.R.data(),
|
|
(int) result.L.size(), target.sample_rate, output_bits)) {
|
|
return 1;
|
|
}
|
|
|
|
auto t_end = std::chrono::high_resolution_clock::now();
|
|
double elapsed = std::chrono::duration<double>(t_end - t_start).count();
|
|
fprintf(stderr, "[Mastering] Total time: %.2f seconds\n", elapsed);
|
|
|
|
return 0;
|
|
}
|