summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkdx <kikoodx@paranoici.org>2023-01-15 01:11:59 +0100
committerkdx <kikoodx@paranoici.org>2023-01-15 01:11:59 +0100
commitff9144048a2db6d443b2f5c17578ca7fdbae796d (patch)
treec26696fe8479c862c11545d886c4774825e01fc5
download005-ff9144048a2db6d443b2f5c17578ca7fdbae796d.tar.gz
initial commit
-rw-r--r--.gitignore3
-rw-r--r--Makefile63
-rw-r--r--cfg.h20
-rw-r--r--cmixer.c757
-rw-r--r--cmixer.h70
-rw-r--r--lzr.c953
-rw-r--r--lzr.h129
-rw-r--r--main.c14
8 files changed, 2009 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..102b00f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+005
+*.o
+*.d
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8101d80
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,63 @@
+CCXX := g++
+CC := gcc
+LD := gcc
+SDL2-CFG := sdl2-config
+SRC := $(wildcard *.c) $(wildcard *.cpp)
+OBJ := $(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(SRC)))
+WINOBJ := $(patsubst %.c,%.win.o,$(patsubst %.cpp,%.win.o,$(SRC)))
+DEP := $(patsubst %.c,%.d,$(patsubst %.cpp,%.d,$(SRC)))
+NAME := 005
+ifeq ($(TARGET),windows)
+ SDL2-CFG := /usr/x86_64-w64-mingw32/bin/sdl2-config
+ CCXX := x86_64-w64-mingw32-$(CCXX)
+ CC := x86_64-w64-mingw32-$(CC)
+ LD := x86_64-w64-mingw32-$(LD)
+ NAME := $(NAME).exe
+ OBJ := $(WINOBJ)
+endif
+LZR_FLAGS := -DLZR_DISABLE_IMAGE -DLZR_DISABLE_GFX -DLZR_DISABLE_DEVMODE
+CFLAGS := -Wall -Wextra -std=c99 -pedantic -MMD \
+ -D_POSIX_C_SOURCE=200809L $(LZR_FLAGS) $(shell $(SDL2-CFG) --cflags)
+CXXFLAGS := -Wall -Wextra -std=c++03 -pedantic -MMD \
+ -nostdlib -fno-rtti -fno-exceptions \
+ $(LZR_FLAGS) $(shell $(SDL2-CFG) --cflags)
+LDFLAGS := -lm $(shell $(SDL2-CFG) --libs) -lSDL2_mixer
+
+all: $(NAME)
+
+$(NAME): $(OBJ)
+ @printf '[ld] *.o -> %s\n' "$(NAME)"
+ @$(LD) -o $(NAME) $(OBJ) $(LDFLAGS)
+
+%.o: %.cpp
+ @printf '[c++] %s -> %s\n' "$<" "$@"
+ @$(CCXX) $(CXXFLAGS) -c -o $@ $<
+
+%.win.o: %.cpp
+ @printf '[c++] %s -> %s\n' "$<" "$@"
+ @$(CCXX) $(CXXFLAGS) -c -o $@ $<
+
+%.o: %.c
+ @printf '[cc] %s -> %s\n' "$<" "$@"
+ @$(CC) $(CFLAGS) -c -o $@ $<
+
+%.win.o: %.c
+ @printf '[cc] %s -> %s\n' "$<" "$@"
+ @$(CC) $(CFLAGS) -c -o $@ $<
+
+clean:
+ @printf '[rm]\n'
+ @rm -rf $(NAME) $(OBJ) $(WINOBJ) $(DEP)
+
+run: $(NAME)
+ @printf '[run]\n'
+ @./$(NAME)
+
+re:
+ @printf '[re]\n'
+ @make --no-print-directory clean
+ @make --no-print-directory
+
+.PHONY: all clean run re
+
+-include $(DEP)
diff --git a/cfg.h b/cfg.h
new file mode 100644
index 0000000..d88c011
--- /dev/null
+++ b/cfg.h
@@ -0,0 +1,20 @@
+#pragma once
+#include "lzr.h"
+
+enum {
+ CFG_DWIDTH = 256,
+ CFG_DHEIGHT = 256,
+ CFG_FPS = 60,
+ CFG_TSIZE = 16,
+};
+
+static const LZR_Config cfg = {
+ CFG_DWIDTH,
+ CFG_DHEIGHT,
+ CFG_FPS,
+ CFG_TSIZE,
+ "my life is not changing",
+ 0.0,
+ false,
+ true
+};
diff --git a/cmixer.c b/cmixer.c
new file mode 100644
index 0000000..add9ac7
--- /dev/null
+++ b/cmixer.c
@@ -0,0 +1,757 @@
+/*
+** 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 <stdio.h>
+#include <stdlib.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);
+ }
+
+ /* Don't process if not playing */
+ if (src->state != CM_STATE_PLAYING) {
+ return;
+ }
+
+ /* 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 = CLAMP(pan, -1.0, 1.0);
+ recalc_source_gains(src);
+}
+
+void cm_set_pitch(cm_Source *src, double pitch)
+{
+ double rate;
+ if (pitch > 0.) {
+ rate = src->samplerate / (double)cmixer.samplerate * pitch;
+ } else {
+ rate = 0.001;
+ }
+ 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
diff --git a/cmixer.h b/cmixer.h
new file mode 100644
index 0000000..6ca680b
--- /dev/null
+++ b/cmixer.h
@@ -0,0 +1,70 @@
+/*
+** 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.1"
+
+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
diff --git a/lzr.c b/lzr.c
new file mode 100644
index 0000000..ee4d1e4
--- /dev/null
+++ b/lzr.c
@@ -0,0 +1,953 @@
+/* Licensing information can be found at the end of the file. */
+#include "lzr.h"
+#include <SDL2/SDL.h>
+#ifdef LZR_ENABLE_GFX
+# include <SDL2/SDL2_gfxPrimitives.h>
+#endif
+#ifdef LZR_ENABLE_IMAGE
+# include <SDL2/SDL_image.h>
+#endif
+#ifdef LZR_ENABLE_MIXER
+# include "cmixer.h"
+#endif
+#ifdef LZR_ENABLE_DEVMODE
+# include <sys/stat.h>
+#endif
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#define UNPACKED_COLOR color[0], color[1], color[2]
+#define SCODE_BIND_MENU SDL_SCANCODE_F1
+#define SCODE_FULLSCREEN SDL_SCANCODE_F11
+
+static LZR_Config config = {0};
+static char *basepath = NULL;
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+static SDL_Texture *target = NULL;
+static uint_least64_t next_time = 0;
+static uint_least64_t min_dt = 0;
+static bool should_quit = false;
+static struct {
+ SDL_Texture *tex;
+ int width, height;
+ char *path;
+ long mtime;
+} images[LZR_MAX_IMAGES] = {0};
+static unsigned int color[3] = {0};
+static unsigned int map[LZR_BUTTON_COUNT] = {
+ SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UP,
+ SDL_SCANCODE_DOWN, SDL_SCANCODE_X, SDL_SCANCODE_C};
+static bool input[LZR_BUTTON_COUNT] = {false};
+static SDL_Point *points = NULL;
+static uint64_t tick = 0;
+static int off_x = 0;
+static int off_y = 0;
+static float scale = 1.0;
+static int mouse_x = 0;
+static int mouse_y = 0;
+
+#ifdef LZR_ENABLE_MIXER
+static struct {
+ cm_Source *ptr;
+} sounds[LZR_MAX_SOUNDS] = {0};
+static SDL_mutex *audio_mutex = NULL;
+static SDL_AudioDeviceID audio_dev = 0;
+
+static void _lock_handler(cm_Event *e)
+{
+ if (e->type == CM_EVENT_LOCK)
+ SDL_LockMutex(audio_mutex);
+ else
+ SDL_UnlockMutex(audio_mutex);
+}
+
+static void _audio_callback(void *udata, Uint8 *stream, int size)
+{
+ (void)udata;
+ cm_process((void *)stream, size / 2);
+}
+#endif
+
+static char *_lzrstrdup(const char *str)
+{
+ char *const cpy = malloc(strlen(str) + 1);
+ if (cpy == NULL)
+ return NULL;
+ strcpy(cpy, str);
+ return cpy;
+}
+
+static int _scode_to_button(unsigned int scode)
+{
+ for (int i = 0; i < LZR_BUTTON_MOUSE_L; i++)
+ if (map[i] == scode)
+ return i;
+ return -1;
+}
+
+static void _draw_btn(SDL_Renderer *ren, int btn, int x, int y,
+ unsigned int size)
+{
+#ifdef LZR_ENABLE_GFX
+ const unsigned int size_2thirds = size * 2 / 3;
+ switch (btn) {
+ case LZR_BUTTON_LEFT:
+ filledTrigonRGBA(ren, x - size / 2, y, x + size / 2,
+ y + size_2thirds, x + size / 2,
+ y - size_2thirds, 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_RIGHT:
+ filledTrigonRGBA(ren, x + size / 2, y, x - size / 2,
+ y + size_2thirds, x - size / 2,
+ y - size_2thirds, 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_UP:
+ filledTrigonRGBA(ren, x, y - size / 2, x - size_2thirds,
+ y + size / 2, x + size_2thirds, y + size / 2,
+ 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_DOWN:
+ filledTrigonRGBA(ren, x, y + size / 2, x - size_2thirds,
+ y - size / 2, x + size_2thirds, y - size / 2,
+ 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_O:
+ filledCircleRGBA(ren, x, y, size * 2 / 3, 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_X:
+ thickLineRGBA(ren, x - size / 2, y - size / 2, x + size / 2,
+ y + size / 2, size / 16 + 1, 0, 0, 0, 255);
+ thickLineRGBA(ren, x + size / 2, y - size / 2, x - size / 2,
+ y + size / 2, size / 16 + 1, 0, 0, 0, 255);
+ break;
+ default:
+ break;
+ }
+#else
+ (void)ren, (void)btn, (void)x, (void)y, (void)size;
+#endif
+}
+
+static void _bind_menu(void)
+{
+ SDL_Log("entering bind menu");
+ SDL_Window *win = NULL;
+ SDL_Renderer *ren = NULL;
+ if (SDL_CreateWindowAndRenderer(256, 256, 0, &win, &ren) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return;
+ }
+ int btn = 0;
+ SDL_Event e;
+ while (btn < LZR_BUTTON_MOUSE_L) {
+ while (SDL_PollEvent(&e)) {
+ if (e.type != SDL_KEYDOWN || e.key.repeat ||
+ e.key.keysym.scancode == SCODE_BIND_MENU ||
+ e.key.keysym.scancode == SCODE_FULLSCREEN)
+ continue;
+ if (e.key.keysym.scancode == SDL_SCANCODE_ESCAPE ||
+ e.type == SDL_QUIT)
+ goto exit_bind_menu;
+ LZR_ButtonBind(btn, e.key.keysym.scancode);
+ btn++;
+ }
+ SDL_SetRenderDrawColor(ren, 220, 220, 200, 255);
+ SDL_RenderClear(ren);
+ SDL_SetRenderDrawColor(ren, 0, 0, 0, 255);
+ _draw_btn(ren, btn, 128, 128, 104);
+ SDL_RenderPresent(ren);
+ sleep(0);
+ }
+exit_bind_menu:
+ SDL_DestroyRenderer(ren);
+ SDL_DestroyWindow(win);
+ SDL_Log("leaving bind menu");
+}
+
+int LZR_Init(LZR_Config cfg)
+{
+ memcpy(&config, &cfg, sizeof(config));
+ if (config.display_width == 0) {
+ SDL_Log("display_width can't be 0");
+ return -1;
+ }
+ if (config.display_height == 0) {
+ SDL_Log("display_height can't be 0");
+ return -1;
+ }
+ if (config.title == NULL) {
+ SDL_Log("title is NULL, defaulting to 'LZR'");
+ config.title = "LZR";
+ }
+ if (config.tile_size == 0)
+ config.tile_size = 1;
+ if (config.ratio <= 0.0)
+ config.ratio = 1.0;
+ else {
+ const double ratio =
+ (float)config.display_width / (float)config.display_height;
+ config.ratio /= ratio;
+ }
+ if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+#ifdef LZR_ENABLE_IMAGE
+ if (IMG_Init(IMG_INIT_PNG) != IMG_INIT_PNG) {
+ SDL_Log("%s", IMG_GetError());
+ return -1;
+ }
+#endif
+#ifdef LZR_ENABLE_MIXER
+ audio_mutex = SDL_CreateMutex();
+ if (audio_mutex == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ SDL_AudioSpec fmt = {.freq = 44100,
+ .format = AUDIO_S16,
+ .channels = 2,
+ .samples = 1024,
+ .callback = _audio_callback};
+ SDL_AudioSpec got;
+ audio_dev = SDL_OpenAudioDevice(NULL, 0, &fmt, &got,
+ SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
+ if (audio_dev == 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ cm_init(44100);
+ cm_set_lock(_lock_handler);
+ SDL_PauseAudioDevice(audio_dev, 0);
+#endif
+ basepath = SDL_GetBasePath();
+ if (basepath == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ const int dwidth = config.display_width * config.ratio;
+ window = SDL_CreateWindow(config.title, SDL_WINDOWPOS_UNDEFINED,
+ SDL_WINDOWPOS_UNDEFINED, dwidth,
+ config.display_height, SDL_WINDOW_RESIZABLE);
+ if (window == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
+ if (renderer == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ target = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB888,
+ SDL_TEXTUREACCESS_TARGET,
+ config.display_width, config.display_height);
+ if (target == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ points =
+ calloc(cfg.display_width * cfg.display_height, sizeof(SDL_Point));
+ if (points == NULL) {
+ SDL_Log("calloc failed");
+ return -1;
+ }
+ SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, 0);
+ if (config.target_fps) {
+ min_dt = 1000 / config.target_fps;
+ next_time = SDL_GetTicks64();
+ }
+ if (config.hide_cursor && SDL_ShowCursor(SDL_DISABLE) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+void LZR_Quit(void)
+{
+#ifdef LZR_ENABLE_MIXER
+ for (int i = LZR_MAX_SOUNDS - 1; i >= 0; i--)
+ if (sounds[i].ptr != NULL)
+ cm_stop(sounds[i].ptr);
+ SDL_Delay(100);
+ for (int i = LZR_MAX_SOUNDS - 1; i >= 0; i--)
+ if (sounds[i].ptr != NULL) {
+ cm_destroy_source(sounds[i].ptr);
+ sounds[i].ptr = NULL;
+ SDL_Log("destroyed sound %d", i);
+ }
+ if (audio_dev != 0) {
+ SDL_CloseAudioDevice(audio_dev);
+ audio_dev = 0;
+ }
+ if (audio_mutex != NULL) {
+ SDL_DestroyMutex(audio_mutex);
+ audio_mutex = NULL;
+ }
+#endif
+ for (int i = 0; i < LZR_MAX_IMAGES; i++) {
+ if (images[i].tex != NULL) {
+ SDL_DestroyTexture(images[i].tex);
+ images[i].tex = NULL;
+ SDL_Log("destroyed image %d", i);
+ }
+ if (images[i].path != NULL) {
+ free(images[i].path);
+ images[i].path = NULL;
+ }
+ }
+ if (points != NULL) {
+ free(points);
+ points = NULL;
+ }
+ if (target != NULL) {
+ SDL_DestroyTexture(target);
+ target = NULL;
+ }
+ if (renderer != NULL) {
+ SDL_DestroyRenderer(renderer);
+ renderer = NULL;
+ }
+ if (window != NULL) {
+ SDL_DestroyWindow(window);
+ window = NULL;
+ }
+ if (basepath != NULL) {
+ SDL_free(basepath);
+ basepath = NULL;
+ }
+#ifdef LZR_ENABLE_IMAGE
+ IMG_Quit();
+#endif
+ SDL_Quit();
+}
+
+bool LZR_ShouldQuit(void)
+{
+ return should_quit;
+}
+
+char *LZR_PathPrefix(const char *path)
+{
+ if (path == NULL) {
+ SDL_Log("path is NULL");
+ return NULL;
+ }
+ if (basepath == NULL) {
+ SDL_Log("basepath is NULL");
+ return _lzrstrdup(path);
+ }
+ char *const buf = malloc(strlen(basepath) + strlen(path) + 1);
+ if (buf == NULL) {
+ SDL_Log("malloc failed");
+ return NULL;
+ }
+ strcpy(buf, basepath);
+ strcat(buf, path);
+ return buf;
+}
+
+int LZR_ImageLoad(const char *path)
+{
+ char *apath;
+ long mtime = 0;
+#ifdef LZR_ENABLE_DEVMODE
+ apath = LZR_PathPrefix(path);
+ if (apath == NULL)
+ return -1;
+ struct stat st = {0};
+ (void)stat(apath, &st); /* stat can fail safely */
+ mtime = st.st_mtim.tv_nsec;
+#endif
+ int i;
+ for (i = 0; i < LZR_MAX_IMAGES; i++) {
+ if (images[i].path != NULL &&
+ strcmp(images[i].path, path) == 0) {
+ if (mtime != images[i].mtime) {
+ SDL_Log("reloading %d", i);
+ break;
+ }
+ return i;
+ }
+ if (images[i].tex == NULL)
+ break;
+ }
+ if (i >= LZR_MAX_IMAGES) {
+ SDL_Log("reached image limit (%d)", LZR_MAX_IMAGES);
+ return -1;
+ }
+ apath = LZR_PathPrefix(path);
+ if (apath == NULL) {
+ SDL_Log("LZR_PathPrefix failed");
+ return -1;
+ }
+#ifdef LZR_ENABLE_IMAGE
+ SDL_Surface *const surf = IMG_Load(apath);
+#else
+ SDL_Surface *const surf = SDL_LoadBMP(apath);
+#endif
+ free(apath);
+ if (surf == NULL) {
+ SDL_Log("%s: %s", path, SDL_GetError());
+ return -1;
+ }
+ SDL_Texture *const tex = SDL_CreateTextureFromSurface(renderer, surf);
+ SDL_FreeSurface(surf);
+ if (tex == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (images[i].tex != NULL)
+ SDL_DestroyTexture(images[i].tex);
+ images[i].tex = tex;
+ images[i].mtime = mtime;
+ if (SDL_SetTextureBlendMode(images[i].tex, SDL_BLENDMODE_BLEND) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (SDL_QueryTexture(tex, NULL, NULL, &images[i].width,
+ &images[i].height)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (images[i].path == NULL)
+ images[i].path = _lzrstrdup(path);
+ return i;
+}
+
+int LZR_SoundLoad(const char *path, float volume)
+{
+#ifdef LZR_ENABLE_MIXER
+ int i;
+ for (i = 0; i < LZR_MAX_SOUNDS; i++)
+ if (sounds[i].ptr == NULL)
+ break;
+ if (i >= LZR_MAX_SOUNDS) {
+ SDL_Log("reached sounds limit (%d)", LZR_MAX_SOUNDS);
+ return -1;
+ }
+ char *const apath = LZR_PathPrefix(path);
+ if (apath == NULL) {
+ SDL_Log("LZR_PathPrefix failed");
+ return -1;
+ }
+ cm_Source *const chunk = cm_new_source_from_file(apath);
+ free(apath);
+ if (chunk == NULL) {
+ SDL_Log("%s: %s", path, cm_get_error());
+ return -1;
+ }
+ cm_set_gain(chunk, volume);
+ sounds[i].ptr = chunk;
+ return i;
+#else
+ (void)path, (void)volume;
+ return -1;
+#endif
+}
+
+bool LZR_PollEvent(LZR_Event *e)
+{
+ if (e == NULL) {
+ SDL_Log("e is NULL");
+ return false;
+ }
+ SDL_Event se;
+ while (SDL_PollEvent(&se)) {
+ switch (se.type) {
+ case SDL_QUIT:
+ e->type = LZR_EVENT_QUIT;
+ should_quit = true;
+ return true;
+ case SDL_KEYDOWN: {
+ if (!config.disable_bind_menu &&
+ se.key.keysym.scancode == SCODE_BIND_MENU)
+ _bind_menu();
+ if (se.key.keysym.scancode == SCODE_FULLSCREEN)
+ LZR_ToggleFullscreen();
+ const int b = _scode_to_button(se.key.keysym.scancode);
+ if (se.key.repeat || b < 0)
+ break;
+ e->type = LZR_EVENT_BUTTON_DOWN;
+ e->button = b;
+ input[b] = true;
+ return true;
+ }
+ case SDL_MOUSEBUTTONDOWN: {
+ e->type = LZR_EVENT_BUTTON_DOWN;
+ e->button = LZR_BUTTON_MOUSE_L + se.button.button - 1;
+ e->x = se.button.x, e->y = se.button.y;
+ LZR_ScreenTransform(&e->x, &e->y);
+ mouse_x = e->x, mouse_y = e->y;
+ if (e->button >= LZR_BUTTON_COUNT)
+ continue;
+ input[e->button] = true;
+ return true;
+ }
+ case SDL_KEYUP: {
+ const int b = _scode_to_button(se.key.keysym.scancode);
+ if (b < 0)
+ break;
+ e->type = LZR_EVENT_BUTTON_UP;
+ e->button = b;
+ input[b] = false;
+ return true;
+ }
+ case SDL_MOUSEBUTTONUP: {
+ e->type = LZR_EVENT_BUTTON_DOWN;
+ e->button = LZR_BUTTON_MOUSE_L + se.button.button - 1;
+ e->x = se.button.x, e->y = se.button.y;
+ LZR_ScreenTransform(&e->x, &e->y);
+ mouse_x = e->x, mouse_y = e->y;
+ if (e->button >= LZR_BUTTON_COUNT)
+ continue;
+ input[e->button] = false;
+ return true;
+ }
+ case SDL_MOUSEMOTION: {
+ e->type = LZR_EVENT_MOUSE_MOVE;
+ e->x = se.motion.x, e->y = se.motion.y;
+ LZR_ScreenTransform(&e->x, &e->y);
+ mouse_x = e->x, mouse_y = e->y;
+ return true;
+ }
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+void LZR_CycleEvents(void)
+{
+ LZR_Event e;
+ while (LZR_PollEvent(&e))
+ ;
+}
+
+bool LZR_ButtonDown(LZR_Button btn)
+{
+ if (btn >= 0 && btn < LZR_BUTTON_COUNT)
+ return input[btn];
+ else
+ SDL_Log("%d button doesn't exist", btn);
+ return false;
+}
+
+void LZR_ButtonBind(LZR_Button btn, unsigned int code)
+{
+ if (btn < LZR_BUTTON_MOUSE_L && code != SCODE_BIND_MENU) {
+ map[btn] = code;
+ SDL_Log("bound key %s to button %u",
+ SDL_GetScancodeName(map[btn]), btn);
+ } else
+ SDL_Log("button %u can't be remapped to key %s", btn,
+ SDL_GetScancodeName(code));
+}
+
+int LZR_DrawBegin(void)
+{
+ if (config.target_fps > 0)
+ next_time += min_dt;
+ if (SDL_SetRenderTarget(renderer, target) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+ SDL_RenderClear(renderer);
+ return 0;
+}
+
+int LZR_DrawEnd(void)
+{
+ if (SDL_SetRenderTarget(renderer, NULL)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ LZR_DrawSetColor(0.0f, 0.0f, 0.0f);
+ if (LZR_DrawClear()) {
+ SDL_Log("LZY_DrawClear failed");
+ return -1;
+ }
+ if (config.target_fps) {
+ const uint_least64_t cur_time = SDL_GetTicks64();
+ if (next_time <= cur_time)
+ next_time = cur_time;
+ else
+ SDL_Delay(next_time - cur_time);
+ }
+ int win_w, win_h;
+ SDL_GetWindowSize(window, &win_w, &win_h);
+ const int width = config.display_width * config.ratio;
+ const int height = config.display_height;
+ const int ratio_w = win_w / width;
+ const int ratio_h = win_h / height;
+ scale = (ratio_w <= ratio_h) ? ratio_w : ratio_h;
+ off_x = (win_w - width * scale) / 2;
+ off_y = (win_h - height * scale) / 2;
+ const SDL_Rect dest = {off_x, off_y, width * scale, height * scale};
+ if (SDL_RenderCopyEx(renderer, target, NULL, &dest, 0.0, NULL, 0) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ SDL_RenderPresent(renderer);
+ tick++;
+ return 0;
+}
+
+int LZR_DrawSetColor(float r, float g, float b)
+{
+ const unsigned int ur = (unsigned int)(r * 255) & 255;
+ const unsigned int ug = (unsigned int)(g * 255) & 255;
+ const unsigned int ub = (unsigned int)(b * 255) & 255;
+ if (SDL_SetRenderDrawColor(renderer, ur, ug, ub, 255) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ color[0] = ur, color[1] = ug, color[2] = ub;
+ return 0;
+}
+
+int LZR_DrawClear(void)
+{
+ if (SDL_RenderClear(renderer) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawPoint(int x, int y)
+{
+ if (SDL_RenderDrawPoint(renderer, x, y) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawPoints(int *x, int *y, int n)
+{
+
+ if (n > (int)(config.display_width * config.display_height)) {
+ SDL_Log("%d > %u", n,
+ config.display_width * config.display_height);
+ return -1;
+ }
+ for (int i = 0; i < n; i++) {
+ points[i].x = x[i];
+ points[i].y = y[i];
+ }
+ if (SDL_RenderDrawPoints(renderer, points, n) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawLine(int x0, int y0, int x1, int y1)
+{
+ if (SDL_RenderDrawLine(renderer, x0, y0, x1, y1) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawRectangle(bool fill, int x, int y, int w, int h)
+{
+ SDL_Rect rect = {x, y, w, h};
+ if ((fill ? SDL_RenderFillRect : SDL_RenderDrawRect)(renderer, &rect)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawCircle(bool fill, int x, int y, int radius)
+{
+#ifdef LZR_ENABLE_GFX
+ if ((fill ? filledCircleRGBA : circleRGBA)(renderer, x, y, radius,
+ UNPACKED_COLOR, 255) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+#else
+ (void)fill, (void)x, (void)y, (void)radius;
+ SDL_Log("LZR GFX module is disabled");
+ return -1;
+#endif
+}
+
+int LZR_DrawPolygon(bool fill, int *vx, int *vy, int n)
+{
+#ifdef LZR_ENABLE_GFX
+ if (n > 32) {
+ SDL_Log("%d > 32", n);
+ return -1;
+ }
+ Sint16 x[32], y[32];
+ for (int i = 0; i < n; i++)
+ x[i] = vx[i], y[i] = vy[i];
+ if ((fill ? filledPolygonRGBA : polygonRGBA)(renderer, x, y, n,
+ UNPACKED_COLOR, 255) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+#else
+ (void)fill, (void)vx, (void)vy, (void)n;
+ SDL_Log("LZR GFX module is disabled");
+ return -1;
+#endif
+}
+
+int LZR_DrawImage(int id, int x, int y)
+{
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_IMAGES || images[id].tex == NULL) {
+ SDL_Log("no image with id %d", id);
+ return -1;
+ }
+ if (SDL_SetTextureColorMod(images[id].tex, UNPACKED_COLOR) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ const SDL_Rect dest = {x, y, images[id].width, images[id].height};
+ if (SDL_RenderCopy(renderer, images[id].tex, NULL, &dest) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawImageEx(int id, int x, int y, LZR_ImageDrawSettings stg)
+{
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_IMAGES || images[id].tex == NULL) {
+ SDL_Log("no image with id %d", id);
+ return -1;
+ }
+ const int width = (stg.width > 0) ? stg.width : images[id].width;
+ const int height = (stg.height > 0) ? stg.height : images[id].height;
+ if (stg.center) {
+ x -= stg.scale_x * width / 2;
+ y -= stg.scale_y * height / 2;
+ }
+ SDL_Rect src = {stg.ix, stg.iy, width, height};
+ SDL_Rect dst = {x, y, width * stg.scale_x, height * stg.scale_y};
+ if (stg.ix < 0) {
+ src.w += stg.ix;
+ dst.x = 0 - stg.ix;
+ dst.w += stg.ix;
+ }
+ if (stg.iy < 0) {
+ src.y = 0 - stg.iy;
+ src.h += stg.iy;
+ dst.y -= stg.iy;
+ dst.h += stg.iy;
+ }
+ if (stg.ix + width > images[id].width) {
+ src.w = images[id].width - stg.ix;
+ dst.w = images[id].width - stg.ix;
+ }
+ if (stg.iy + height > images[id].height) {
+ src.h = images[id].height - stg.iy;
+ dst.h = images[id].height - stg.iy;
+ }
+ if (SDL_SetTextureColorMod(images[id].tex, UNPACKED_COLOR) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ const int flip = (stg.flip_v ? SDL_FLIP_VERTICAL : 0) |
+ (stg.flip_h ? SDL_FLIP_HORIZONTAL : 0);
+ if (SDL_RenderCopyEx(renderer, images[id].tex, &src, &dst,
+ stg.angle * 360.0, NULL, flip)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawTile(int id, int tile, int x, int y, double rot, int flip)
+{
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_IMAGES || images[id].tex == NULL) {
+ SDL_Log("no image with id %d", id);
+ return -1;
+ }
+ if (tile < 0) {
+ SDL_Log("tile is negative");
+ return -1;
+ }
+ const int img_width = images[id].width / config.tile_size;
+ const int img_height = images[id].height / config.tile_size;
+ if (tile >= img_width * img_height) {
+ SDL_Log("tile exceeds boundaries");
+ return -1;
+ }
+ if (SDL_SetTextureColorMod(images[id].tex, UNPACKED_COLOR) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ SDL_Rect src;
+ src.x = (tile % img_width) * config.tile_size;
+ src.y = (tile / img_width) * config.tile_size;
+ src.w = config.tile_size, src.h = config.tile_size;
+ const SDL_Rect dst = {x, y, config.tile_size, config.tile_size};
+ if (SDL_RenderCopyEx(renderer, images[id].tex, &src, &dst, rot, NULL,
+ flip) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_PlaySound(int id, int loops)
+{
+#ifdef LZR_ENABLE_MIXER
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_SOUNDS || sounds[id].ptr == NULL) {
+ SDL_Log("no sound with id %d", id);
+ return -1;
+ }
+ cm_stop(sounds[id].ptr);
+ cm_set_loop(sounds[id].ptr, loops);
+ cm_play(sounds[id].ptr);
+ return 0;
+#else
+ (void)id, (void)loops;
+ SDL_Log("LZR MIXER module is disabled");
+ return -1;
+#endif
+}
+
+void LZR_StopSound(int id)
+{
+#ifdef LZR_ENABLE_MIXER
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return;
+ }
+ if (id >= LZR_MAX_SOUNDS || sounds[id].ptr == NULL) {
+ SDL_Log("no sound with id %d", id);
+ return;
+ }
+ cm_stop(sounds[id].ptr);
+#else
+ (void)id;
+#endif
+}
+
+int LZR_SetSoundVolume(int id, float volume)
+{
+#ifdef LZR_ENABLE_MIXER
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_SOUNDS || sounds[id].ptr == NULL) {
+ SDL_Log("no sound with id %d", id);
+ return -1;
+ }
+ cm_set_gain(sounds[id].ptr, volume);
+ return 0;
+#else
+ (void)id, (void)volume;
+ SDL_Log("LZR MIXER module is disabled");
+ return -1;
+#endif
+}
+
+int LZR_SetSoundPan(int id, float pan)
+{
+#ifdef LZR_ENABLE_MIXER
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_SOUNDS || sounds[id].ptr == NULL) {
+ SDL_Log("no sound with id %d", id);
+ return -1;
+ }
+ cm_set_pan(sounds[id].ptr, pan);
+ return 0;
+#else
+ (void)id, (void)pan;
+ SDL_Log("LZR MIXER module is disabled");
+ return -1;
+#endif
+}
+
+void LZR_ToggleFullscreen(void)
+{
+ static int fullscreen = 0;
+ fullscreen = !fullscreen;
+ SDL_SetWindowFullscreen(window,
+ fullscreen * SDL_WINDOW_FULLSCREEN_DESKTOP);
+}
+
+uint64_t LZR_GetTick(void)
+{
+ return tick;
+}
+
+void LZR_ScreenTransform(int *x, int *y)
+{
+ if (scale == 0.0)
+ return;
+ if (x != NULL) {
+ *x -= off_x;
+ *x /= scale;
+ }
+ if (y != NULL) {
+ *y -= off_y;
+ *y /= scale;
+ }
+}
+
+void LZR_MousePosition(int *x, int *y)
+{
+ if (x != NULL)
+ *x = mouse_x;
+ if (y != NULL)
+ *y = mouse_y;
+}
+
+/*
+** Copyright (c) 2022, 2023 kdx
+**
+** 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.
+*/
diff --git a/lzr.h b/lzr.h
new file mode 100644
index 0000000..12f2bb5
--- /dev/null
+++ b/lzr.h
@@ -0,0 +1,129 @@
+/* Licensing informations can be found at the end of the file. */
+#pragma once
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifndef LZR_DISABLE_IMAGE
+# define LZR_ENABLE_IMAGE
+#endif
+#ifndef LZR_DISABLE_MIXER
+# define LZR_ENABLE_MIXER
+#endif
+#ifndef LZR_DISABLE_GFX
+# define LZR_ENABLE_GFX
+#endif
+
+/* devmode tracks and autoreloads ressources on change */
+#ifndef LZR_DISABLE_DEVMODE
+# define LZR_ENABLE_DEVMODE
+#endif
+
+#define LZR_MAX_IMAGES 64
+#define LZR_MAX_SOUNDS 64
+#define LZR_BUTTON(btn) LZR_ButtonDown(LZR_BUTTON_##btn)
+#define LZR_IMAGE(img) LZR_ImageLoad(img)
+
+typedef struct LZR_Config {
+ unsigned int display_width;
+ unsigned int display_height;
+ unsigned int target_fps;
+ unsigned int tile_size;
+ const char *title;
+ double ratio;
+ bool disable_bind_menu;
+ bool hide_cursor;
+} LZR_Config;
+
+typedef enum LZR_EventType {
+ LZR_EVENT_QUIT,
+ LZR_EVENT_BUTTON_DOWN,
+ LZR_EVENT_BUTTON_UP,
+ LZR_EVENT_MOUSE_MOVE
+} LZR_EventType;
+
+typedef enum LZR_Button {
+ LZR_BUTTON_LEFT,
+ LZR_BUTTON_RIGHT,
+ LZR_BUTTON_UP,
+ LZR_BUTTON_DOWN,
+ LZR_BUTTON_O,
+ LZR_BUTTON_X,
+ LZR_BUTTON_MOUSE_L,
+ LZR_BUTTON_MOUSE_M,
+ LZR_BUTTON_MOUSE_R,
+ LZR_BUTTON_COUNT
+} LZR_Button;
+
+typedef struct LZR_Event {
+ LZR_EventType type;
+ LZR_Button button;
+ int x, y;
+} LZR_Event;
+
+typedef struct LZR_ImageDrawSettings {
+ int ix, iy, width, height;
+ double scale_x, scale_y, angle;
+ bool center, flip_h, flip_v;
+} LZR_ImageDrawSettings;
+
+int LZR_Init(LZR_Config cfg);
+void LZR_Quit(void);
+bool LZR_ShouldQuit(void);
+bool LZR_PollEvent(LZR_Event *e);
+void LZR_CycleEvents(void);
+bool LZR_ButtonDown(LZR_Button btn);
+void LZR_ButtonBind(LZR_Button btn, unsigned int code);
+char *LZR_PathPrefix(const char *path);
+int LZR_ImageLoad(const char *path);
+int LZR_SoundLoad(const char *path, float volume);
+int LZR_DrawBegin(void);
+int LZR_DrawEnd(void);
+int LZR_DrawSetColor(float r, float g, float b);
+int LZR_DrawClear(void);
+int LZR_DrawPoint(int x, int y);
+int LZR_DrawPoints(int *x, int *y, int n);
+int LZR_DrawLine(int x0, int y0, int x1, int y1);
+int LZR_DrawRectangle(bool fill, int x, int y, int w, int h);
+int LZR_DrawCircle(bool fill, int x, int y, int radius);
+int LZR_DrawPolygon(bool fill, int *vx, int *vy, int n);
+int LZR_DrawImage(int id, int x, int y);
+int LZR_DrawImageEx(int id, int x, int y, LZR_ImageDrawSettings stg);
+int LZR_DrawTile(int id, int tile, int x, int y, double rot, int flip);
+int LZR_PlaySound(int id, int loops);
+void LZR_StopSound(int id);
+int LZR_SetSoundVolume(int id, float volume);
+int LZR_SetSoundPan(int id, float pan);
+void LZR_ToggleFullscreen(void);
+uint64_t LZR_GetTick(void);
+void LZR_ScreenTransform(int *x, int *y);
+void LZR_MousePosition(int *x, int *y);
+
+#ifdef __cplusplus
+}
+#endif
+
+/*
+** Copyright (c) 2022, 2023 kdx
+**
+** 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.
+*/
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..8a232e3
--- /dev/null
+++ b/main.c
@@ -0,0 +1,14 @@
+#include "cfg.h"
+#include "lzr.h"
+
+int main(int argc, char **argv)
+{
+ (void)argc, (void)argv;
+ if (LZR_Init(cfg))
+ {
+ LZR_Quit();
+ return 1;
+ }
+ LZR_Quit();
+ return 0;
+}