Files
midi-sequencer/c-backend/src/main.c
mikael-lovqvists-claude-agent 9b45905d80 Add multi-track playback with mute/solo support
Replaces the single-root-pattern sequencer with a Track[] array that
allows multiple patterns to loop independently. Adds ADD_TRACK (0x0A),
REMOVE_TRACK (0x0B), PLAY_TRACKS (0x0C), and SET_TRACK_MUTE (0x0D)
protocol records. The C backend gains per-track pending_subs and a
tracks_mutex. The Node server gains track-state APIs (/api/tracks/:id/
active, mute, solo) and the frontend shows per-pattern track/mute/solo
buttons in the sidebar list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 04:16:06 +00:00

207 lines
6.3 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include "pattern_store.h"
#include "sequencer.h"
#include "socket_server.h"
#include "../generated/protocol.h"
/* ── Application context ──────────────────────────────────────────── */
typedef struct {
Pattern_Store ps;
Sequencer seq;
Socket_Server srv;
} App;
static App g_app;
/* ── Send callback for sequencer ─────────────────────────────────── */
static void seq_send(void *ctx, const uint8_t *buf, int len) {
App *app = (App *)ctx;
socket_server_send(&app->srv, buf, len);
}
/* ── Frame handler ────────────────────────────────────────────────── */
static void send_ack(App *app, uint8_t acked_type) {
uint8_t buf[16];
Msg_Ack ack = { .acked_type = acked_type };
int len = proto_encode_ack(buf, &ack);
socket_server_send(&app->srv, buf, len);
}
static void send_error(App *app, uint8_t code, uint8_t context_type) {
uint8_t buf[16];
Msg_Error err = { .code = code, .context_type = context_type };
int len = proto_encode_error(buf, &err);
socket_server_send(&app->srv, buf, len);
}
static void on_frame(uint8_t rt, const uint8_t *payload, uint16_t payload_len, void *ctx) {
App *app = (App *)ctx;
switch (rt) {
case RT_HELLO: {
Msg_Hello msg;
if (proto_decode_hello(payload, payload_len, &msg) < 0) goto bad_payload;
fprintf(stderr, "main: HELLO from client, version=%u\n", msg.version);
/* Reply with our HELLO */
uint8_t buf[16];
Msg_Hello reply = { .version = PROTOCOL_VERSION };
int len = proto_encode_hello(buf, &reply);
socket_server_send(&app->srv, buf, len);
return;
}
case RT_DEFINE_PATTERN: {
Msg_Define_Pattern msg;
if (proto_decode_define_pattern(payload, payload_len, &msg) < 0) goto bad_payload;
pattern_store_define(&app->ps, msg.pattern_id, msg.steps, msg.channel);
break;
}
case RT_CLEAR_PATTERN: {
Msg_Clear_Pattern msg;
if (proto_decode_clear_pattern(payload, payload_len, &msg) < 0) goto bad_payload;
pattern_store_clear(&app->ps, msg.pattern_id);
break;
}
case RT_ADD_NOTE: {
Msg_Add_Note msg;
if (proto_decode_add_note(payload, payload_len, &msg) < 0) goto bad_payload;
if (pattern_store_add_note(&app->ps, msg.pattern_id, msg.step,
msg.note, msg.velocity, msg.duration_steps) < 0) {
send_error(app, 0x01, rt);
return;
}
break;
}
case RT_ADD_SUB_PATTERN: {
Msg_Add_Sub_Pattern msg;
if (proto_decode_add_sub_pattern(payload, payload_len, &msg) < 0) goto bad_payload;
if (pattern_store_add_sub_ref(&app->ps, msg.pattern_id, msg.step,
msg.sub_pattern_id) < 0) {
send_error(app, 0x02, rt);
return;
}
break;
}
case RT_PLAY: {
Msg_Play msg;
if (proto_decode_play(payload, payload_len, &msg) < 0) goto bad_payload;
sequencer_stop(&app->seq);
sequencer_add_track(&app->seq, msg.pattern_id);
sequencer_play(&app->seq);
break;
}
case RT_STOP: {
sequencer_stop(&app->seq);
break;
}
case RT_SET_TEMPO: {
Msg_Set_Tempo msg;
if (proto_decode_set_tempo(payload, payload_len, &msg) < 0) goto bad_payload;
sequencer_set_tempo(&app->seq, msg.bpm_x10);
break;
}
case RT_PREVIEW_NOTE: {
Msg_Preview_Note msg;
if (proto_decode_preview_note(payload, payload_len, &msg) < 0) goto bad_payload;
sequencer_preview_note(&app->seq, msg.channel, msg.note, msg.velocity, msg.duration_ms);
break;
}
case RT_ADD_TRACK: {
Msg_Add_Track msg;
if (proto_decode_add_track(payload, payload_len, &msg) < 0) goto bad_payload;
sequencer_add_track(&app->seq, msg.pattern_id);
break;
}
case RT_REMOVE_TRACK: {
Msg_Remove_Track msg;
if (proto_decode_remove_track(payload, payload_len, &msg) < 0) goto bad_payload;
sequencer_remove_track(&app->seq, msg.pattern_id);
break;
}
case RT_PLAY_TRACKS: {
sequencer_play(&app->seq);
break;
}
case RT_SET_TRACK_MUTE: {
Msg_Set_Track_Mute msg;
if (proto_decode_set_track_mute(payload, payload_len, &msg) < 0) goto bad_payload;
sequencer_set_track_mute(&app->seq, msg.pattern_id, msg.muted);
break;
}
default:
fprintf(stderr, "main: unknown record type 0x%02x\n", rt);
return;
}
send_ack(app, rt);
return;
bad_payload:
fprintf(stderr, "main: bad payload for record type 0x%02x\n", rt);
send_error(app, 0xFF, rt);
}
/* ── Connect / disconnect callbacks ──────────────────────────────── */
static void on_connect(int fd, void *ctx) {
(void)fd;
(void)ctx;
fprintf(stderr, "main: node client connected\n");
}
static void on_disconnect(void *ctx) {
App *app = (App *)ctx;
sequencer_stop(&app->seq);
fprintf(stderr, "main: node client disconnected, playback stopped\n");
}
/* ── Signal handler ───────────────────────────────────────────────── */
static volatile int g_running = 1;
static void on_signal(int sig) { (void)sig; g_running = 0; }
/* ── Entry point ──────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
const char *socket_path = argc > 1 ? argv[1] : "/tmp/midi-sequencer.sock";
const char *alsa_name = argc > 2 ? argv[2] : "midi-sequencer";
fprintf(stderr, "midi-sequencer starting\n");
fprintf(stderr, " socket : %s\n", socket_path);
fprintf(stderr, " ALSA : %s\n", alsa_name);
pattern_store_init(&g_app.ps);
if (sequencer_init(&g_app.seq, &g_app.ps, alsa_name, seq_send, &g_app) < 0) {
fprintf(stderr, "main: sequencer init failed\n");
return 1;
}
if (socket_server_start(&g_app.srv, socket_path,
on_frame, on_connect, on_disconnect, &g_app) < 0) {
fprintf(stderr, "main: socket server failed to start\n");
return 1;
}
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
fprintf(stderr, "midi-sequencer ready, waiting for node client\n");
while (g_running) {
sleep(1);
}
fprintf(stderr, "midi-sequencer shutting down\n");
socket_server_stop(&g_app.srv);
sequencer_destroy(&g_app.seq);
pattern_store_destroy(&g_app.ps);
return 0;
}