feat: add ncurses-based TUI frontend with 8x8 clip grid and keyboard controls

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-01 09:48:59 +00:00
parent 9b1959e13d
commit 4573eb0201
5 changed files with 775 additions and 11 deletions

View File

@@ -0,0 +1,474 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "engine.h"
#include "tui.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;
engine->quantize_mode = QUANTIZE_OFF;
engine->quantize_threshold = 0;
engine->queued_triggers = NULL;
// Initialize transport
engine->transport.rolling = false;
engine->transport.clock_count = 0;
engine->transport.beat_position = 0;
engine->transport.bar_position = 0;
engine->transport.sample_position = 0;
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) {
QueuedTrigger *qt = engine->queued_triggers;
while (qt) {
QueuedTrigger *next = qt->next;
free(qt);
qt = next;
}
for (int i = 0; i < MAX_CLIPS; i++) {
free(engine->clips[i].buffer);
}
free(engine);
}
}
// Test 1: Grid to clip index mapping
void test_grid_to_clip_index(void) {
printf("Test 1: Grid to clip index mapping... ");
// 8x8 grid should map to 64 clips
assert(0 * 8 + 0 == 0); // Top-left
assert(0 * 8 + 7 == 7); // Top-right
assert(7 * 8 + 0 == 56); // Bottom-left
assert(7 * 8 + 7 == 63); // Bottom-right
assert(3 * 8 + 4 == 28); // Middle
printf("PASSED\n");
}
// Test 2: Trigger clip via grid position
void test_trigger_via_grid(void) {
printf("Test 2: Trigger clip via grid position... ");
Engine *engine = create_test_engine();
// Simulate pressing 't' on grid position (3, 4) = clip 28
int clip_idx = 3 * 8 + 4;
engine_trigger_clip(engine, clip_idx);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 3: Reset clip via grid position
void test_reset_via_grid(void) {
printf("Test 3: Reset clip via grid position... ");
Engine *engine = create_test_engine();
// Set up a clip at grid position (1, 2) = clip 10
int clip_idx = 1 * 8 + 2;
engine->clips[clip_idx].state = CLIP_LOOPING;
engine->clips[clip_idx].buffer_size = 100;
// Simulate pressing 'r'
engine_reset_clip(engine, clip_idx);
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
assert(engine->clips[clip_idx].buffer_size == 0);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 4: Scene trigger via grid row
void test_scene_via_grid(void) {
printf("Test 4: Scene trigger via grid row... ");
Engine *engine = create_test_engine();
// Simulate pressing 's' on row 3
int scene_index = 3;
engine_trigger_scene(engine, scene_index);
// All clips in scene 3 should be recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_index, ch);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
}
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 5: Quantize mode cycling
void test_quantize_cycling(void) {
printf("Test 5: Quantize mode cycling... ");
Engine *engine = create_test_engine();
// Simulate pressing 'q' to cycle through modes
assert(engine->quantize_mode == QUANTIZE_OFF);
// Cycle: OFF -> BEAT
engine_set_quantize_mode(engine, QUANTIZE_BEAT);
assert(engine->quantize_mode == QUANTIZE_BEAT);
// Cycle: BEAT -> BAR
engine_set_quantize_mode(engine, QUANTIZE_BAR);
assert(engine->quantize_mode == QUANTIZE_BAR);
// Cycle: BAR -> OFF
engine_set_quantize_mode(engine, QUANTIZE_OFF);
assert(engine->quantize_mode == QUANTIZE_OFF);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 6: Threshold toggling
void test_threshold_toggle(void) {
printf("Test 6: Threshold toggling... ");
Engine *engine = create_test_engine();
// Simulate pressing 'T' to toggle threshold
assert(engine->quantize_threshold == 0);
// Toggle to 1000
engine_set_quantize_threshold(engine, 1000);
assert(engine->quantize_threshold == 1000);
// Toggle back to 0
engine_set_quantize_threshold(engine, 0);
assert(engine->quantize_threshold == 0);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 7: Transport reset
void test_transport_reset_via_tui(void) {
printf("Test 7: Transport reset via TUI... ");
Engine *engine = create_test_engine();
// Set up transport state
engine->transport.rolling = true;
engine->transport.clock_count = 100;
engine->transport.beat_position = 2;
engine->transport.bar_position = 5;
engine->transport.sample_position = 10000;
// Simulate pressing 'x'
engine_reset_transport(engine);
assert(engine->transport.rolling == false);
assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0);
assert(engine->transport.sample_position == 0);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 8: Navigation wrapping
void test_navigation_wrapping(void) {
printf("Test 8: Navigation wrapping... ");
// Test that navigation wraps around the grid
// Left from column 0 should go to column 7
int col = 0;
col = (col - 1 + 8) % 8;
assert(col == 7);
// Right from column 7 should go to column 0
col = 7;
col = (col + 1) % 8;
assert(col == 0);
// Up from row 0 should go to row 7
int row = 0;
row = (row - 1 + 8) % 8;
assert(row == 7);
// Down from row 7 should go to row 0
row = 7;
row = (row + 1) % 8;
assert(row == 0);
printf("PASSED\n");
}
// Test 9: Multiple clips in different states
void test_multiple_clip_states(void) {
printf("Test 9: Multiple clips in different states... ");
Engine *engine = create_test_engine();
// Set up clips in various states
engine->clips[0].state = CLIP_EMPTY;
engine->clips[1].state = CLIP_RECORDING;
engine->clips[2].state = CLIP_LOOPING;
engine->clips[3].state = CLIP_STOPPED;
// Verify states
assert(engine->clips[0].state == CLIP_EMPTY);
assert(engine->clips[1].state == CLIP_RECORDING);
assert(engine->clips[2].state == CLIP_LOOPING);
assert(engine->clips[3].state == CLIP_STOPPED);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 10: Buffer size display
void test_buffer_size_display(void) {
printf("Test 10: Buffer size display... ");
Engine *engine = create_test_engine();
// Set up a clip with known buffer size
engine->clips[5].state = CLIP_LOOPING;
engine->clips[5].buffer_size = 48000; // 1 second at 48kHz
// Verify buffer size
assert(engine->clips[5].buffer_size == 48000);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 11: Help toggle
void test_help_toggle(void) {
printf("Test 11: Help toggle... ");
// Test that help flag toggles correctly
bool show_help = false;
show_help = !show_help;
assert(show_help == true);
show_help = !show_help;
assert(show_help == false);
printf("PASSED\n");
}
// Test 12: Escape key handling
void test_escape_handling(void) {
printf("Test 12: Escape key handling... ");
// Test that escape key (27) is handled
int ch = 27;
assert(ch == 27); // Escape
// Test that 'Q' is handled
ch = 'Q';
assert(ch == 'Q');
printf("PASSED\n");
}
// Test 13: TUI init and cleanup (without ncurses)
void test_tui_init_cleanup(void) {
printf("Test 13: TUI init and cleanup... ");
Engine *engine = create_test_engine();
// We can't actually call tui_init/tui_cleanup in test environment
// but we can verify the engine is valid
assert(engine != NULL);
assert(engine->sample_rate == 48000);
destroy_test_engine(engine);
printf("PASSED (skipped ncurses init)\n");
}
// Test 14: State to color mapping
void test_state_to_color_mapping(void) {
printf("Test 14: State to color mapping... ");
// Verify state values match expected color indices
assert(CLIP_EMPTY == 0);
assert(CLIP_RECORDING == 1);
assert(CLIP_LOOPING == 2);
assert(CLIP_STOPPED == 3);
printf("PASSED\n");
}
// Test 15: Full grid coverage
void test_full_grid_coverage(void) {
printf("Test 15: Full grid coverage... ");
Engine *engine = create_test_engine();
// Trigger all 64 clips via grid positions
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
int clip_idx = row * 8 + col;
engine_trigger_clip(engine, clip_idx);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
}
}
// Verify all clips are recording
for (int i = 0; i < MAX_CLIPS; i++) {
assert(engine->clips[i].state == CLIP_RECORDING);
}
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 16: Scene trigger from each row
void test_scene_from_each_row(void) {
printf("Test 16: Scene trigger from each row... ");
Engine *engine = create_test_engine();
// Trigger scene from each row
for (int row = 0; row < 8; row++) {
engine_trigger_scene(engine, row);
// Verify all clips in this scene are recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(row, ch);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
}
}
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 17: Quantize mode cycle through all modes
void test_quantize_full_cycle(void) {
printf("Test 17: Quantize mode full cycle... ");
Engine *engine = create_test_engine();
// Cycle through all modes twice
for (int cycle = 0; cycle < 2; cycle++) {
engine_set_quantize_mode(engine, QUANTIZE_OFF);
assert(engine->quantize_mode == QUANTIZE_OFF);
engine_set_quantize_mode(engine, QUANTIZE_BEAT);
assert(engine->quantize_mode == QUANTIZE_BEAT);
engine_set_quantize_mode(engine, QUANTIZE_BAR);
assert(engine->quantize_mode == QUANTIZE_BAR);
}
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 18: Multiple threshold toggles
void test_multiple_threshold_toggles(void) {
printf("Test 18: Multiple threshold toggles... ");
Engine *engine = create_test_engine();
// Toggle threshold multiple times
for (int i = 0; i < 5; i++) {
if (engine->quantize_threshold == 0) {
engine_set_quantize_threshold(engine, 1000);
assert(engine->quantize_threshold == 1000);
} else {
engine_set_quantize_threshold(engine, 0);
assert(engine->quantize_threshold == 0);
}
}
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 19: Transport reset multiple times
void test_multiple_transport_resets(void) {
printf("Test 19: Multiple transport resets... ");
Engine *engine = create_test_engine();
// Reset transport multiple times
for (int i = 0; i < 5; i++) {
engine->transport.rolling = true;
engine->transport.clock_count = 100 + i;
engine->transport.beat_position = i % 4;
engine->transport.bar_position = i;
engine->transport.sample_position = 10000 * i;
engine_reset_transport(engine);
assert(engine->transport.rolling == false);
assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0);
assert(engine->transport.sample_position == 0);
}
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 20: Navigation with arrow keys
void test_arrow_key_navigation(void) {
printf("Test 20: Arrow key navigation... ");
// Test that arrow keys produce same results as hjkl
int row = 3, col = 4;
// KEY_LEFT (same as 'h')
col = (col - 1 + 8) % 8;
assert(col == 3);
// KEY_DOWN (same as 'j')
row = (row + 1) % 8;
assert(row == 4);
// KEY_UP (same as 'k')
row = (row - 1 + 8) % 8;
assert(row == 3);
// KEY_RIGHT (same as 'l')
col = (col + 1) % 8;
assert(col == 4);
printf("PASSED\n");
}
int main(void) {
printf("Running TUI tests...\n\n");
test_grid_to_clip_index();
test_trigger_via_grid();
test_reset_via_grid();
test_scene_via_grid();
test_quantize_cycling();
test_threshold_toggle();
test_transport_reset_via_tui();
test_navigation_wrapping();
test_multiple_clip_states();
test_buffer_size_display();
test_help_toggle();
test_escape_handling();
test_tui_init_cleanup();
test_state_to_color_mapping();
test_full_grid_coverage();
test_scene_from_each_row();
test_quantize_full_cycle();
test_multiple_threshold_toggles();
test_multiple_transport_resets();
test_arrow_key_navigation();
printf("\nAll TUI tests passed!\n");
return 0;
}