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>
207 lines
6.3 KiB
C
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;
|
|
}
|