mirror of https://github.com/bjornbytes/lovr.git
453 lines
16 KiB
C
453 lines
16 KiB
C
#include "data/sound.h"
|
|
#include "data/blob.h"
|
|
#include "util.h"
|
|
#include "lib/stb/stb_vorbis.h"
|
|
#include "lib/miniaudio/miniaudio.h"
|
|
#define MINIMP3_FLOAT_OUTPUT
|
|
#define MINIMP3_NO_STDIO
|
|
#include "lib/minimp3/minimp3_ex.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static const ma_format miniaudioFormats[] = {
|
|
[SAMPLE_I16] = ma_format_s16,
|
|
[SAMPLE_F32] = ma_format_f32
|
|
};
|
|
|
|
struct Sound {
|
|
uint32_t ref;
|
|
SoundCallback* read;
|
|
void* callbackMemo; // When using lovrSoundCreateFromCallback, any state the read callback uses should be stored here
|
|
SoundDestroyCallback* callbackMemoDestroy; // This should be used to free the callbackMemo pointer (if appropriate)
|
|
Blob* blob;
|
|
void* decoder;
|
|
void* stream;
|
|
SampleFormat format;
|
|
ChannelLayout layout;
|
|
uint32_t sampleRate;
|
|
uint32_t frames;
|
|
uint32_t cursor;
|
|
};
|
|
|
|
// Readers
|
|
|
|
static uint32_t lovrSoundReadRaw(Sound* sound, uint32_t offset, uint32_t count, void* data) {
|
|
uint8_t* p = sound->blob->data;
|
|
uint32_t n = sound->frames == LOVR_SOUND_ENDLESS ? count : MIN(count, sound->frames - offset);
|
|
size_t stride = lovrSoundGetStride(sound);
|
|
memcpy(data, p + offset * stride, n * stride);
|
|
return n;
|
|
}
|
|
|
|
static uint32_t lovrSoundReadStream(Sound* sound, uint32_t offset, uint32_t count, void* data) {
|
|
void* p = NULL;
|
|
uint32_t frames = count;
|
|
ma_pcm_rb_acquire_read(sound->stream, &frames, &p);
|
|
memcpy(data, p, frames * lovrSoundGetStride(sound));
|
|
ma_pcm_rb_commit_read(sound->stream, frames, p);
|
|
return frames;
|
|
}
|
|
|
|
static uint32_t lovrSoundReadOgg(Sound* sound, uint32_t offset, uint32_t count, void* data) {
|
|
if (sound->cursor != offset) {
|
|
stb_vorbis_seek(sound->decoder, (int) offset);
|
|
sound->cursor = offset;
|
|
}
|
|
|
|
uint32_t channelCount = lovrSoundGetChannelCount(sound);
|
|
uint32_t sampleCount = count * channelCount;
|
|
uint32_t n = stb_vorbis_get_samples_float_interleaved(sound->decoder, channelCount, data, sampleCount);
|
|
sound->cursor += n;
|
|
return n;
|
|
}
|
|
|
|
static uint32_t lovrSoundReadMp3(Sound* sound, uint32_t offset, uint32_t count, void* data) {
|
|
if (sound->cursor != offset) {
|
|
mp3dec_ex_seek(sound->decoder, offset);
|
|
sound->cursor = offset;
|
|
}
|
|
|
|
uint32_t channels = lovrSoundGetChannelCount(sound);
|
|
size_t samples = mp3dec_ex_read(sound->decoder, data, count * channels);
|
|
uint32_t frames = samples / channels;
|
|
sound->cursor += frames;
|
|
return frames;
|
|
}
|
|
|
|
// Sound
|
|
|
|
Sound* lovrSoundCreateRaw(uint32_t frames, SampleFormat format, ChannelLayout layout, uint32_t sampleRate, Blob* blob) {
|
|
Sound* sound = calloc(1, sizeof(Sound));
|
|
lovrAssert(sound, "Out of memory");
|
|
sound->ref = 1;
|
|
sound->frames = frames;
|
|
sound->format = format;
|
|
sound->layout = layout;
|
|
sound->sampleRate = sampleRate;
|
|
sound->read = lovrSoundReadRaw;
|
|
size_t size = frames * lovrSoundGetStride(sound);
|
|
void* data = calloc(1, size);
|
|
lovrAssert(data, "Out of memory");
|
|
sound->blob = lovrBlobCreate(data, size, "Sound");
|
|
|
|
if (blob) {
|
|
memcpy(sound->blob->data, blob->data, MIN(size, blob->size));
|
|
}
|
|
|
|
return sound;
|
|
}
|
|
|
|
Sound* lovrSoundCreateStream(uint32_t frames, SampleFormat format, ChannelLayout layout, uint32_t sampleRate) {
|
|
Sound* sound = calloc(1, sizeof(Sound));
|
|
lovrAssert(sound, "Out of memory");
|
|
sound->ref = 1;
|
|
sound->frames = frames;
|
|
sound->format = format;
|
|
sound->layout = layout;
|
|
sound->sampleRate = sampleRate;
|
|
sound->read = lovrSoundReadStream;
|
|
sound->stream = malloc(sizeof(ma_pcm_rb));
|
|
lovrAssert(sound->stream, "Out of memory");
|
|
size_t size = frames * lovrSoundGetStride(sound);
|
|
void* data = malloc(size);
|
|
lovrAssert(data, "Out of memory");
|
|
sound->blob = lovrBlobCreate(data, size, NULL);
|
|
ma_result status = ma_pcm_rb_init(miniaudioFormats[format], lovrSoundGetChannelCount(sound), frames, data, NULL, sound->stream);
|
|
lovrAssert(status == MA_SUCCESS, "Failed to create ring buffer for streamed Sound: %s (%d)", ma_result_description(status), status);
|
|
return sound;
|
|
}
|
|
|
|
static bool loadOgg(Sound* sound, Blob* blob, bool decode) {
|
|
if (blob->size < 4 || memcmp(blob->data, "OggS", 4)) return false;
|
|
|
|
sound->decoder = stb_vorbis_open_memory(blob->data, (int) blob->size, NULL, NULL);
|
|
lovrAssert(sound->decoder, "Could not load Ogg from '%s'", blob->name);
|
|
|
|
stb_vorbis_info info = stb_vorbis_get_info(sound->decoder);
|
|
sound->format = SAMPLE_F32;
|
|
sound->layout = info.channels >= 2 ? CHANNEL_STEREO : CHANNEL_MONO;
|
|
sound->sampleRate = info.sample_rate;
|
|
sound->frames = stb_vorbis_stream_length_in_samples(sound->decoder);
|
|
|
|
if (decode) {
|
|
sound->read = lovrSoundReadRaw;
|
|
size_t size = sound->frames * lovrSoundGetStride(sound);
|
|
void* data = calloc(1, size);
|
|
lovrAssert(data, "Out of memory");
|
|
sound->blob = lovrBlobCreate(data, size, "Sound");
|
|
if (stb_vorbis_get_samples_float_interleaved(sound->decoder, lovrSoundGetChannelCount(sound), data, size / sizeof(float)) < (int) sound->frames) {
|
|
lovrThrow("Could not decode vorbis from '%s'", blob->name);
|
|
}
|
|
stb_vorbis_close(sound->decoder);
|
|
sound->decoder = NULL;
|
|
return true;
|
|
} else {
|
|
sound->read = lovrSoundReadOgg;
|
|
sound->blob = blob;
|
|
lovrRetain(blob);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// The WAV importer supports:
|
|
// - 16, 24, 32 bit PCM or 32 bit floating point samples, uncompressed
|
|
// - WAVE_FORMAT_EXTENSIBLE format extension
|
|
// - mono (1), stereo (2), or first-order full-sphere ambisonic (4) channel layouts
|
|
// - Ambisonic formats:
|
|
// - AMB: AMBISONIC_B_FORMAT extensible format GUIDs (Furse-Malham channel ordering/normalization)
|
|
// - AmbiX: All other 4 channel files assume ACN channel ordering and SN3D normalization
|
|
static bool loadWAV(Sound* sound, Blob* blob, bool decode) {
|
|
if (blob->size < 64 || memcmp(blob->data, "RIFF", 4)) return false;
|
|
|
|
typedef struct {
|
|
uint32_t id;
|
|
uint32_t size;
|
|
uint32_t fileFormat;
|
|
uint32_t fmtId;
|
|
uint32_t fmtSize;
|
|
uint16_t format;
|
|
uint16_t channels;
|
|
uint32_t sampleRate;
|
|
uint32_t byteRate;
|
|
uint16_t frameSize;
|
|
uint16_t sampleSize;
|
|
uint16_t extSize;
|
|
uint16_t validBits;
|
|
uint32_t channelMask;
|
|
char guid[16];
|
|
} wav_t;
|
|
|
|
uint8_t guidpcm[16] = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 };
|
|
uint8_t guidf32[16] = { 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 };
|
|
uint8_t guidpcmamb[16] = { 0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00 };
|
|
uint8_t guidf32amb[16] = { 0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00 };
|
|
|
|
wav_t* wav = blob->data;
|
|
lovrAssert(wav->size == blob->size - 8, "Invalid WAV");
|
|
lovrAssert(!memcmp(&wav->fileFormat, "WAVE", 4), "Invalid WAV");
|
|
lovrAssert(!memcmp(&wav->fmtId, "fmt ", 4), "Invalid WAV");
|
|
lovrAssert(wav->sampleSize == 16 || wav->sampleSize == 24 || wav->sampleSize == 32, "Invalid WAV sample size");
|
|
bool extensible = wav->fmtSize == 40 && wav->extSize == 22 && wav->format == 65534;
|
|
bool amb = extensible && (!memcmp(wav->guid, guidpcmamb, 16) || !memcmp(wav->guid, guidf32amb, 16));
|
|
bool pcm = extensible ? wav->guid[0] == 0x01 : wav->format == 1;
|
|
bool f32 = (extensible ? wav->guid[0] == 0x03 : wav->format == 3) && wav->sampleSize == 32;
|
|
|
|
if (extensible && !amb && memcmp(wav->guid, guidpcm, 16) && memcmp(wav->guid, guidf32, 16)) {
|
|
lovrThrow("Invalid WAV GUID");
|
|
}
|
|
|
|
lovrAssert(pcm || f32, "Invalid WAV sample format");
|
|
lovrAssert(wav->channels != 9 && wav->channels != 16, "Invalid WAV channel count"" (Note: only first order ambisonics are supported)");
|
|
lovrAssert(wav->channels == 1 || wav->channels == 2 || wav->channels == 4, "Invalid WAV channel count");
|
|
|
|
sound->format = f32 || wav->sampleSize == 24 || wav->sampleSize == 32 ? SAMPLE_F32 : SAMPLE_I16;
|
|
sound->layout = wav->channels == 4 ? CHANNEL_AMBISONIC : (wav->channels == 2 ? CHANNEL_STEREO : CHANNEL_MONO);
|
|
sound->sampleRate = wav->sampleRate;
|
|
|
|
// Search for data chunk containing samples
|
|
size_t offset = 12 + 8 + wav->fmtSize;
|
|
char* data = (char*) blob->data + offset;
|
|
for (;;) {
|
|
uint32_t size;
|
|
memcpy(&size, data + 4, 4);
|
|
if (!memcmp(data, "data", 4)) {
|
|
lovrAssert(offset + 8 + size <= blob->size, "Invalid WAV");
|
|
sound->frames = size / wav->frameSize;
|
|
data += 8;
|
|
break;
|
|
} else if (offset + 8 + size > blob->size - 8) { // EOF
|
|
return false;
|
|
} else {
|
|
offset += 8 + size;
|
|
data += 8 + size;
|
|
}
|
|
}
|
|
|
|
// Conversion
|
|
size_t samples = sound->frames * lovrSoundGetChannelCount(sound);
|
|
size_t bytes = sound->frames * lovrSoundGetStride(sound);
|
|
void* raw = malloc(bytes);
|
|
lovrAssert(raw, "Out of memory");
|
|
if (pcm && wav->sampleSize == 24) {
|
|
float* out = raw;
|
|
const char* in = data;
|
|
for (size_t i = 0; i < samples; i++) {
|
|
int32_t x = ((in[i * 3 + 0] << 8) | (in[i * 3 + 1] << 16) | (in[i * 3 + 2] << 24)) >> 8;
|
|
out[i] = x * (1. / 8388608.);
|
|
}
|
|
} else if (pcm && wav->sampleSize == 32) {
|
|
float* out = raw;
|
|
const int32_t* in = (const int32_t*) data;
|
|
for (size_t i = 0; i < samples; i++) {
|
|
out[i] = in[i] * (1. / 2147483648.);
|
|
}
|
|
} else {
|
|
memcpy(raw, data, bytes);
|
|
}
|
|
|
|
// Reorder/normalize Furse-Malham channels to ACN/SN3D
|
|
if (amb) {
|
|
if (sound->format == SAMPLE_I16) {
|
|
short* f = raw;
|
|
for (size_t i = 0; i < samples; i += 4) {
|
|
short tmp = f[1];
|
|
f[0] = f[0] * 1.414213562 + .5;
|
|
f[1] = f[2];
|
|
f[2] = f[3];
|
|
f[3] = tmp;
|
|
}
|
|
} else if (sound->format == SAMPLE_F32) {
|
|
float* f = raw;
|
|
for (size_t i = 0; i < samples; i += 4) {
|
|
float tmp = f[1];
|
|
f[0] = f[0] * 1.414213562f;
|
|
f[1] = f[2];
|
|
f[2] = f[3];
|
|
f[3] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
sound->blob = lovrBlobCreate(raw, bytes, blob->name);
|
|
sound->read = lovrSoundReadRaw;
|
|
return true;
|
|
}
|
|
|
|
static bool loadMP3(Sound* sound, Blob* blob, bool decode) {
|
|
if (mp3dec_detect_buf(blob->data, blob->size)) return false;
|
|
|
|
if (decode) {
|
|
mp3dec_t decoder;
|
|
mp3dec_file_info_t info;
|
|
int status = mp3dec_load_buf(&decoder, blob->data, blob->size, &info, NULL, NULL);
|
|
lovrAssert(!status, "Could not decode mp3 from '%s'", blob->name);
|
|
sound->blob = lovrBlobCreate(info.buffer, info.samples * sizeof(float), blob->name);
|
|
sound->format = SAMPLE_F32;
|
|
sound->sampleRate = info.hz;
|
|
sound->layout = info.channels == 2 ? CHANNEL_STEREO : CHANNEL_MONO;
|
|
sound->frames = info.samples / info.channels;
|
|
sound->read = lovrSoundReadRaw;
|
|
return true;
|
|
} else {
|
|
mp3dec_ex_t* decoder = sound->decoder = malloc(sizeof(mp3dec_ex_t));
|
|
lovrAssert(decoder, "Out of memory");
|
|
if (mp3dec_ex_open_buf(sound->decoder, blob->data, blob->size, MP3D_SEEK_TO_SAMPLE)) {
|
|
free(sound->decoder);
|
|
lovrThrow("Could not load mp3 from '%s'", blob->name);
|
|
}
|
|
sound->format = SAMPLE_F32;
|
|
sound->sampleRate = decoder->info.hz;
|
|
sound->layout = decoder->info.channels == 2 ? CHANNEL_STEREO : CHANNEL_MONO;
|
|
sound->frames = decoder->samples / decoder->info.channels;
|
|
sound->read = lovrSoundReadMp3;
|
|
sound->blob = blob;
|
|
lovrRetain(blob);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Sound* lovrSoundCreateFromFile(Blob* blob, bool decode) {
|
|
Sound* sound = calloc(1, sizeof(Sound));
|
|
lovrAssert(sound, "Out of memory");
|
|
sound->ref = 1;
|
|
|
|
if (loadOgg(sound, blob, decode)) return sound;
|
|
if (loadWAV(sound, blob, decode)) return sound;
|
|
if (loadMP3(sound, blob, decode)) return sound;
|
|
|
|
lovrThrow("Could not load sound from '%s': Audio format not recognized", blob->name);
|
|
}
|
|
|
|
Sound* lovrSoundCreateFromCallback(SoundCallback read, void *callbackMemo, SoundDestroyCallback callbackMemoDestroy, SampleFormat format, uint32_t sampleRate, ChannelLayout layout, uint32_t maxFrames) {
|
|
Sound* sound = calloc(1, sizeof(Sound));
|
|
lovrAssert(sound, "Out of memory");
|
|
sound->ref = 1;
|
|
sound->read = read;
|
|
sound->format = format;
|
|
sound->sampleRate = sampleRate;
|
|
sound->layout = layout;
|
|
sound->frames = maxFrames;
|
|
sound->callbackMemo = callbackMemo;
|
|
sound->callbackMemoDestroy = callbackMemoDestroy;
|
|
return sound;
|
|
}
|
|
|
|
void lovrSoundDestroy(void* ref) {
|
|
Sound* sound = (Sound*) ref;
|
|
if (sound->callbackMemoDestroy) sound->callbackMemoDestroy(sound);
|
|
lovrRelease(sound->blob, lovrBlobDestroy);
|
|
if (sound->read == lovrSoundReadOgg) stb_vorbis_close(sound->decoder);
|
|
if (sound->read == lovrSoundReadMp3) mp3dec_ex_close(sound->decoder), free(sound->decoder);
|
|
ma_pcm_rb_uninit(sound->stream);
|
|
free(sound->stream);
|
|
free(sound);
|
|
}
|
|
|
|
Blob* lovrSoundGetBlob(Sound* sound) {
|
|
return sound->blob;
|
|
}
|
|
|
|
SampleFormat lovrSoundGetFormat(Sound* sound) {
|
|
return sound->format;
|
|
}
|
|
|
|
ChannelLayout lovrSoundGetChannelLayout(Sound* sound) {
|
|
return sound->layout;
|
|
}
|
|
|
|
uint32_t lovrSoundGetChannelCount(Sound* sound) {
|
|
return 1 << sound->layout;
|
|
}
|
|
|
|
uint32_t lovrSoundGetSampleRate(Sound* sound) {
|
|
return sound->sampleRate;
|
|
}
|
|
|
|
uint32_t lovrSoundGetFrameCount(Sound* sound) {
|
|
return sound->stream ? ma_pcm_rb_available_read(sound->stream) : sound->frames;
|
|
}
|
|
|
|
uint32_t lovrSoundGetCapacity(Sound* sound) {
|
|
return sound->stream ? ma_pcm_rb_available_write(sound->stream) : sound->frames;
|
|
}
|
|
|
|
size_t lovrSoundGetStride(Sound* sound) {
|
|
return lovrSoundGetChannelCount(sound) * (sound->format == SAMPLE_I16 ? sizeof(short) : sizeof(float));
|
|
}
|
|
|
|
bool lovrSoundIsCompressed(Sound* sound) {
|
|
return sound->decoder;
|
|
}
|
|
|
|
bool lovrSoundIsStream(Sound* sound) {
|
|
return sound->stream;
|
|
}
|
|
|
|
uint32_t lovrSoundRead(Sound* sound, uint32_t offset, uint32_t count, void* data) {
|
|
return sound->read(sound, offset, count, data);
|
|
}
|
|
|
|
uint32_t lovrSoundWrite(Sound* sound, uint32_t offset, uint32_t count, const void* data) {
|
|
lovrAssert(!sound->decoder, "Compressed Sound can not be written to");
|
|
lovrAssert(sound->stream || sound->blob, "Live-generated sound can not be written to");
|
|
size_t stride = lovrSoundGetStride(sound);
|
|
uint32_t frames = 0;
|
|
|
|
if (sound->stream) {
|
|
const char* bytes = data;
|
|
while (frames < count) {
|
|
void* pointer;
|
|
uint32_t chunk = count - frames;
|
|
ma_pcm_rb_acquire_write(sound->stream, &chunk, &pointer);
|
|
memcpy(pointer, bytes, chunk * stride);
|
|
ma_pcm_rb_commit_write(sound->stream, chunk, pointer);
|
|
if (chunk == 0) break;
|
|
bytes += chunk * stride;
|
|
frames += chunk;
|
|
}
|
|
} else {
|
|
count = MIN(count, sound->frames - offset);
|
|
memcpy((char*) sound->blob->data + offset * stride, data, count * stride);
|
|
frames = count;
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
|
|
uint32_t lovrSoundCopy(Sound* src, Sound* dst, uint32_t count, uint32_t srcOffset, uint32_t dstOffset) {
|
|
lovrAssert(!dst->decoder, "Compressed Sound can not be written to");
|
|
lovrAssert(dst->stream || dst->blob, "Live-generated sound can not be written to");
|
|
lovrAssert(src != dst, "Can not copy a Sound to itself");
|
|
lovrAssert(src->format == dst->format, "Sound formats need to match");
|
|
lovrAssert(src->layout == dst->layout, "Sound channel layouts need to match");
|
|
uint32_t frames = 0;
|
|
|
|
if (dst->stream) {
|
|
while (frames < count) {
|
|
void* data;
|
|
uint32_t available = count - frames;
|
|
ma_pcm_rb_acquire_write(dst->stream, &available, &data);
|
|
uint32_t read = src->read(src, srcOffset + frames, available, data);
|
|
ma_pcm_rb_commit_write(dst->stream, read, data);
|
|
if (read == 0) break;
|
|
frames += read;
|
|
}
|
|
} else {
|
|
count = MIN(count, dst->frames - dstOffset);
|
|
size_t stride = lovrSoundGetStride(src);
|
|
char* data = (char*) dst->blob->data + dstOffset * stride;
|
|
while (frames < count) {
|
|
uint32_t read = src->read(src, srcOffset + frames, count - frames, data);
|
|
if (read == 0) break;
|
|
data += read * stride;
|
|
frames += read;
|
|
}
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
|
|
void *lovrSoundGetCallbackMemo(Sound* sound) {
|
|
return sound->callbackMemo;
|
|
}
|