diff --git a/makefile b/makefile index bc98f22..ac1529b 100644 --- a/makefile +++ b/makefile @@ -1,8 +1,8 @@ CC ?= gcc CFLAGS ?= -Wall -Wextra -g -Isrc -LDFLAGS ?= -ljack -lm +LDFLAGS ?= -ljack -lm -lpthread -SRC = src/main.c src/looper.c src/channel.c src/midi.c +SRC = src/main.c src/looper.c src/channel.c src/midi.c src/ringbuffer.c src/wav.c OBJ = $(SRC:.c=.o) looper: $(OBJ) @@ -12,7 +12,7 @@ src/%.o: src/%.c $(CC) $(CFLAGS) -c -o $@ $< integration: looper tests/integration.c - $(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm + $(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm -lpthread ./integration_test test: integration diff --git a/src/channel.c b/src/channel.c index 8eaf4d7..eb399a4 100644 --- a/src/channel.c +++ b/src/channel.c @@ -28,6 +28,7 @@ void channel_add(jack_client_t *client, int idx) { channels[idx].loop_count = 0; channels[idx].record_pos = 0; channels[idx].playback_pos = 0; + channels[idx].save_ring = NULL; next_channel_id++; channel_count++; diff --git a/src/channel.h b/src/channel.h index a0c2f89..a97e337 100644 --- a/src/channel.h +++ b/src/channel.h @@ -8,6 +8,8 @@ #define LOOP_BUF_SIZE (5 * 48000) #define MAX_CHANNELS 16 +#include "ringbuffer.h" + typedef enum { STATE_IDLE, STATE_RECORD, @@ -25,6 +27,8 @@ struct channel_t { atomic_int active; jack_port_t *audio_in; jack_port_t *audio_out; + + RingBuf *save_ring; }; /* Globals declared in looper.c */ @@ -33,6 +37,8 @@ extern atomic_int channel_count; extern int next_channel_id; extern atomic_int cmd_add; extern atomic_int cmd_remove; +extern atomic_int cmd_load; +extern atomic_int cmd_save; void channel_add(jack_client_t *client, int idx); void channel_remove(jack_client_t *client, int idx); diff --git a/src/looper.c b/src/looper.c index dae0dd1..7d20ee7 100644 --- a/src/looper.c +++ b/src/looper.c @@ -2,6 +2,7 @@ #include "looper.h" #include "channel.h" #include "midi.h" +#include "wav.h" #include #include #include @@ -9,6 +10,8 @@ #include #include #include +#include +#include /* Global state (shared across files) */ struct channel_t channels[MAX_CHANNELS]; @@ -16,6 +19,8 @@ atomic_int channel_count = 0; int next_channel_id = 1; atomic_int cmd_add = 0; atomic_int cmd_remove = 0; +atomic_int cmd_load = 0; +atomic_int cmd_save = 0; jack_port_t *midi_control_port = NULL; jack_port_t *midi_clock_port = NULL; atomic_int control_key_active = 0; @@ -24,6 +29,10 @@ atomic_int bind_channel = 0; /* Deferred removal index (1 second grace) */ static int pending_unregister_idx = -1; +/* writer thread function and sample rate holder */ +static void *writer_thread(void *arg); +static int global_sample_rate = 0; + /* ---------------------------------------------------------------- * process callback * ---------------------------------------------------------------- */ @@ -172,6 +181,9 @@ void jack_shutdown_cb(void *arg) { * looper initialisation * ---------------------------------------------------------------- */ int looper_init(jack_client_t *client) { + /* store sample rate for writer thread */ + global_sample_rate = jack_get_sample_rate(client); + /* channel 0 */ channels[0].active = 1; atomic_store(&channels[0].state, STATE_IDLE); @@ -179,6 +191,7 @@ int looper_init(jack_client_t *client) { channels[0].loop_count = 0; channels[0].record_pos = 0; channels[0].playback_pos = 0; + channels[0].save_ring = NULL; channels[0].audio_in = jack_port_register( client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); @@ -202,6 +215,44 @@ int looper_init(jack_client_t *client) { return 0; } +/* ---------------------------------------------------------------- + * writer thread – consumes the save ring and writes WAV file + * ---------------------------------------------------------------- */ +static void *writer_thread(void *arg) { + struct channel_t *ch = (struct channel_t *)arg; + RingBuf *ring = ch->save_ring; + if (!ring) return NULL; + + static const char *path = "save.wav"; + unsigned sr = (unsigned)global_sample_rate; + if (sr == 0) sr = 48000; + + float *outbuf = malloc((size_t)ch->loop_count * sizeof(float)); + if (!outbuf) { + ring_destroy(ring); + free(ring); + ch->save_ring = NULL; + return NULL; + } + size_t collected = 0; + size_t want = (size_t)ch->loop_count; + while (collected < want) { + size_t got = ring_read(ring, outbuf + collected, want - collected); + collected += got; + if (got == 0) { + struct timespec req = { .tv_sec = 0, .tv_nsec = 10000000 }; + nanosleep(&req, NULL); + } + } + wav_write(path, outbuf, ch->loop_count, sr); + free(outbuf); + + ring_destroy(ring); + free(ring); + ch->save_ring = NULL; + return NULL; +} + /* ---------------------------------------------------------------- * main‑loop command processing * ---------------------------------------------------------------- */ @@ -239,4 +290,42 @@ void looper_process_commands(jack_client_t *client) { pending_unregister_idx = remove_idx; } } + + /* ---------- load command ---------- */ + if (atomic_exchange(&cmd_load, 0)) { + float *buf = NULL; + unsigned frames = 0; + if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) { + if (frames > LOOP_BUF_SIZE) frames = LOOP_BUF_SIZE; + memcpy(channels[0].loop_buffer, buf, frames * sizeof(float)); + channels[0].loop_count = (int)frames; + channels[0].record_pos = 0; + channels[0].playback_pos = 0; + atomic_store(&channels[0].state, STATE_LOOPING); + channels[0].prev_state = -1; + free(buf); + } else { + fprintf(stderr, "Failed to load loop.wav\n"); + } + } + + /* ---------- save command (writer thread) ---------- */ + if (atomic_exchange(&cmd_save, 0)) { + if (atomic_load(&channels[0].state) == STATE_LOOPING && + channels[0].loop_count > 0 && + channels[0].save_ring == NULL) { + RingBuf *ring = (RingBuf*)malloc(sizeof(RingBuf)); + if (ring) { + size_t sz = (size_t)channels[0].loop_count * 2; + if (ring_init(ring, sz) == 0) { + channels[0].save_ring = ring; + pthread_t th; + pthread_create(&th, NULL, writer_thread, &channels[0]); + pthread_detach(th); + } else { + free(ring); + } + } + } + } } diff --git a/src/midi.c b/src/midi.c index bac71c5..76a81b7 100644 --- a/src/midi.c +++ b/src/midi.c @@ -8,6 +8,8 @@ extern atomic_int control_key_active; extern atomic_int cmd_add; extern atomic_int cmd_remove; +extern atomic_int cmd_load; +extern atomic_int cmd_save; extern atomic_int bind_channel; void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { @@ -67,6 +69,12 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { case 63: /* unbind – reset bind to channel 0 */ atomic_store(&bind_channel, 0); break; + case 70: /* load WAV into channel 0 */ + atomic_store(&cmd_load, 1); + break; + case 71: /* save WAV of channel 0 */ + atomic_store(&cmd_save, 1); + break; default: break; } diff --git a/src/ringbuffer.c b/src/ringbuffer.c new file mode 100644 index 0000000..6543388 --- /dev/null +++ b/src/ringbuffer.c @@ -0,0 +1,69 @@ +#include "ringbuffer.h" +#include + +static inline size_t load_head(const RingBuf *r) { + return atomic_load_explicit(&r->head, memory_order_relaxed); +} +static inline size_t load_tail(const RingBuf *r) { + return atomic_load_explicit(&r->tail, memory_order_relaxed); +} +static inline void store_head(RingBuf *r, size_t v) { + atomic_store_explicit(&r->head, v, memory_order_relaxed); +} +static inline void store_tail(RingBuf *r, size_t v) { + atomic_store_explicit(&r->tail, v, memory_order_relaxed); +} + +int ring_init(RingBuf *r, size_t capacity) { + r->buf = (float*)malloc(capacity * sizeof(float)); + if (!r->buf) return -1; + r->capacity = capacity; + store_head(r, 0); + store_tail(r, 0); + return 0; +} + +void ring_destroy(RingBuf *r) { + free(r->buf); + r->buf = NULL; + r->capacity = 0; +} + +size_t ring_readable(const RingBuf *r) { + size_t h = load_head(r); + size_t t = load_tail(r); + if (h >= t) return h - t; + else return r->capacity - (t - h); +} + +size_t ring_writeable(const RingBuf *r) { + return r->capacity - 1 - ring_readable(r); +} + +size_t ring_write(RingBuf *r, const float *data, size_t count) { + size_t avail = ring_writeable(r); + if (count > avail) count = avail; + if (count == 0) return 0; + size_t head = load_head(r); + size_t cap = r->capacity; + for (size_t i = 0; i < count; ++i) { + r->buf[head] = data[i]; + head = (head + 1) % cap; + } + store_head(r, head); + return count; +} + +size_t ring_read(RingBuf *r, float *data, size_t count) { + size_t avail = ring_readable(r); + if (count > avail) count = avail; + if (count == 0) return 0; + size_t tail = load_tail(r); + size_t cap = r->capacity; + for (size_t i = 0; i < count; ++i) { + data[i] = r->buf[tail]; + tail = (tail + 1) % cap; + } + store_tail(r, tail); + return count; +} diff --git a/src/ringbuffer.h b/src/ringbuffer.h new file mode 100644 index 0000000..5d9fd58 --- /dev/null +++ b/src/ringbuffer.h @@ -0,0 +1,21 @@ +#ifndef RINGBUFFER_H +#define RINGBUFFER_H + +#include +#include + +typedef struct { + atomic_size_t head; + atomic_size_t tail; + size_t capacity; + float *buf; +} RingBuf; + +int ring_init(RingBuf *r, size_t capacity); +void ring_destroy(RingBuf *r); +size_t ring_readable(const RingBuf *r); +size_t ring_writeable(const RingBuf *r); +size_t ring_write(RingBuf *r, const float *data, size_t count); +size_t ring_read(RingBuf *r, float *data, size_t count); + +#endif diff --git a/src/wav.c b/src/wav.c new file mode 100644 index 0000000..63d8aeb --- /dev/null +++ b/src/wav.c @@ -0,0 +1,113 @@ +#include "wav.h" +#include "channel.h" +#include +#include +#include +#include +#include +#include + +static inline int read_uint16(int fd, uint16_t *v) { + return read(fd, v, sizeof(uint16_t)) == sizeof(uint16_t) ? 0 : -1; +} +static inline int read_uint32(int fd, uint32_t *v) { + return read(fd, v, sizeof(uint32_t)) == sizeof(uint32_t) ? 0 : -1; +} + +int wav_read(const char *path, float **buffer, unsigned *frames) { + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL); + char riff[4]; + if (read(fd, riff, 4) != 4 || memcmp(riff, "RIFF", 4) != 0) { close(fd); return -1; } + uint32_t chunk_size; + if (read_uint32(fd, &chunk_size) != 0) { close(fd); return -1; } + char wave[4]; + if (read(fd, wave, 4) != 4 || memcmp(wave, "WAVE", 4) != 0) { close(fd); return -1; } + uint32_t fmt_size = 0; + uint16_t audio_format = 0; + uint16_t num_channels = 0; + uint32_t sample_rate = 0; + uint16_t bits_per_sample = 0; + while (1) { + char sub_id[4]; + if (read(fd, sub_id, 4) != 4) { close(fd); return -1; } + if (read_uint32(fd, &fmt_size) != 0) { close(fd); return -1; } + if (memcmp(sub_id, "fmt ", 4) == 0) { + if (read_uint16(fd, &audio_format) != 0) { close(fd); return -1; } + if (read_uint16(fd, &num_channels) != 0) { close(fd); return -1; } + if (read_uint32(fd, &sample_rate) != 0) { close(fd); return -1; } + if (read_uint16(fd, &bits_per_sample) != 0){ close(fd); return -1; } + if (fmt_size > 16) lseek(fd, fmt_size - 16, SEEK_CUR); + continue; + } + if (memcmp(sub_id, "data", 4) == 0) break; + lseek(fd, fmt_size, SEEK_CUR); + } + if (audio_format != 1 || num_channels != 1 || bits_per_sample != 16) { + close(fd); return -1; + } + unsigned max_frames = LOOP_BUF_SIZE; + unsigned total_frames = 0; + float *buf = (float*)malloc(max_frames * sizeof(float)); + if (!buf) { close(fd); return -1; } + while (total_frames < max_frames) { + int16_t sample; + ssize_t n = read(fd, &sample, 2); + if (n < 2) break; + buf[total_frames++] = sample / 32768.0f; + } + close(fd); + *buffer = buf; + *frames = total_frames; + return 0; +} + +int wav_write(const char *path, const float *data, unsigned frames, unsigned sample_rate) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return -1; + posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL); + unsigned data_bytes = frames * 2; + unsigned file_size = 44 + data_bytes; + unsigned char header[44]; + memset(header, 0, 44); + memcpy(header, "RIFF", 4); + header[4] = (unsigned char)( file_size & 0xff); + header[5] = (unsigned char)((file_size>>8) & 0xff); + header[6] = (unsigned char)((file_size>>16) & 0xff); + header[7] = (unsigned char)((file_size>>24) & 0xff); + memcpy(header+8, "WAVE", 4); + memcpy(header+12, "fmt ", 4); + header[16]=16; header[17]=0; header[18]=0; header[19]=0; + header[20]=1; header[21]=0; + header[22]=1; header[23]=0; + unsigned sr = sample_rate; + header[24] = (unsigned char)( sr & 0xff); + header[25] = (unsigned char)((sr>>8) & 0xff); + header[26] = (unsigned char)((sr>>16)& 0xff); + header[27] = (unsigned char)((sr>>24)& 0xff); + unsigned br = sr * 2; + header[28] = (unsigned char)( br & 0xff); + header[29] = (unsigned char)((br>>8) & 0xff); + header[30] = (unsigned char)((br>>16)& 0xff); + header[31] = (unsigned char)((br>>24)& 0xff); + header[32]=2; header[33]=0; + header[34]=16; header[35]=0; + memcpy(header+36, "data", 4); + header[40] = (unsigned char)( data_bytes & 0xff); + header[41] = (unsigned char)((data_bytes>>8) & 0xff); + header[42] = (unsigned char)((data_bytes>>16)& 0xff); + header[43] = (unsigned char)((data_bytes>>24)& 0xff); + ssize_t written = write(fd, header, 44); + if (written != 44) { close(fd); return -1; } + for (unsigned i = 0; i < frames; ++i) { + float s = data[i]; + if (s < -1.0f) s = -1.0f; + if (s > 1.0f) s = 1.0f; + int16_t sample = (int16_t)(s * 32767); + written = write(fd, &sample, 2); + if (written != 2) { close(fd); return -1; } + } + close(fd); + return 0; +} diff --git a/src/wav.h b/src/wav.h new file mode 100644 index 0000000..e53a002 --- /dev/null +++ b/src/wav.h @@ -0,0 +1,9 @@ +#ifndef WAV_H +#define WAV_H + +#include + +int wav_read(const char *path, float **buffer, unsigned *frames); +int wav_write(const char *path, const float *data, unsigned frames, unsigned sample_rate); + +#endif