diff options
author | kdx <kikoodx@paranoici.org> | 2023-01-15 01:11:59 +0100 |
---|---|---|
committer | kdx <kikoodx@paranoici.org> | 2023-01-15 01:11:59 +0100 |
commit | ff9144048a2db6d443b2f5c17578ca7fdbae796d (patch) | |
tree | c26696fe8479c862c11545d886c4774825e01fc5 | |
download | 005-ff9144048a2db6d443b2f5c17578ca7fdbae796d.tar.gz |
initial commit
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 63 | ||||
-rw-r--r-- | cfg.h | 20 | ||||
-rw-r--r-- | cmixer.c | 757 | ||||
-rw-r--r-- | cmixer.h | 70 | ||||
-rw-r--r-- | lzr.c | 953 | ||||
-rw-r--r-- | lzr.h | 129 | ||||
-rw-r--r-- | main.c | 14 |
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) @@ -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 @@ -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. +*/ @@ -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. +*/ @@ -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; +} |