Added lib/cmixer and new audio system
This commit is contained in:
727
src/lib/cmixer/cmixer.c
Normal file
727
src/lib/cmixer/cmixer.c
Normal file
@@ -0,0 +1,727 @@
|
||||
/*
|
||||
** Copyright (c) 2017 rxi
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
** of this software and associated documentation files (the "Software"), to
|
||||
** deal in the Software without restriction, including without limitation the
|
||||
** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
** sell copies of the Software, and to permit persons to whom the Software is
|
||||
** furnished to do so, subject to the following conditions:
|
||||
**
|
||||
** The above copyright notice and this permission notice shall be included in
|
||||
** all copies or substantial portions of the Software.
|
||||
**
|
||||
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
** IN THE SOFTWARE.
|
||||
**/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cmixer.h"
|
||||
|
||||
#define UNUSED(x) ((void) (x))
|
||||
#define CLAMP(x, a, b) ((x) < (a) ? (a) : (x) > (b) ? (b) : (x))
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define MAX(a, b) ((a) > (b) ? (a) : (b))
|
||||
|
||||
#define FX_BITS (12)
|
||||
#define FX_UNIT (1 << FX_BITS)
|
||||
#define FX_MASK (FX_UNIT - 1)
|
||||
#define FX_FROM_FLOAT(f) ((f) * FX_UNIT)
|
||||
#define FX_LERP(a, b, p) ((a) + ((((b) - (a)) * (p)) >> FX_BITS))
|
||||
|
||||
#define BUFFER_SIZE (512)
|
||||
#define BUFFER_MASK (BUFFER_SIZE - 1)
|
||||
|
||||
|
||||
struct cm_Source {
|
||||
cm_Source *next; /* Next source in list */
|
||||
cm_Int16 buffer[BUFFER_SIZE]; /* Internal buffer with raw stereo PCM */
|
||||
cm_EventHandler handler; /* Event handler */
|
||||
void *udata; /* Stream's udata (from cm_SourceInfo) */
|
||||
int samplerate; /* Stream's native samplerate */
|
||||
int length; /* Stream's length in frames */
|
||||
int end; /* End index for the current play-through */
|
||||
int state; /* Current state (playing|paused|stopped) */
|
||||
cm_Int64 position; /* Current playhead position (fixed point) */
|
||||
int lgain, rgain; /* Left and right gain (fixed point) */
|
||||
int rate; /* Playback rate (fixed point) */
|
||||
int nextfill; /* Next frame idx where the buffer needs to be filled */
|
||||
int loop; /* Whether the source will loop when `end` is reached */
|
||||
int rewind; /* Whether the source will rewind before playing */
|
||||
int active; /* Whether the source is part of `sources` list */
|
||||
double gain; /* Gain set by `cm_set_gain()` */
|
||||
double pan; /* Pan set by `cm_set_pan()` */
|
||||
};
|
||||
|
||||
|
||||
static struct {
|
||||
const char *lasterror; /* Last error message */
|
||||
cm_EventHandler lock; /* Event handler for lock/unlock events */
|
||||
cm_Source *sources; /* Linked list of active (playing) sources */
|
||||
cm_Int32 buffer[BUFFER_SIZE]; /* Internal master buffer */
|
||||
int samplerate; /* Master samplerate */
|
||||
int gain; /* Master gain (fixed point) */
|
||||
} cmixer;
|
||||
|
||||
|
||||
static void dummy_handler(cm_Event *e) {
|
||||
UNUSED(e);
|
||||
}
|
||||
|
||||
|
||||
static void lock(void) {
|
||||
cm_Event e;
|
||||
e.type = CM_EVENT_LOCK;
|
||||
cmixer.lock(&e);
|
||||
}
|
||||
|
||||
|
||||
static void unlock(void) {
|
||||
cm_Event e;
|
||||
e.type = CM_EVENT_UNLOCK;
|
||||
cmixer.lock(&e);
|
||||
}
|
||||
|
||||
|
||||
const char* cm_get_error(void) {
|
||||
const char *res = cmixer.lasterror;
|
||||
cmixer.lasterror = NULL;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
static const char* error(const char *msg) {
|
||||
cmixer.lasterror = msg;
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
||||
void cm_init(int samplerate) {
|
||||
cmixer.samplerate = samplerate;
|
||||
cmixer.lock = dummy_handler;
|
||||
cmixer.sources = NULL;
|
||||
cmixer.gain = FX_UNIT;
|
||||
}
|
||||
|
||||
|
||||
void cm_set_lock(cm_EventHandler lock) {
|
||||
cmixer.lock = lock;
|
||||
}
|
||||
|
||||
|
||||
void cm_set_master_gain(double gain) {
|
||||
cmixer.gain = FX_FROM_FLOAT(gain);
|
||||
}
|
||||
|
||||
|
||||
static void rewind_source(cm_Source *src) {
|
||||
cm_Event e;
|
||||
e.type = CM_EVENT_REWIND;
|
||||
e.udata = src->udata;
|
||||
src->handler(&e);
|
||||
src->position = 0;
|
||||
src->rewind = 0;
|
||||
src->end = src->length;
|
||||
src->nextfill = 0;
|
||||
}
|
||||
|
||||
|
||||
static void fill_source_buffer(cm_Source *src, int offset, int length) {
|
||||
cm_Event e;
|
||||
e.type = CM_EVENT_SAMPLES;
|
||||
e.udata = src->udata;
|
||||
e.buffer = src->buffer + offset;
|
||||
e.length = length;
|
||||
src->handler(&e);
|
||||
}
|
||||
|
||||
|
||||
static void process_source(cm_Source *src, int len) {
|
||||
int i, n, a, b, p;
|
||||
int frame, count;
|
||||
cm_Int32 *dst = cmixer.buffer;
|
||||
|
||||
/* Do rewind if flag is set */
|
||||
if (src->rewind) {
|
||||
rewind_source(src);
|
||||
}
|
||||
|
||||
/* Process audio */
|
||||
while (len > 0) {
|
||||
/* Get current position frame */
|
||||
frame = src->position >> FX_BITS;
|
||||
|
||||
/* Fill buffer if required */
|
||||
if (frame + 3 >= src->nextfill) {
|
||||
fill_source_buffer(src, (src->nextfill*2) & BUFFER_MASK, BUFFER_SIZE/2);
|
||||
src->nextfill += BUFFER_SIZE / 4;
|
||||
}
|
||||
|
||||
/* Handle reaching the end of the playthrough */
|
||||
if (frame >= src->end) {
|
||||
/* As streams continiously fill the raw buffer in a loop we simply
|
||||
** increment the end idx by one length and continue reading from it for
|
||||
** another play-through */
|
||||
src->end = frame + src->length;
|
||||
/* Set state and stop processing if we're not set to loop */
|
||||
if (!src->loop) {
|
||||
src->state = CM_STATE_STOPPED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Work out how many frames we should process in the loop */
|
||||
n = MIN(src->nextfill - 2, src->end) - frame;
|
||||
count = (n << FX_BITS) / src->rate;
|
||||
count = MAX(count, 1);
|
||||
count = MIN(count, len / 2);
|
||||
len -= count * 2;
|
||||
|
||||
/* Add audio to master buffer */
|
||||
if (src->rate == FX_UNIT) {
|
||||
/* Add audio to buffer -- basic */
|
||||
n = frame * 2;
|
||||
for (i = 0; i < count; i++) {
|
||||
dst[0] += (src->buffer[(n ) & BUFFER_MASK] * src->lgain) >> FX_BITS;
|
||||
dst[1] += (src->buffer[(n + 1) & BUFFER_MASK] * src->rgain) >> FX_BITS;
|
||||
n += 2;
|
||||
dst += 2;
|
||||
}
|
||||
src->position += count * FX_UNIT;
|
||||
|
||||
} else {
|
||||
/* Add audio to buffer -- interpolated */
|
||||
for (i = 0; i < count; i++) {
|
||||
n = (src->position >> FX_BITS) * 2;
|
||||
p = src->position & FX_MASK;
|
||||
a = src->buffer[(n ) & BUFFER_MASK];
|
||||
b = src->buffer[(n + 2) & BUFFER_MASK];
|
||||
dst[0] += (FX_LERP(a, b, p) * src->lgain) >> FX_BITS;
|
||||
n++;
|
||||
a = src->buffer[(n ) & BUFFER_MASK];
|
||||
b = src->buffer[(n + 2) & BUFFER_MASK];
|
||||
dst[1] += (FX_LERP(a, b, p) * src->rgain) >> FX_BITS;
|
||||
src->position += src->rate;
|
||||
dst += 2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void cm_process(cm_Int16 *dst, int len) {
|
||||
int i;
|
||||
cm_Source **s;
|
||||
|
||||
/* Process in chunks of BUFFER_SIZE if `len` is larger than BUFFER_SIZE */
|
||||
while (len > BUFFER_SIZE) {
|
||||
cm_process(dst, BUFFER_SIZE);
|
||||
dst += BUFFER_SIZE;
|
||||
len -= BUFFER_SIZE;
|
||||
}
|
||||
|
||||
/* Zeroset internal buffer */
|
||||
memset(cmixer.buffer, 0, len * sizeof(cmixer.buffer[0]));
|
||||
|
||||
/* Process active sources */
|
||||
lock();
|
||||
s = &cmixer.sources;
|
||||
while (*s) {
|
||||
process_source(*s, len);
|
||||
/* Remove source from list if it is no longer playing */
|
||||
if ((*s)->state != CM_STATE_PLAYING) {
|
||||
(*s)->active = 0;
|
||||
*s = (*s)->next;
|
||||
} else {
|
||||
s = &(*s)->next;
|
||||
}
|
||||
}
|
||||
unlock();
|
||||
|
||||
/* Copy internal buffer to destination and clip */
|
||||
for (i = 0; i < len; i++) {
|
||||
int x = (cmixer.buffer[i] * cmixer.gain) >> FX_BITS;
|
||||
dst[i] = CLAMP(x, -32768, 32767);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
cm_Source* cm_new_source(const cm_SourceInfo *info) {
|
||||
cm_Source *src = calloc(1, sizeof(*src));
|
||||
if (!src) {
|
||||
error("allocation failed");
|
||||
return NULL;
|
||||
}
|
||||
src->handler = info->handler;
|
||||
src->length = info->length;
|
||||
src->samplerate = info->samplerate;
|
||||
src->udata = info->udata;
|
||||
cm_set_gain(src, 1);
|
||||
cm_set_pan(src, 0);
|
||||
cm_set_pitch(src, 1);
|
||||
cm_set_loop(src, 0);
|
||||
cm_stop(src);
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
static const char* wav_init(cm_SourceInfo *info, void *data, int len, int ownsdata);
|
||||
|
||||
#ifdef CM_USE_STB_VORBIS
|
||||
static const char* ogg_init(cm_SourceInfo *info, void *data, int len, int ownsdata);
|
||||
#endif
|
||||
|
||||
|
||||
static int check_header(void *data, int size, char *str, int offset) {
|
||||
int len = strlen(str);
|
||||
return (size >= offset + len) && !memcmp((char*) data + offset, str, len);
|
||||
}
|
||||
|
||||
|
||||
static cm_Source* new_source_from_mem(void *data, int size, int ownsdata) {
|
||||
const char *err;
|
||||
cm_SourceInfo info;
|
||||
|
||||
if (check_header(data, size, "WAVE", 8)) {
|
||||
err = wav_init(&info, data, size, ownsdata);
|
||||
if (err) {
|
||||
return NULL;
|
||||
}
|
||||
return cm_new_source(&info);
|
||||
}
|
||||
|
||||
#ifdef CM_USE_STB_VORBIS
|
||||
if (check_header(data, size, "OggS", 0)) {
|
||||
err = ogg_init(&info, data, size, ownsdata);
|
||||
if (err) {
|
||||
return NULL;
|
||||
}
|
||||
return cm_new_source(&info);
|
||||
}
|
||||
#endif
|
||||
|
||||
error("unknown format or invalid data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static void* load_file(const char *filename, int *size) {
|
||||
FILE *fp;
|
||||
void *data;
|
||||
int n;
|
||||
|
||||
fp = fopen(filename, "rb");
|
||||
if (!fp) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Get size */
|
||||
fseek(fp, 0, SEEK_END);
|
||||
*size = ftell(fp);
|
||||
rewind(fp);
|
||||
|
||||
/* Malloc, read and return data */
|
||||
data = malloc(*size);
|
||||
if (!data) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
n = fread(data, 1, *size, fp);
|
||||
fclose(fp);
|
||||
if (n != *size) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
cm_Source* cm_new_source_from_file(const char *filename) {
|
||||
int size;
|
||||
cm_Source *src;
|
||||
void *data;
|
||||
|
||||
/* Load file into memory */
|
||||
data = load_file(filename, &size);
|
||||
if (!data) {
|
||||
error("could not load file");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Try to load and return */
|
||||
src = new_source_from_mem(data, size, 1);
|
||||
if (!src) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
cm_Source* cm_new_source_from_mem(void *data, int size) {
|
||||
return new_source_from_mem(data, size, 0);
|
||||
}
|
||||
|
||||
|
||||
void cm_destroy_source(cm_Source *src) {
|
||||
cm_Event e;
|
||||
lock();
|
||||
if (src->active) {
|
||||
cm_Source **s = &cmixer.sources;
|
||||
while (*s) {
|
||||
if (*s == src) {
|
||||
*s = src->next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
unlock();
|
||||
e.type = CM_EVENT_DESTROY;
|
||||
e.udata = src->udata;
|
||||
src->handler(&e);
|
||||
free(src);
|
||||
}
|
||||
|
||||
|
||||
double cm_get_length(cm_Source *src) {
|
||||
return src->length / (double) src->samplerate;
|
||||
}
|
||||
|
||||
|
||||
double cm_get_position(cm_Source *src) {
|
||||
return ((src->position >> FX_BITS) % src->length) / (double) src->samplerate;
|
||||
}
|
||||
|
||||
|
||||
int cm_get_state(cm_Source *src) {
|
||||
return src->state;
|
||||
}
|
||||
|
||||
|
||||
static void recalc_source_gains(cm_Source *src) {
|
||||
double l, r;
|
||||
double pan = src->pan;
|
||||
l = src->gain * (pan <= 0. ? 1. : 1. - pan);
|
||||
r = src->gain * (pan >= 0. ? 1. : 1. + pan);
|
||||
src->lgain = FX_FROM_FLOAT(l);
|
||||
src->rgain = FX_FROM_FLOAT(r);
|
||||
}
|
||||
|
||||
|
||||
void cm_set_gain(cm_Source *src, double gain) {
|
||||
src->gain = gain;
|
||||
recalc_source_gains(src);
|
||||
}
|
||||
|
||||
|
||||
void cm_set_pan(cm_Source *src, double pan) {
|
||||
src->pan = pan;
|
||||
recalc_source_gains(src);
|
||||
}
|
||||
|
||||
|
||||
void cm_set_pitch(cm_Source *src, double pitch) {
|
||||
double rate = src->samplerate / (double) cmixer.samplerate * pitch;
|
||||
src->rate = FX_FROM_FLOAT(rate);
|
||||
}
|
||||
|
||||
|
||||
void cm_set_loop(cm_Source *src, int loop) {
|
||||
src->loop = loop;
|
||||
}
|
||||
|
||||
|
||||
void cm_play(cm_Source *src) {
|
||||
lock();
|
||||
src->state = CM_STATE_PLAYING;
|
||||
if (!src->active) {
|
||||
src->active = 1;
|
||||
src->next = cmixer.sources;
|
||||
cmixer.sources = src;
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
|
||||
void cm_pause(cm_Source *src) {
|
||||
src->state = CM_STATE_PAUSED;
|
||||
}
|
||||
|
||||
|
||||
void cm_stop(cm_Source *src) {
|
||||
src->state = CM_STATE_STOPPED;
|
||||
src->rewind = 1;
|
||||
}
|
||||
|
||||
|
||||
/*============================================================================
|
||||
** Wav stream
|
||||
**============================================================================*/
|
||||
|
||||
typedef struct {
|
||||
void *data;
|
||||
int bitdepth;
|
||||
int samplerate;
|
||||
int channels;
|
||||
int length;
|
||||
} Wav;
|
||||
|
||||
typedef struct {
|
||||
Wav wav;
|
||||
void *data;
|
||||
int idx;
|
||||
} WavStream;
|
||||
|
||||
|
||||
static char* find_subchunk(char *data, int len, char *id, int *size) {
|
||||
/* TODO : Error handling on malformed wav file */
|
||||
int idlen = strlen(id);
|
||||
char *p = data + 12;
|
||||
next:
|
||||
*size = *((cm_UInt32*) (p + 4));
|
||||
if (memcmp(p, id, idlen)) {
|
||||
p += 8 + *size;
|
||||
if (p > data + len) return NULL;
|
||||
goto next;
|
||||
}
|
||||
return p + 8;
|
||||
}
|
||||
|
||||
|
||||
static const char* read_wav(Wav *w, void *data, int len) {
|
||||
int bitdepth, channels, samplerate, format;
|
||||
int sz;
|
||||
char *p = data;
|
||||
memset(w, 0, sizeof(*w));
|
||||
|
||||
/* Check header */
|
||||
if (memcmp(p, "RIFF", 4) || memcmp(p + 8, "WAVE", 4)) {
|
||||
return error("bad wav header");
|
||||
}
|
||||
/* Find fmt subchunk */
|
||||
p = find_subchunk(data, len, "fmt", &sz);
|
||||
if (!p) {
|
||||
return error("no fmt subchunk");
|
||||
}
|
||||
|
||||
/* Load fmt info */
|
||||
format = *((cm_UInt16*) (p));
|
||||
channels = *((cm_UInt16*) (p + 2));
|
||||
samplerate = *((cm_UInt32*) (p + 4));
|
||||
bitdepth = *((cm_UInt16*) (p + 14));
|
||||
if (format != 1) {
|
||||
return error("unsupported format");
|
||||
}
|
||||
if (channels == 0 || samplerate == 0 || bitdepth == 0) {
|
||||
return error("bad format");
|
||||
}
|
||||
|
||||
/* Find data subchunk */
|
||||
p = find_subchunk(data, len, "data", &sz);
|
||||
if (!p) {
|
||||
return error("no data subchunk");
|
||||
}
|
||||
|
||||
/* Init struct */
|
||||
w->data = (void*) p;
|
||||
w->samplerate = samplerate;
|
||||
w->channels = channels;
|
||||
w->length = (sz / (bitdepth / 8)) / channels;
|
||||
w->bitdepth = bitdepth;
|
||||
/* Done */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
#define WAV_PROCESS_LOOP(X) \
|
||||
while (n--) { \
|
||||
X \
|
||||
dst += 2; \
|
||||
s->idx++; \
|
||||
}
|
||||
|
||||
static void wav_handler(cm_Event *e) {
|
||||
int x, n;
|
||||
cm_Int16 *dst;
|
||||
WavStream *s = e->udata;
|
||||
int len;
|
||||
|
||||
switch (e->type) {
|
||||
|
||||
case CM_EVENT_DESTROY:
|
||||
free(s->data);
|
||||
free(s);
|
||||
break;
|
||||
|
||||
case CM_EVENT_SAMPLES:
|
||||
dst = e->buffer;
|
||||
len = e->length / 2;
|
||||
fill:
|
||||
n = MIN(len, s->wav.length - s->idx);
|
||||
len -= n;
|
||||
if (s->wav.bitdepth == 16 && s->wav.channels == 1) {
|
||||
WAV_PROCESS_LOOP({
|
||||
dst[0] = dst[1] = ((cm_Int16*) s->wav.data)[s->idx];
|
||||
});
|
||||
} else if (s->wav.bitdepth == 16 && s->wav.channels == 2) {
|
||||
WAV_PROCESS_LOOP({
|
||||
x = s->idx * 2;
|
||||
dst[0] = ((cm_Int16*) s->wav.data)[x ];
|
||||
dst[1] = ((cm_Int16*) s->wav.data)[x + 1];
|
||||
});
|
||||
} else if (s->wav.bitdepth == 8 && s->wav.channels == 1) {
|
||||
WAV_PROCESS_LOOP({
|
||||
dst[0] = dst[1] = (((cm_UInt8*) s->wav.data)[s->idx] - 128) << 8;
|
||||
});
|
||||
} else if (s->wav.bitdepth == 8 && s->wav.channels == 2) {
|
||||
WAV_PROCESS_LOOP({
|
||||
x = s->idx * 2;
|
||||
dst[0] = (((cm_UInt8*) s->wav.data)[x ] - 128) << 8;
|
||||
dst[1] = (((cm_UInt8*) s->wav.data)[x + 1] - 128) << 8;
|
||||
});
|
||||
}
|
||||
/* Loop back and continue filling buffer if we didn't fill the buffer */
|
||||
if (len > 0) {
|
||||
s->idx = 0;
|
||||
goto fill;
|
||||
}
|
||||
break;
|
||||
|
||||
case CM_EVENT_REWIND:
|
||||
s->idx = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const char* wav_init(cm_SourceInfo *info, void *data, int len, int ownsdata) {
|
||||
WavStream *stream;
|
||||
Wav wav;
|
||||
|
||||
const char *err = read_wav(&wav, data, len);
|
||||
if (err != NULL) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (wav.channels > 2 || (wav.bitdepth != 16 && wav.bitdepth != 8)) {
|
||||
return error("unsupported wav format");
|
||||
}
|
||||
|
||||
stream = calloc(1, sizeof(*stream));
|
||||
if (!stream) {
|
||||
return error("allocation failed");
|
||||
}
|
||||
stream->wav = wav;
|
||||
|
||||
if (ownsdata) {
|
||||
stream->data = data;
|
||||
}
|
||||
stream->idx = 0;
|
||||
|
||||
info->udata = stream;
|
||||
info->handler = wav_handler;
|
||||
info->samplerate = wav.samplerate;
|
||||
info->length = wav.length;
|
||||
|
||||
/* Return NULL (no error) for success */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/*============================================================================
|
||||
** Ogg stream
|
||||
**============================================================================*/
|
||||
|
||||
#ifdef CM_USE_STB_VORBIS
|
||||
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
#include "stb_vorbis.c"
|
||||
|
||||
typedef struct {
|
||||
stb_vorbis *ogg;
|
||||
void *data;
|
||||
} OggStream;
|
||||
|
||||
|
||||
static void ogg_handler(cm_Event *e) {
|
||||
int n, len;
|
||||
OggStream *s = e->udata;
|
||||
cm_Int16 *buf;
|
||||
|
||||
switch (e->type) {
|
||||
|
||||
case CM_EVENT_DESTROY:
|
||||
stb_vorbis_close(s->ogg);
|
||||
free(s->data);
|
||||
free(s);
|
||||
break;
|
||||
|
||||
case CM_EVENT_SAMPLES:
|
||||
len = e->length;
|
||||
buf = e->buffer;
|
||||
fill:
|
||||
n = stb_vorbis_get_samples_short_interleaved(s->ogg, 2, buf, len);
|
||||
n *= 2;
|
||||
/* rewind and fill remaining buffer if we reached the end of the ogg
|
||||
** before filling it */
|
||||
if (len != n) {
|
||||
stb_vorbis_seek_start(s->ogg);
|
||||
buf += n;
|
||||
len -= n;
|
||||
goto fill;
|
||||
}
|
||||
break;
|
||||
|
||||
case CM_EVENT_REWIND:
|
||||
stb_vorbis_seek_start(s->ogg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const char* ogg_init(cm_SourceInfo *info, void *data, int len, int ownsdata) {
|
||||
OggStream *stream;
|
||||
stb_vorbis *ogg;
|
||||
stb_vorbis_info ogginfo;
|
||||
int err;
|
||||
|
||||
ogg = stb_vorbis_open_memory(data, len, &err, NULL);
|
||||
if (!ogg) {
|
||||
return error("invalid ogg data");
|
||||
}
|
||||
|
||||
stream = calloc(1, sizeof(*stream));
|
||||
if (!stream) {
|
||||
stb_vorbis_close(ogg);
|
||||
return error("allocation failed");
|
||||
}
|
||||
|
||||
stream->ogg = ogg;
|
||||
if (ownsdata) {
|
||||
stream->data = data;
|
||||
}
|
||||
|
||||
ogginfo = stb_vorbis_get_info(ogg);
|
||||
|
||||
info->udata = stream;
|
||||
info->handler = ogg_handler;
|
||||
info->samplerate = ogginfo.sample_rate;
|
||||
info->length = stb_vorbis_stream_length_in_samples(ogg);
|
||||
|
||||
/* Return NULL (no error) for success */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
76
src/lib/cmixer/cmixer.h
Normal file
76
src/lib/cmixer/cmixer.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
** Copyright (c) 2017 rxi
|
||||
**
|
||||
** This library is free software; you can redistribute it and/or modify it
|
||||
** under the terms of the MIT license. See `cmixer.c` for details.
|
||||
**/
|
||||
|
||||
#ifndef CMIXER_H
|
||||
#define CMIXER_H
|
||||
|
||||
#define CM_VERSION "0.1.0"
|
||||
|
||||
typedef short cm_Int16;
|
||||
typedef int cm_Int32;
|
||||
typedef long long cm_Int64;
|
||||
typedef unsigned char cm_UInt8;
|
||||
typedef unsigned short cm_UInt16;
|
||||
typedef unsigned cm_UInt32;
|
||||
|
||||
typedef struct cm_Source cm_Source;
|
||||
|
||||
typedef struct {
|
||||
int type;
|
||||
void *udata;
|
||||
const char *msg;
|
||||
cm_Int16 *buffer;
|
||||
int length;
|
||||
} cm_Event;
|
||||
|
||||
typedef void (*cm_EventHandler)(cm_Event *e);
|
||||
|
||||
typedef struct {
|
||||
cm_EventHandler handler;
|
||||
void *udata;
|
||||
int samplerate;
|
||||
int length;
|
||||
} cm_SourceInfo;
|
||||
|
||||
|
||||
enum {
|
||||
CM_STATE_STOPPED,
|
||||
CM_STATE_PLAYING,
|
||||
CM_STATE_PAUSED
|
||||
};
|
||||
|
||||
enum {
|
||||
CM_EVENT_LOCK,
|
||||
CM_EVENT_UNLOCK,
|
||||
CM_EVENT_DESTROY,
|
||||
CM_EVENT_SAMPLES,
|
||||
CM_EVENT_REWIND
|
||||
};
|
||||
|
||||
|
||||
const char* cm_get_error(void);
|
||||
void cm_init(int samplerate);
|
||||
void cm_set_lock(cm_EventHandler lock);
|
||||
void cm_set_master_gain(double gain);
|
||||
void cm_process(cm_Int16 *dst, int len);
|
||||
|
||||
cm_Source* cm_new_source(const cm_SourceInfo *info);
|
||||
cm_Source* cm_new_source_from_file(const char *filename);
|
||||
cm_Source* cm_new_source_from_mem(void *data, int size);
|
||||
void cm_destroy_source(cm_Source *src);
|
||||
double cm_get_length(cm_Source *src);
|
||||
double cm_get_position(cm_Source *src);
|
||||
int cm_get_state(cm_Source *src);
|
||||
void cm_set_gain(cm_Source *src, double gain);
|
||||
void cm_set_pan(cm_Source *src, double pan);
|
||||
void cm_set_pitch(cm_Source *src, double pitch);
|
||||
void cm_set_loop(cm_Source *src, int loop);
|
||||
void cm_play(cm_Source *src);
|
||||
void cm_pause(cm_Source *src);
|
||||
void cm_stop(cm_Source *src);
|
||||
|
||||
#endif
|
||||
@@ -25,6 +25,7 @@ typedef struct {
|
||||
#define LUAOBJ_TYPE_IMAGE (1 << 0)
|
||||
#define LUAOBJ_TYPE_QUAD (1 << 1)
|
||||
#define LUAOBJ_TYPE_FONT (1 << 2)
|
||||
#define LUAOBJ_TYPE_SOURCE (1 << 3)
|
||||
|
||||
|
||||
int luaobj_newclass(lua_State *L, const char *name, const char *extends,
|
||||
|
||||
20
src/main.c
20
src/main.c
@@ -12,6 +12,7 @@
|
||||
#include <dos.h>
|
||||
|
||||
#include "lib/dmt/dmt.h"
|
||||
#include "lib/cmixer/cmixer.h"
|
||||
#include "vga.h"
|
||||
#include "luaobj.h"
|
||||
#include "keyboard.h"
|
||||
@@ -46,6 +47,22 @@ static int onLuaPanic(lua_State *L) {
|
||||
}
|
||||
|
||||
|
||||
static short audioBuffer[SOUNDBLASTER_SAMPLES_PER_BUFFER * 2];
|
||||
|
||||
static const short* audioCallback(void) {
|
||||
/* For the moment the soundblaster code expects mono audio while the cmixer
|
||||
** library outputs stereo -- we process to a stereo buffer, then copy the left
|
||||
** channel to the start of the buffer */
|
||||
int i;
|
||||
int len = SOUNDBLASTER_SAMPLES_PER_BUFFER;
|
||||
cm_process(audioBuffer, len * 2);
|
||||
for (i = 0; i < len; i++) {
|
||||
audioBuffer[i] = audioBuffer[i * 2];
|
||||
}
|
||||
return audioBuffer;
|
||||
}
|
||||
|
||||
|
||||
int luaopen_love(lua_State *L);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
@@ -57,7 +74,8 @@ int main(int argc, char **argv) {
|
||||
|
||||
/* Init everything */
|
||||
atexit(deinit);
|
||||
// soundblaster_init(mixer_getNextBlock);
|
||||
cm_init(soundblaster_getSampleRate());
|
||||
soundblaster_init(audioCallback);
|
||||
vga_init();
|
||||
palette_init();
|
||||
keyboard_init();
|
||||
|
||||
20
src/modules/l_audio.c
Normal file
20
src/modules/l_audio.c
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2017 rxi
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the MIT license. See LICENSE for details.
|
||||
*/
|
||||
|
||||
#include "luaobj.h"
|
||||
|
||||
|
||||
int l_source_new(lua_State *L);
|
||||
|
||||
int luaopen_audio(lua_State *L) {
|
||||
luaL_Reg reg[] = {
|
||||
{ "newSource", l_source_new },
|
||||
{ 0, 0 },
|
||||
};
|
||||
luaL_newlib(L, reg);
|
||||
return 1;
|
||||
}
|
||||
@@ -19,10 +19,12 @@ int l_love_getVersion(lua_State *L) {
|
||||
int luaopen_image(lua_State *L);
|
||||
int luaopen_quad(lua_State *L);
|
||||
int luaopen_font(lua_State *L);
|
||||
int luaopen_source(lua_State *L);
|
||||
int luaopen_system(lua_State *L);
|
||||
int luaopen_event(lua_State *L);
|
||||
int luaopen_filesystem(lua_State *L);
|
||||
int luaopen_graphics(lua_State *L);
|
||||
int luaopen_audio(lua_State *L);
|
||||
int luaopen_timer(lua_State *L);
|
||||
int luaopen_keyboard(lua_State *L);
|
||||
int luaopen_mouse(lua_State *L);
|
||||
@@ -36,6 +38,7 @@ int luaopen_love(lua_State *L) {
|
||||
luaopen_image,
|
||||
luaopen_quad,
|
||||
luaopen_font,
|
||||
luaopen_source,
|
||||
NULL,
|
||||
};
|
||||
for (i = 0; classes[i]; i++) {
|
||||
@@ -56,6 +59,7 @@ int luaopen_love(lua_State *L) {
|
||||
{ "event", luaopen_event },
|
||||
{ "filesystem", luaopen_filesystem },
|
||||
{ "graphics", luaopen_graphics },
|
||||
{ "audio", luaopen_audio },
|
||||
{ "timer", luaopen_timer },
|
||||
{ "keyboard", luaopen_keyboard },
|
||||
{ "mouse", luaopen_mouse },
|
||||
|
||||
155
src/modules/l_source.c
Normal file
155
src/modules/l_source.c
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Copyright (c) 2017 rxi
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the MIT license. See LICENSE for details.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "lib/cmixer/cmixer.h"
|
||||
#include "filesystem.h"
|
||||
#include "luaobj.h"
|
||||
|
||||
|
||||
#define CLASS_TYPE LUAOBJ_TYPE_SOURCE
|
||||
#define CLASS_NAME "Source"
|
||||
|
||||
|
||||
typedef struct {
|
||||
cm_Source *source;
|
||||
void *data;
|
||||
} source_t;
|
||||
|
||||
|
||||
int l_source_new(lua_State *L) {
|
||||
const char *filename = luaL_checkstring(L, 1);
|
||||
/* Create object */
|
||||
source_t *self = luaobj_newudata(L, sizeof(*self));
|
||||
luaobj_setclass(L, CLASS_TYPE, CLASS_NAME);
|
||||
memset(self, 0, sizeof(*self));
|
||||
/* Load file */
|
||||
int size;
|
||||
self->data = filesystem_read(filename, &size);
|
||||
if (!self->data) {
|
||||
luaL_error(L, "could not open file");
|
||||
}
|
||||
/* Init source */
|
||||
self->source = cm_new_source_from_mem(self->data, size);
|
||||
if (!self->source) {
|
||||
luaL_error(L, "%s", cm_get_error());
|
||||
}
|
||||
/* Return object */
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int l_source_gc(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
if (self->source) cm_destroy_source(self->source);
|
||||
if (self->data) filesystem_free(self->data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int l_source_setVolume(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
double n = luaL_checknumber(L, 2);
|
||||
cm_set_gain(self->source, n);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int l_source_setPitch(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
double n = luaL_checknumber(L, 2);
|
||||
cm_set_pitch(self->source, n);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int l_source_setLooping(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
int enable = lua_toboolean(L, 2);
|
||||
cm_set_loop(self->source, enable);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int l_source_getDuration(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
double n = cm_get_length(self->source);
|
||||
lua_pushnumber(L, n);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int l_source_isPlaying(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
lua_pushboolean(L, cm_get_state(self->source) == CM_STATE_PLAYING);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int l_source_isPaused(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
lua_pushboolean(L, cm_get_state(self->source) == CM_STATE_PAUSED);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int l_source_isStopped(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
lua_pushboolean(L, cm_get_state(self->source) == CM_STATE_STOPPED);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int l_source_tell(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
double n = cm_get_position(self->source);
|
||||
lua_pushnumber(L, n);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int l_source_play(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
cm_play(self->source);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int l_source_pause(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
cm_pause(self->source);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int l_source_stop(lua_State *L) {
|
||||
source_t *self = luaobj_checkudata(L, 1, CLASS_TYPE);
|
||||
cm_stop(self->source);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int luaopen_source(lua_State *L) {
|
||||
luaL_Reg reg[] = {
|
||||
{ "new", l_source_new },
|
||||
{ "__gc", l_source_gc },
|
||||
{ "setVolume", l_source_setVolume },
|
||||
{ "setPitch", l_source_setPitch },
|
||||
{ "setLooping", l_source_setLooping },
|
||||
{ "getDuration", l_source_getDuration },
|
||||
{ "isPlaying", l_source_isPlaying },
|
||||
{ "isPaused", l_source_isPaused },
|
||||
{ "isStopped", l_source_isStopped },
|
||||
{ "tell", l_source_tell },
|
||||
{ "play", l_source_play },
|
||||
{ "pause", l_source_pause },
|
||||
{ "stop", l_source_stop },
|
||||
{ 0, 0 },
|
||||
};
|
||||
luaobj_newclass(L, CLASS_NAME, NULL, l_source_new, reg);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user