feat: add JACK audio looper with clip state machine and tests
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
28
Let's produce the blocks.makefile
Normal file
28
Let's produce the blocks.makefile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags jack`
|
||||||
|
LDFLAGS = `pkg-config --libs jack`
|
||||||
|
TARGET = jack-looper
|
||||||
|
SRCS = main.c engine.c
|
||||||
|
OBJS = $(SRCS:.c=.o)
|
||||||
|
TEST_SRCS = test_engine.c
|
||||||
|
TEST_OBJS = $(TEST_SRCS:.c=.o)
|
||||||
|
TEST_TARGET = test_engine
|
||||||
|
|
||||||
|
.PHONY: all clean test
|
||||||
|
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
|
%.o: %.c
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
test: $(TEST_TARGET)
|
||||||
|
./$(TEST_TARGET)
|
||||||
|
|
||||||
|
$(TEST_TARGET): $(TEST_OBJS) engine.o
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(OBJS) $(TEST_OBJS) $(TARGET) $(TEST_TARGET)
|
||||||
257
engine.c
Normal file
257
engine.c
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#include "engine.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
// JACK process callback
|
||||||
|
static int process_callback(jack_nframes_t nframes, void *arg) {
|
||||||
|
Engine *engine = (Engine *)arg;
|
||||||
|
|
||||||
|
// Get ports
|
||||||
|
jack_default_audio_sample_t *audio_in = (jack_default_audio_sample_t *)
|
||||||
|
jack_port_get_buffer(engine->audio_in_port, nframes);
|
||||||
|
jack_default_audio_sample_t *audio_out = (jack_default_audio_sample_t *)
|
||||||
|
jack_port_get_buffer(engine->audio_out_port, nframes);
|
||||||
|
void *midi_in_buf = jack_port_get_buffer(engine->midi_in_port, nframes);
|
||||||
|
void *midi_out_buf = jack_port_get_buffer(engine->midi_out_port, nframes);
|
||||||
|
|
||||||
|
// Clear output MIDI buffer
|
||||||
|
jack_midi_clear_buffer(midi_out_buf);
|
||||||
|
|
||||||
|
// Process MIDI input
|
||||||
|
jack_midi_event_t midi_event;
|
||||||
|
jack_nframes_t event_index = 0;
|
||||||
|
|
||||||
|
while (jack_midi_event_get(&midi_event, midi_in_buf, event_index) == 0) {
|
||||||
|
event_index++;
|
||||||
|
|
||||||
|
uint8_t *data = midi_event.buffer;
|
||||||
|
uint8_t status = data[0] & 0xF0;
|
||||||
|
uint8_t channel = data[0] & 0x0F;
|
||||||
|
uint8_t note = data[1];
|
||||||
|
uint8_t velocity = data[2];
|
||||||
|
|
||||||
|
// Only process note on messages on the control channel
|
||||||
|
if (status == 0x90 && channel == engine->control_channel && velocity > 0) {
|
||||||
|
int clip_index = note % MAX_CLIPS;
|
||||||
|
engine_trigger_clip(engine, clip_index);
|
||||||
|
|
||||||
|
// Send note with velocity representing state
|
||||||
|
uint8_t out_velocity = clip_state_to_velocity(engine->clips[clip_index].state);
|
||||||
|
uint8_t out_msg[3] = {0x90 | channel, note, out_velocity};
|
||||||
|
|
||||||
|
if (jack_midi_event_write(midi_out_buf, midi_event.time, out_msg, 3) != 0) {
|
||||||
|
fprintf(stderr, "Failed to write MIDI event\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pass through all other MIDI messages
|
||||||
|
if (jack_midi_event_write(midi_out_buf, midi_event.time,
|
||||||
|
midi_event.buffer, midi_event.size) != 0) {
|
||||||
|
fprintf(stderr, "Failed to write MIDI event\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process audio
|
||||||
|
memset(audio_out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
|
||||||
|
for (jack_nframes_t i = 0; i < nframes; i++) {
|
||||||
|
// Record input to recording clips
|
||||||
|
for (int c = 0; c < MAX_CLIPS; c++) {
|
||||||
|
Clip *clip = &engine->clips[c];
|
||||||
|
|
||||||
|
if (clip->state == CLIP_RECORDING) {
|
||||||
|
if (clip->write_position < MAX_BUFFER_SIZE) {
|
||||||
|
clip->buffer[clip->write_position++] = audio_in[i];
|
||||||
|
} else {
|
||||||
|
// Buffer full, stop recording
|
||||||
|
clip->state = CLIP_LOOPING;
|
||||||
|
clip->buffer_size = clip->write_position;
|
||||||
|
clip->read_position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play looping clips
|
||||||
|
if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) {
|
||||||
|
audio_out[i] += clip->buffer[clip->read_position];
|
||||||
|
clip->read_position = (clip->read_position + 1) % clip->buffer_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JACK shutdown callback
|
||||||
|
static void shutdown_callback(jack_status_t code, const char *reason, void *arg) {
|
||||||
|
Engine *engine = (Engine *)arg;
|
||||||
|
engine->running = false;
|
||||||
|
fprintf(stderr, "JACK shutdown: %s\n", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
int engine_init(Engine *engine, const char *client_name) {
|
||||||
|
if (!engine || !client_name) return -1;
|
||||||
|
|
||||||
|
memset(engine, 0, sizeof(Engine));
|
||||||
|
engine->control_channel = 0;
|
||||||
|
engine->running = false;
|
||||||
|
|
||||||
|
// Initialize clips
|
||||||
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
|
engine->clips[i].state = CLIP_EMPTY;
|
||||||
|
engine->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
|
||||||
|
if (!engine->clips[i].buffer) {
|
||||||
|
// Cleanup on allocation failure
|
||||||
|
for (int j = 0; j < i; j++) {
|
||||||
|
free(engine->clips[j].buffer);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
engine->clips[i].buffer_size = 0;
|
||||||
|
engine->clips[i].write_position = 0;
|
||||||
|
engine->clips[i].read_position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open JACK client
|
||||||
|
jack_status_t status;
|
||||||
|
engine->client = jack_client_open(client_name, JackNullOption, &status, NULL);
|
||||||
|
if (!engine->client) {
|
||||||
|
fprintf(stderr, "Failed to open JACK client, status = 0x%2.0x\n", status);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register ports
|
||||||
|
engine->audio_in_port = jack_port_register(engine->client, "audio_in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
engine->audio_out_port = jack_port_register(engine->client, "audio_out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
engine->midi_in_port = jack_port_register(engine->client, "midi_in",
|
||||||
|
JACK_DEFAULT_MIDI_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
engine->midi_out_port = jack_port_register(engine->client, "midi_out",
|
||||||
|
JACK_DEFAULT_MIDI_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
|
||||||
|
if (!engine->audio_in_port || !engine->audio_out_port ||
|
||||||
|
!engine->midi_in_port || !engine->midi_out_port) {
|
||||||
|
fprintf(stderr, "Failed to register ports\n");
|
||||||
|
engine_cleanup(engine);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set callbacks
|
||||||
|
jack_set_process_callback(engine->client, process_callback, engine);
|
||||||
|
jack_on_shutdown(engine->client, shutdown_callback, engine);
|
||||||
|
|
||||||
|
// Get sample rate
|
||||||
|
engine->sample_rate = jack_get_sample_rate(engine->client);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void engine_cleanup(Engine *engine) {
|
||||||
|
if (!engine) return;
|
||||||
|
|
||||||
|
if (engine->client) {
|
||||||
|
jack_client_close(engine->client);
|
||||||
|
engine->client = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
|
free(engine->clips[i].buffer);
|
||||||
|
engine->clips[i].buffer = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int engine_start(Engine *engine) {
|
||||||
|
if (!engine || !engine->client) return -1;
|
||||||
|
|
||||||
|
if (jack_activate(engine->client) != 0) {
|
||||||
|
fprintf(stderr, "Failed to activate JACK client\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
engine->running = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void engine_stop(Engine *engine) {
|
||||||
|
if (!engine || !engine->client) return;
|
||||||
|
|
||||||
|
engine->running = false;
|
||||||
|
jack_deactivate(engine->client);
|
||||||
|
}
|
||||||
|
|
||||||
|
void engine_trigger_clip(Engine *engine, int clip_index) {
|
||||||
|
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
|
||||||
|
|
||||||
|
Clip *clip = &engine->clips[clip_index];
|
||||||
|
|
||||||
|
switch (clip->state) {
|
||||||
|
case CLIP_EMPTY:
|
||||||
|
// Start recording
|
||||||
|
clip->state = CLIP_RECORDING;
|
||||||
|
clip->write_position = 0;
|
||||||
|
clip->buffer_size = 0;
|
||||||
|
clip->read_position = 0;
|
||||||
|
printf("Clip %d: Recording started\n", clip_index);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIP_RECORDING:
|
||||||
|
// Stop recording, start looping
|
||||||
|
clip->state = CLIP_LOOPING;
|
||||||
|
clip->buffer_size = clip->write_position;
|
||||||
|
clip->read_position = 0;
|
||||||
|
printf("Clip %d: Recording stopped, looping %zu samples\n",
|
||||||
|
clip_index, clip->buffer_size);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIP_LOOPING:
|
||||||
|
// Stop looping
|
||||||
|
clip->state = CLIP_STOPPED;
|
||||||
|
clip->read_position = 0;
|
||||||
|
printf("Clip %d: Looping stopped\n", clip_index);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIP_STOPPED:
|
||||||
|
// Start looping again
|
||||||
|
clip->state = CLIP_LOOPING;
|
||||||
|
clip->read_position = 0;
|
||||||
|
printf("Clip %d: Looping resumed\n", clip_index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void engine_reset_clip(Engine *engine, int clip_index) {
|
||||||
|
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
|
||||||
|
|
||||||
|
Clip *clip = &engine->clips[clip_index];
|
||||||
|
clip->state = CLIP_EMPTY;
|
||||||
|
clip->buffer_size = 0;
|
||||||
|
clip->write_position = 0;
|
||||||
|
clip->read_position = 0;
|
||||||
|
memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* clip_state_to_string(ClipState state) {
|
||||||
|
switch (state) {
|
||||||
|
case CLIP_EMPTY: return "Empty";
|
||||||
|
case CLIP_RECORDING: return "Recording";
|
||||||
|
case CLIP_LOOPING: return "Looping";
|
||||||
|
case CLIP_STOPPED: return "Stopped";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t clip_state_to_velocity(ClipState state) {
|
||||||
|
switch (state) {
|
||||||
|
case CLIP_EMPTY: return 0;
|
||||||
|
case CLIP_RECORDING: return 64;
|
||||||
|
case CLIP_LOOPING: return 127;
|
||||||
|
case CLIP_STOPPED: return 32;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
engine.h
Normal file
56
engine.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#ifndef ENGINE_H
|
||||||
|
#define ENGINE_H
|
||||||
|
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <jack/midiport.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define MAX_CLIPS 128
|
||||||
|
#define MAX_BUFFER_SIZE 441000 // 10 seconds at 44.1kHz
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CLIP_EMPTY,
|
||||||
|
CLIP_RECORDING,
|
||||||
|
CLIP_LOOPING,
|
||||||
|
CLIP_STOPPED
|
||||||
|
} ClipState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ClipState state;
|
||||||
|
float *buffer;
|
||||||
|
size_t buffer_size;
|
||||||
|
size_t write_position;
|
||||||
|
size_t read_position;
|
||||||
|
bool is_playing;
|
||||||
|
} Clip;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_port_t *audio_in_port;
|
||||||
|
jack_port_t *audio_out_port;
|
||||||
|
jack_port_t *midi_in_port;
|
||||||
|
jack_port_t *midi_out_port;
|
||||||
|
|
||||||
|
Clip clips[MAX_CLIPS];
|
||||||
|
int control_channel;
|
||||||
|
jack_nframes_t sample_rate;
|
||||||
|
|
||||||
|
bool running;
|
||||||
|
} Engine;
|
||||||
|
|
||||||
|
// Engine lifecycle
|
||||||
|
int engine_init(Engine *engine, const char *client_name);
|
||||||
|
void engine_cleanup(Engine *engine);
|
||||||
|
int engine_start(Engine *engine);
|
||||||
|
void engine_stop(Engine *engine);
|
||||||
|
|
||||||
|
// Clip management
|
||||||
|
void engine_trigger_clip(Engine *engine, int clip_index);
|
||||||
|
void engine_reset_clip(Engine *engine, int clip_index);
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
const char* clip_state_to_string(ClipState state);
|
||||||
|
uint8_t clip_state_to_velocity(ClipState state);
|
||||||
|
|
||||||
|
#endif // ENGINE_H
|
||||||
84
main.c
84
main.c
@@ -0,0 +1,84 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include "engine.h"
|
||||||
|
|
||||||
|
static Engine engine;
|
||||||
|
static volatile int keep_running = 1;
|
||||||
|
|
||||||
|
void signal_handler(int sig) {
|
||||||
|
keep_running = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_usage(const char *program) {
|
||||||
|
printf("Usage: %s [options]\n", program);
|
||||||
|
printf("Options:\n");
|
||||||
|
printf(" -n <name> JACK client name (default: jack-looper)\n");
|
||||||
|
printf(" -c <channel> MIDI control channel (default: 0)\n");
|
||||||
|
printf(" -h Show this help\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
const char *client_name = "jack-looper";
|
||||||
|
int control_channel = 0;
|
||||||
|
int opt;
|
||||||
|
|
||||||
|
while ((opt = getopt(argc, argv, "n:c:h")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'n':
|
||||||
|
client_name = optarg;
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
control_channel = atoi(optarg);
|
||||||
|
if (control_channel < 0 || control_channel > 15) {
|
||||||
|
fprintf(stderr, "Control channel must be 0-15\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize engine
|
||||||
|
if (engine_init(&engine, client_name) != 0) {
|
||||||
|
fprintf(stderr, "Failed to initialize engine\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.control_channel = control_channel;
|
||||||
|
|
||||||
|
// Set up signal handler
|
||||||
|
signal(SIGINT, signal_handler);
|
||||||
|
signal(SIGTERM, signal_handler);
|
||||||
|
|
||||||
|
// Start engine
|
||||||
|
if (engine_start(&engine) != 0) {
|
||||||
|
fprintf(stderr, "Failed to start engine\n");
|
||||||
|
engine_cleanup(&engine);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("JACK Looper started\n");
|
||||||
|
printf("Client name: %s\n", client_name);
|
||||||
|
printf("Control channel: %d\n", control_channel);
|
||||||
|
printf("Sample rate: %u Hz\n", engine.sample_rate);
|
||||||
|
printf("Press Ctrl+C to stop\n\n");
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
while (keep_running) {
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
printf("\nShutting down...\n");
|
||||||
|
engine_stop(&engine);
|
||||||
|
engine_cleanup(&engine);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
278
test_engine.c
Normal file
278
test_engine.c
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include "engine.h"
|
||||||
|
|
||||||
|
// Test helper
|
||||||
|
static Engine *create_test_engine(void) {
|
||||||
|
Engine *engine = (Engine *)calloc(1, sizeof(Engine));
|
||||||
|
assert(engine != NULL);
|
||||||
|
|
||||||
|
engine->control_channel = 0;
|
||||||
|
engine->sample_rate = 48000;
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
|
engine->clips[i].state = CLIP_EMPTY;
|
||||||
|
engine->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
|
||||||
|
assert(engine->clips[i].buffer != NULL);
|
||||||
|
engine->clips[i].buffer_size = 0;
|
||||||
|
engine->clips[i].write_position = 0;
|
||||||
|
engine->clips[i].read_position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void destroy_test_engine(Engine *engine) {
|
||||||
|
if (engine) {
|
||||||
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
|
free(engine->clips[i].buffer);
|
||||||
|
}
|
||||||
|
free(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 1: Initial state is empty
|
||||||
|
void test_initial_state(void) {
|
||||||
|
printf("Test 1: Initial state is empty... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
|
assert(engine->clips[i].state == CLIP_EMPTY);
|
||||||
|
assert(engine->clips[i].buffer_size == 0);
|
||||||
|
assert(engine->clips[i].write_position == 0);
|
||||||
|
assert(engine->clips[i].read_position == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Trigger empty clip starts recording
|
||||||
|
void test_trigger_empty_starts_recording(void) {
|
||||||
|
printf("Test 2: Trigger empty clip starts recording... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||||
|
assert(engine->clips[0].write_position == 0);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Trigger recording clip stops and starts looping
|
||||||
|
void test_trigger_recording_starts_looping(void) {
|
||||||
|
printf("Test 3: Trigger recording clip stops and starts looping... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||||
|
|
||||||
|
// Simulate some recording
|
||||||
|
engine->clips[0].write_position = 100;
|
||||||
|
|
||||||
|
// Trigger again to stop recording and start looping
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||||
|
assert(engine->clips[0].buffer_size == 100);
|
||||||
|
assert(engine->clips[0].read_position == 0);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Trigger looping clip stops it
|
||||||
|
void test_trigger_looping_stops(void) {
|
||||||
|
printf("Test 4: Trigger looping clip stops it... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// Set up a looping clip
|
||||||
|
engine->clips[0].state = CLIP_LOOPING;
|
||||||
|
engine->clips[0].buffer_size = 100;
|
||||||
|
engine->clips[0].read_position = 50;
|
||||||
|
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_STOPPED);
|
||||||
|
assert(engine->clips[0].read_position == 0);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Trigger stopped clip starts looping again
|
||||||
|
void test_trigger_stopped_resumes_looping(void) {
|
||||||
|
printf("Test 5: Trigger stopped clip starts looping again... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// Set up a stopped clip
|
||||||
|
engine->clips[0].state = CLIP_STOPPED;
|
||||||
|
engine->clips[0].buffer_size = 100;
|
||||||
|
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||||
|
assert(engine->clips[0].read_position == 0);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 6: Full cycle test
|
||||||
|
void test_full_cycle(void) {
|
||||||
|
printf("Test 6: Full cycle test... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// Empty -> Recording
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||||
|
|
||||||
|
// Recording -> Looping
|
||||||
|
engine->clips[0].write_position = 200;
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||||
|
assert(engine->clips[0].buffer_size == 200);
|
||||||
|
|
||||||
|
// Looping -> Stopped
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_STOPPED);
|
||||||
|
|
||||||
|
// Stopped -> Looping
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 7: Multiple clips work independently
|
||||||
|
void test_multiple_clips(void) {
|
||||||
|
printf("Test 7: Multiple clips work independently... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// Clip 0: Empty -> Recording
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||||
|
|
||||||
|
// Clip 1: Empty -> Recording
|
||||||
|
engine_trigger_clip(engine, 1);
|
||||||
|
assert(engine->clips[1].state == CLIP_RECORDING);
|
||||||
|
|
||||||
|
// Clip 0: Recording -> Looping
|
||||||
|
engine->clips[0].write_position = 100;
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||||
|
assert(engine->clips[1].state == CLIP_RECORDING); // Clip 1 unaffected
|
||||||
|
|
||||||
|
// Clip 2: Empty -> Recording
|
||||||
|
engine_trigger_clip(engine, 2);
|
||||||
|
assert(engine->clips[2].state == CLIP_RECORDING);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 8: Reset clip
|
||||||
|
void test_reset_clip(void) {
|
||||||
|
printf("Test 8: Reset clip... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// Set up a clip with data
|
||||||
|
engine->clips[0].state = CLIP_LOOPING;
|
||||||
|
engine->clips[0].buffer_size = 100;
|
||||||
|
engine->clips[0].write_position = 100;
|
||||||
|
engine->clips[0].read_position = 50;
|
||||||
|
|
||||||
|
engine_reset_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_EMPTY);
|
||||||
|
assert(engine->clips[0].buffer_size == 0);
|
||||||
|
assert(engine->clips[0].write_position == 0);
|
||||||
|
assert(engine->clips[0].read_position == 0);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 9: Clip state to velocity mapping
|
||||||
|
void test_state_to_velocity(void) {
|
||||||
|
printf("Test 9: Clip state to velocity mapping... ");
|
||||||
|
|
||||||
|
assert(clip_state_to_velocity(CLIP_EMPTY) == 0);
|
||||||
|
assert(clip_state_to_velocity(CLIP_RECORDING) == 64);
|
||||||
|
assert(clip_state_to_velocity(CLIP_LOOPING) == 127);
|
||||||
|
assert(clip_state_to_velocity(CLIP_STOPPED) == 32);
|
||||||
|
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 10: Clip state to string
|
||||||
|
void test_state_to_string(void) {
|
||||||
|
printf("Test 10: Clip state to string... ");
|
||||||
|
|
||||||
|
assert(strcmp(clip_state_to_string(CLIP_EMPTY), "Empty") == 0);
|
||||||
|
assert(strcmp(clip_state_to_string(CLIP_RECORDING), "Recording") == 0);
|
||||||
|
assert(strcmp(clip_state_to_string(CLIP_LOOPING), "Looping") == 0);
|
||||||
|
assert(strcmp(clip_state_to_string(CLIP_STOPPED), "Stopped") == 0);
|
||||||
|
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 11: Invalid clip index
|
||||||
|
void test_invalid_clip_index(void) {
|
||||||
|
printf("Test 11: Invalid clip index... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// These should not crash
|
||||||
|
engine_trigger_clip(engine, -1);
|
||||||
|
engine_trigger_clip(engine, MAX_CLIPS);
|
||||||
|
engine_reset_clip(engine, -1);
|
||||||
|
engine_reset_clip(engine, MAX_CLIPS);
|
||||||
|
|
||||||
|
// Verify no state changes
|
||||||
|
assert(engine->clips[0].state == CLIP_EMPTY);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 12: Buffer overflow protection
|
||||||
|
void test_buffer_overflow(void) {
|
||||||
|
printf("Test 12: Buffer overflow protection... ");
|
||||||
|
Engine *engine = create_test_engine();
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||||
|
|
||||||
|
// Fill buffer to max
|
||||||
|
engine->clips[0].write_position = MAX_BUFFER_SIZE;
|
||||||
|
|
||||||
|
// Trigger should stop recording and start looping
|
||||||
|
engine_trigger_clip(engine, 0);
|
||||||
|
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||||
|
assert(engine->clips[0].buffer_size == MAX_BUFFER_SIZE);
|
||||||
|
|
||||||
|
destroy_test_engine(engine);
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
printf("Running JACK Looper tests...\n\n");
|
||||||
|
|
||||||
|
test_initial_state();
|
||||||
|
test_trigger_empty_starts_recording();
|
||||||
|
test_trigger_recording_starts_looping();
|
||||||
|
test_trigger_looping_stops();
|
||||||
|
test_trigger_stopped_resumes_looping();
|
||||||
|
test_full_cycle();
|
||||||
|
test_multiple_clips();
|
||||||
|
test_reset_clip();
|
||||||
|
test_state_to_velocity();
|
||||||
|
test_state_to_string();
|
||||||
|
test_invalid_clip_index();
|
||||||
|
test_buffer_overflow();
|
||||||
|
|
||||||
|
printf("\nAll tests passed!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user