mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Make FLAC encoder bit depth depend on input sample format, add sampleFormat audio transformation option, fix incorrect bitrate validation for FLAC (closes #357)
This commit is contained in:
Generated
BIN
Binary file not shown.
@@ -12,7 +12,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define BITS_PER_SAMPLE 16
|
||||
#define COMPRESSION_LEVEL 5
|
||||
|
||||
typedef struct {
|
||||
@@ -23,14 +22,10 @@ typedef struct {
|
||||
typedef struct {
|
||||
FLAC__StreamEncoder *encoder;
|
||||
|
||||
// Input buffer for interleaved int16 samples from JS
|
||||
int16_t *input_buffer;
|
||||
// Input buffer for interleaved int32 samples from JS
|
||||
FLAC__int32 *input_buffer;
|
||||
int input_buffer_size;
|
||||
|
||||
// Widened to int32 for libFLAC
|
||||
FLAC__int32 *int32_buffer;
|
||||
int int32_buffer_size;
|
||||
|
||||
// Contiguous output buffer for encoded frame data
|
||||
uint8_t *output_buffer;
|
||||
int output_size;
|
||||
@@ -48,6 +43,7 @@ typedef struct {
|
||||
bool header_done;
|
||||
|
||||
int channels;
|
||||
int bits_per_sample;
|
||||
} EncoderContext;
|
||||
|
||||
static void ensure_output_capacity(EncoderContext *ctx, int needed) {
|
||||
@@ -120,13 +116,14 @@ static void reset_output(EncoderContext *ctx) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int init_encoder(int channels, int sample_rate) {
|
||||
int init_encoder(int channels, int sample_rate, int bits_per_sample) {
|
||||
EncoderContext *ctx = calloc(1, sizeof(EncoderContext));
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx->channels = channels;
|
||||
ctx->bits_per_sample = bits_per_sample;
|
||||
|
||||
ctx->encoder = FLAC__stream_encoder_new();
|
||||
if (!ctx->encoder) {
|
||||
@@ -136,7 +133,7 @@ int init_encoder(int channels, int sample_rate) {
|
||||
|
||||
FLAC__stream_encoder_set_channels(ctx->encoder, channels);
|
||||
FLAC__stream_encoder_set_sample_rate(ctx->encoder, sample_rate);
|
||||
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, BITS_PER_SAMPLE);
|
||||
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, bits_per_sample);
|
||||
FLAC__stream_encoder_set_compression_level(ctx->encoder, COMPRESSION_LEVEL);
|
||||
FLAC__stream_encoder_set_verify(ctx->encoder, false);
|
||||
|
||||
@@ -174,19 +171,15 @@ EMSCRIPTEN_KEEPALIVE
|
||||
int send_samples(int ctx_ptr, int num_samples) {
|
||||
EncoderContext *ctx = (EncoderContext *)ctx_ptr;
|
||||
|
||||
// Widen int16 to int32 for libFLAC
|
||||
int total = num_samples * ctx->channels;
|
||||
if (total > ctx->int32_buffer_size) {
|
||||
ctx->int32_buffer = realloc(ctx->int32_buffer, total * sizeof(FLAC__int32));
|
||||
ctx->int32_buffer_size = total;
|
||||
}
|
||||
int shift = 32 - ctx->bits_per_sample;
|
||||
for (int i = 0; i < total; i++) {
|
||||
ctx->int32_buffer[i] = ctx->input_buffer[i];
|
||||
ctx->input_buffer[i] >>= shift;
|
||||
}
|
||||
|
||||
reset_output(ctx);
|
||||
|
||||
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->int32_buffer, num_samples);
|
||||
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->input_buffer, num_samples);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ type ExtendedEmscriptenModule = EmscriptenModule & {
|
||||
let module: ExtendedEmscriptenModule;
|
||||
let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
|
||||
|
||||
let initEncoderFn: (channels: number, sampleRate: number) => number;
|
||||
let initEncoderFn: (channels: number, sampleRate: number, bitsPerSample: number) => number;
|
||||
let getEncodeInputPtr: (ctx: number, size: number) => number;
|
||||
let sendSamplesFn: (ctx: number, numSamples: number) => number;
|
||||
let getOutputData: (ctx: number) => number;
|
||||
@@ -37,7 +37,7 @@ const ensureModule = async () => {
|
||||
module = await modulePromise;
|
||||
modulePromise = null;
|
||||
|
||||
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number']);
|
||||
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number']);
|
||||
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
|
||||
sendSamplesFn = module.cwrap('send_samples', 'number', ['number', 'number']);
|
||||
getOutputData = module.cwrap('get_output_data', 'number', ['number']);
|
||||
@@ -50,10 +50,10 @@ const ensureModule = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const initEncoder = async (numberOfChannels: number, sampleRate: number) => {
|
||||
const initEncoder = async (numberOfChannels: number, sampleRate: number, bitsPerSample: 16 | 24) => {
|
||||
await ensureModule();
|
||||
|
||||
const ctx = initEncoderFn(numberOfChannels, sampleRate);
|
||||
const ctx = initEncoderFn(numberOfChannels, sampleRate, bitsPerSample);
|
||||
if (ctx === 0) {
|
||||
throw new Error('Failed to initialize FLAC encoder.');
|
||||
}
|
||||
@@ -121,6 +121,7 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
|
||||
const { ctx, header } = await initEncoder(
|
||||
command.data.numberOfChannels,
|
||||
command.data.sampleRate,
|
||||
command.data.bitsPerSample,
|
||||
);
|
||||
result = { type: command.type, ctx, header };
|
||||
transferables.push(header);
|
||||
|
||||
@@ -29,7 +29,7 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
reject: (reason?: unknown) => void;
|
||||
}>();
|
||||
|
||||
private ctx = 0;
|
||||
private ctx: number | null = null;
|
||||
private chunkMetadata: EncodedAudioChunkMetadata = {};
|
||||
private description: Uint8Array | null = null;
|
||||
private nextTimestampInSamples: number | null = null;
|
||||
@@ -65,19 +65,6 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
};
|
||||
nodeWorker.on('message', onMessage);
|
||||
}
|
||||
|
||||
const result = await this.sendCommand({
|
||||
type: 'init',
|
||||
data: {
|
||||
numberOfChannels: this.config.numberOfChannels,
|
||||
sampleRate: this.config.sampleRate,
|
||||
},
|
||||
});
|
||||
|
||||
this.ctx = result.ctx;
|
||||
|
||||
this.description = new Uint8Array(result.header);
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
private resetInternalState() {
|
||||
@@ -94,15 +81,50 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
}
|
||||
|
||||
async encode(audioSample: AudioSample) {
|
||||
if (this.ctx === null) {
|
||||
// This is the first sample, let's do some init
|
||||
|
||||
let bitsPerSample: 16 | 24;
|
||||
switch (audioSample.format) {
|
||||
case 'u8':
|
||||
case 'u8-planar':
|
||||
case 's16':
|
||||
case 's16-planar':
|
||||
bitsPerSample = 16;
|
||||
break;
|
||||
case 's32':
|
||||
case 's32-planar':
|
||||
case 'f32':
|
||||
case 'f32-planar':
|
||||
bitsPerSample = 24;
|
||||
break;
|
||||
default:
|
||||
assertNever(audioSample.format);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
const result = await this.sendCommand({
|
||||
type: 'init',
|
||||
data: {
|
||||
numberOfChannels: this.config.numberOfChannels,
|
||||
sampleRate: this.config.sampleRate,
|
||||
bitsPerSample,
|
||||
},
|
||||
});
|
||||
|
||||
this.ctx = result.ctx;
|
||||
this.description = new Uint8Array(result.header);
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
if (this.nextTimestampInSamples === null) {
|
||||
this.nextTimestampInSamples = Math.round(audioSample.timestamp * this.config.sampleRate);
|
||||
}
|
||||
|
||||
const totalBytes = audioSample.allocationSize({ format: 's16', planeIndex: 0 });
|
||||
const audioBytes = new Uint8Array(totalBytes);
|
||||
audioSample.copyTo(audioBytes, { format: 's16', planeIndex: 0 });
|
||||
const totalBytes = audioSample.allocationSize({ format: 's32', planeIndex: 0 });
|
||||
const audioData = new ArrayBuffer(totalBytes);
|
||||
audioSample.copyTo(audioData, { format: 's32', planeIndex: 0 });
|
||||
|
||||
const audioData = audioBytes.buffer;
|
||||
const result = await this.sendCommand({
|
||||
type: 'encode',
|
||||
data: {
|
||||
@@ -116,6 +138,10 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
}
|
||||
|
||||
async flush() {
|
||||
if (this.ctx === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } });
|
||||
this.emitPackets(result.packets);
|
||||
|
||||
@@ -198,3 +224,8 @@ function assert(x: unknown): asserts x {
|
||||
throw new Error('Assertion failed.');
|
||||
}
|
||||
}
|
||||
|
||||
export const assertNever = (x: never) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unexpected value: ${x}`);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export type WorkerCommand = {
|
||||
data: {
|
||||
numberOfChannels: number;
|
||||
sampleRate: number;
|
||||
bitsPerSample: 16 | 24;
|
||||
};
|
||||
} | {
|
||||
type: 'encode';
|
||||
|
||||
Reference in New Issue
Block a user