Initial scaffold: ALSA MIDI C backend + Node ESM sequencer web app

- protocol.yaml: SSoT for the binary framing protocol (12 record types)
- codegen/gen.mjs: generates C header/source and Node ESM from protocol.yaml
- c-backend: ALSA sequencer with drift-free clock_nanosleep tick thread,
  pattern store (hierarchical sub-patterns), Unix socket server
- node-server: Express 5 web app — REST API, SSE for real-time beat events,
  step-sequencer frontend with pending-edit / explicit-save flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:54:38 +00:00
commit 9aba8057a8
23 changed files with 3037 additions and 0 deletions

20
c-backend/Makefile Normal file
View File

@@ -0,0 +1,20 @@
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -D_GNU_SOURCE -Isrc -Igenerated -O2
LDFLAGS = -lasound -lpthread
TARGET = midi-sequencer
SRCS = src/main.c \
src/socket_server.c \
src/pattern_store.c \
src/sequencer.c \
generated/protocol.c
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(SRCS) src/socket_server.h src/pattern_store.h src/sequencer.h generated/protocol.h
$(CC) $(CFLAGS) -o $@ $(SRCS) $(LDFLAGS)
clean:
rm -f $(TARGET)

View File

@@ -0,0 +1,215 @@
/* AUTO-GENERATED by codegen/gen.mjs — DO NOT EDIT */
#include "protocol.h"
/* ── serialization helpers ───────────────────────────────────── */
static void put_u8(uint8_t *buf, int *off, uint8_t v) {
buf[(*off)++] = v;
}
static void put_u16(uint8_t *buf, int *off, uint16_t v) {
buf[(*off)++] = (uint8_t)(v & 0xFF);
buf[(*off)++] = (uint8_t)(v >> 8);
}
static uint8_t get_u8(const uint8_t *buf, int *off) {
return buf[(*off)++];
}
static uint16_t get_u16(const uint8_t *buf, int *off) {
uint16_t v = (uint16_t)((uint16_t)buf[*off] | ((uint16_t)buf[*off + 1] << 8));
*off += 2;
return v;
}
static int write_frame(uint8_t *buf, uint8_t record_type, int payload_len) {
buf[0] = record_type;
buf[1] = (uint8_t)(payload_len & 0xFF);
buf[2] = (uint8_t)(payload_len >> 8);
return FRAME_HEADER_SIZE + payload_len;
}
/* ── encode ──────────────────────────────────────────────────── */
int proto_encode_hello(uint8_t *buf, const Msg_Hello *r) {
int off = FRAME_HEADER_SIZE;
put_u8(buf, &off, r->version);
return write_frame(buf, RT_HELLO, 1);
}
int proto_encode_define_pattern(uint8_t *buf, const Msg_Define_Pattern *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->pattern_id);
put_u8(buf, &off, r->steps);
put_u8(buf, &off, r->channel);
return write_frame(buf, RT_DEFINE_PATTERN, 4);
}
int proto_encode_clear_pattern(uint8_t *buf, const Msg_Clear_Pattern *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->pattern_id);
return write_frame(buf, RT_CLEAR_PATTERN, 2);
}
int proto_encode_add_note(uint8_t *buf, const Msg_Add_Note *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->pattern_id);
put_u8(buf, &off, r->step);
put_u8(buf, &off, r->note);
put_u8(buf, &off, r->velocity);
put_u8(buf, &off, r->duration_steps);
return write_frame(buf, RT_ADD_NOTE, 6);
}
int proto_encode_add_sub_pattern(uint8_t *buf, const Msg_Add_Sub_Pattern *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->pattern_id);
put_u8(buf, &off, r->step);
put_u16(buf, &off, r->sub_pattern_id);
return write_frame(buf, RT_ADD_SUB_PATTERN, 5);
}
int proto_encode_play(uint8_t *buf, const Msg_Play *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->pattern_id);
return write_frame(buf, RT_PLAY, 2);
}
int proto_encode_stop(uint8_t *buf) {
return write_frame(buf, RT_STOP, 0);
}
int proto_encode_set_tempo(uint8_t *buf, const Msg_Set_Tempo *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->bpm_x10);
return write_frame(buf, RT_SET_TEMPO, 2);
}
int proto_encode_ack(uint8_t *buf, const Msg_Ack *r) {
int off = FRAME_HEADER_SIZE;
put_u8(buf, &off, r->acked_type);
return write_frame(buf, RT_ACK, 1);
}
int proto_encode_error(uint8_t *buf, const Msg_Error *r) {
int off = FRAME_HEADER_SIZE;
put_u8(buf, &off, r->code);
put_u8(buf, &off, r->context_type);
return write_frame(buf, RT_ERROR, 2);
}
int proto_encode_beat_tick(uint8_t *buf, const Msg_Beat_Tick *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->pattern_id);
put_u8(buf, &off, r->step);
put_u8(buf, &off, r->beat);
return write_frame(buf, RT_BEAT_TICK, 4);
}
int proto_encode_pattern_end(uint8_t *buf, const Msg_Pattern_End *r) {
int off = FRAME_HEADER_SIZE;
put_u16(buf, &off, r->pattern_id);
return write_frame(buf, RT_PATTERN_END, 2);
}
/* ── decode ──────────────────────────────────────────────────── */
int proto_decode_hello(const uint8_t *p, uint16_t len, Msg_Hello *r) {
if (len < 1) return -1;
int off = 0;
r->version = get_u8(p, &off);
(void)off;
return 0;
}
int proto_decode_define_pattern(const uint8_t *p, uint16_t len, Msg_Define_Pattern *r) {
if (len < 4) return -1;
int off = 0;
r->pattern_id = get_u16(p, &off);
r->steps = get_u8(p, &off);
r->channel = get_u8(p, &off);
(void)off;
return 0;
}
int proto_decode_clear_pattern(const uint8_t *p, uint16_t len, Msg_Clear_Pattern *r) {
if (len < 2) return -1;
int off = 0;
r->pattern_id = get_u16(p, &off);
(void)off;
return 0;
}
int proto_decode_add_note(const uint8_t *p, uint16_t len, Msg_Add_Note *r) {
if (len < 6) return -1;
int off = 0;
r->pattern_id = get_u16(p, &off);
r->step = get_u8(p, &off);
r->note = get_u8(p, &off);
r->velocity = get_u8(p, &off);
r->duration_steps = get_u8(p, &off);
(void)off;
return 0;
}
int proto_decode_add_sub_pattern(const uint8_t *p, uint16_t len, Msg_Add_Sub_Pattern *r) {
if (len < 5) return -1;
int off = 0;
r->pattern_id = get_u16(p, &off);
r->step = get_u8(p, &off);
r->sub_pattern_id = get_u16(p, &off);
(void)off;
return 0;
}
int proto_decode_play(const uint8_t *p, uint16_t len, Msg_Play *r) {
if (len < 2) return -1;
int off = 0;
r->pattern_id = get_u16(p, &off);
(void)off;
return 0;
}
int proto_decode_stop(const uint8_t *p, uint16_t len, Msg_Stop *r) {
(void)p; (void)len; (void)r;
return 0;
}
int proto_decode_set_tempo(const uint8_t *p, uint16_t len, Msg_Set_Tempo *r) {
if (len < 2) return -1;
int off = 0;
r->bpm_x10 = get_u16(p, &off);
(void)off;
return 0;
}
int proto_decode_ack(const uint8_t *p, uint16_t len, Msg_Ack *r) {
if (len < 1) return -1;
int off = 0;
r->acked_type = get_u8(p, &off);
(void)off;
return 0;
}
int proto_decode_error(const uint8_t *p, uint16_t len, Msg_Error *r) {
if (len < 2) return -1;
int off = 0;
r->code = get_u8(p, &off);
r->context_type = get_u8(p, &off);
(void)off;
return 0;
}
int proto_decode_beat_tick(const uint8_t *p, uint16_t len, Msg_Beat_Tick *r) {
if (len < 4) return -1;
int off = 0;
r->pattern_id = get_u16(p, &off);
r->step = get_u8(p, &off);
r->beat = get_u8(p, &off);
(void)off;
return 0;
}
int proto_decode_pattern_end(const uint8_t *p, uint16_t len, Msg_Pattern_End *r) {
if (len < 2) return -1;
int off = 0;
r->pattern_id = get_u16(p, &off);
(void)off;
return 0;
}

View File

@@ -0,0 +1,116 @@
/* AUTO-GENERATED by codegen/gen.mjs — DO NOT EDIT */
/* Source: protocol.yaml version 1 */
#pragma once
#include <stdint.h>
#include <string.h>
#define PROTOCOL_VERSION 1
#define FRAME_HEADER_SIZE 3
#define DIRECTION_C_TO_NODE(t) ((t) >= 0x80)
/* Record types */
typedef enum {
RT_HELLO = 0x01,
RT_DEFINE_PATTERN = 0x02,
RT_CLEAR_PATTERN = 0x03,
RT_ADD_NOTE = 0x04,
RT_ADD_SUB_PATTERN = 0x05,
RT_PLAY = 0x06,
RT_STOP = 0x07,
RT_SET_TEMPO = 0x08,
RT_ACK = 0x81,
RT_ERROR = 0x82,
RT_BEAT_TICK = 0x83,
RT_PATTERN_END = 0x84,
} Record_Type;
/* Payload sizes (bytes, not including 3-byte frame header) */
#define PAYLOAD_SIZE_HELLO 1
#define PAYLOAD_SIZE_DEFINE_PATTERN 4
#define PAYLOAD_SIZE_CLEAR_PATTERN 2
#define PAYLOAD_SIZE_ADD_NOTE 6
#define PAYLOAD_SIZE_ADD_SUB_PATTERN 5
#define PAYLOAD_SIZE_PLAY 2
#define PAYLOAD_SIZE_STOP 0
#define PAYLOAD_SIZE_SET_TEMPO 2
#define PAYLOAD_SIZE_ACK 1
#define PAYLOAD_SIZE_ERROR 2
#define PAYLOAD_SIZE_BEAT_TICK 4
#define PAYLOAD_SIZE_PATTERN_END 2
/* Record payload structs */
typedef struct {
uint8_t version;
} Msg_Hello;
typedef struct {
uint16_t pattern_id;
uint8_t steps; /* Total step count e.g. 16 for a one-bar pattern at 16th-note resolution */
uint8_t channel; /* MIDI channel 0-15 */
} Msg_Define_Pattern;
typedef struct {
uint16_t pattern_id;
} Msg_Clear_Pattern;
typedef struct {
uint16_t pattern_id;
uint8_t step; /* 0-based step index within the pattern */
uint8_t note; /* MIDI note number 0-127 (middle C = 60) */
uint8_t velocity; /* 0-127 */
uint8_t duration_steps; /* Duration in steps (1 = one step) */
} Msg_Add_Note;
typedef struct {
uint16_t pattern_id; /* Parent pattern ID */
uint8_t step; /* Step within parent at which the sub-pattern begins playing */
uint16_t sub_pattern_id;
} Msg_Add_Sub_Pattern;
typedef struct {
uint16_t pattern_id;
} Msg_Play;
typedef struct {
char _empty;
} Msg_Stop;
typedef struct {
uint16_t bpm_x10; /* BPM × 10 for 0.1 BPM resolution — e.g. 1200 = 120.0 BPM */
} Msg_Set_Tempo;
typedef struct {
uint8_t acked_type; /* Record type being acknowledged */
} Msg_Ack;
typedef struct {
uint8_t code;
uint8_t context_type; /* Record type that triggered this error */
} Msg_Error;
typedef struct {
uint16_t pattern_id;
uint8_t step; /* Current step within the pattern (0-based) */
uint8_t beat; /* Current beat number within the current cycle (wraps at 255) */
} Msg_Beat_Tick;
typedef struct {
uint16_t pattern_id;
} Msg_Pattern_End;
/* Encode: write complete frame to buf, return total bytes written */
int proto_encode_hello(uint8_t *buf, const Msg_Hello *r);
int proto_encode_define_pattern(uint8_t *buf, const Msg_Define_Pattern *r);
int proto_encode_clear_pattern(uint8_t *buf, const Msg_Clear_Pattern *r);
int proto_encode_add_note(uint8_t *buf, const Msg_Add_Note *r);
int proto_encode_add_sub_pattern(uint8_t *buf, const Msg_Add_Sub_Pattern *r);
int proto_encode_play(uint8_t *buf, const Msg_Play *r);
int proto_encode_stop(uint8_t *buf);
int proto_encode_set_tempo(uint8_t *buf, const Msg_Set_Tempo *r);
int proto_encode_ack(uint8_t *buf, const Msg_Ack *r);
int proto_encode_error(uint8_t *buf, const Msg_Error *r);
int proto_encode_beat_tick(uint8_t *buf, const Msg_Beat_Tick *r);
int proto_encode_pattern_end(uint8_t *buf, const Msg_Pattern_End *r);
/* Decode: parse payload into struct, return 0 on success, -1 on error */
int proto_decode_hello(const uint8_t *payload, uint16_t len, Msg_Hello *r);
int proto_decode_define_pattern(const uint8_t *payload, uint16_t len, Msg_Define_Pattern *r);
int proto_decode_clear_pattern(const uint8_t *payload, uint16_t len, Msg_Clear_Pattern *r);
int proto_decode_add_note(const uint8_t *payload, uint16_t len, Msg_Add_Note *r);
int proto_decode_add_sub_pattern(const uint8_t *payload, uint16_t len, Msg_Add_Sub_Pattern *r);
int proto_decode_play(const uint8_t *payload, uint16_t len, Msg_Play *r);
int proto_decode_stop(const uint8_t *payload, uint16_t len, Msg_Stop *r);
int proto_decode_set_tempo(const uint8_t *payload, uint16_t len, Msg_Set_Tempo *r);
int proto_decode_ack(const uint8_t *payload, uint16_t len, Msg_Ack *r);
int proto_decode_error(const uint8_t *payload, uint16_t len, Msg_Error *r);
int proto_decode_beat_tick(const uint8_t *payload, uint16_t len, Msg_Beat_Tick *r);
int proto_decode_pattern_end(const uint8_t *payload, uint16_t len, Msg_Pattern_End *r);

176
c-backend/src/main.c Normal file
View File

@@ -0,0 +1,176 @@
#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_play(&app->seq, msg.pattern_id);
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;
}
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;
}

View File

@@ -0,0 +1,102 @@
#include "pattern_store.h"
#include <string.h>
#include <stdio.h>
void pattern_store_init(Pattern_Store *ps) {
memset(ps, 0, sizeof(*ps));
pthread_mutex_init(&ps->mutex, NULL);
}
void pattern_store_destroy(Pattern_Store *ps) {
pthread_mutex_destroy(&ps->mutex);
}
void pattern_store_lock(Pattern_Store *ps) {
pthread_mutex_lock(&ps->mutex);
}
void pattern_store_unlock(Pattern_Store *ps) {
pthread_mutex_unlock(&ps->mutex);
}
/* Linear search — fine for PATTERN_STORE_SIZE = 256 */
Pattern *pattern_store_get(Pattern_Store *ps, uint16_t id) {
for (int i = 0; i < ps->count; i++) {
if (ps->entries[i].id == id && ps->entries[i].defined) {
return &ps->entries[i];
}
}
return NULL;
}
static Pattern *get_or_create(Pattern_Store *ps, uint16_t id) {
Pattern *p = pattern_store_get(ps, id);
if (p) return p;
if (ps->count >= PATTERN_STORE_SIZE) {
fprintf(stderr, "pattern_store: capacity reached\n");
return NULL;
}
p = &ps->entries[ps->count++];
memset(p, 0, sizeof(*p));
p->id = id;
p->defined = 1;
return p;
}
void pattern_store_define(Pattern_Store *ps, uint16_t id, uint8_t steps, uint8_t channel) {
pattern_store_lock(ps);
Pattern *p = get_or_create(ps, id);
if (p) {
p->steps = steps;
p->channel = channel;
p->defined = 1;
}
pattern_store_unlock(ps);
}
void pattern_store_clear(Pattern_Store *ps, uint16_t id) {
pattern_store_lock(ps);
Pattern *p = pattern_store_get(ps, id);
if (p) {
p->note_count = 0;
p->sub_ref_count = 0;
}
pattern_store_unlock(ps);
}
int pattern_store_add_note(Pattern_Store *ps, uint16_t id, uint8_t step,
uint8_t note, uint8_t velocity, uint8_t duration_steps) {
int result = 0;
pattern_store_lock(ps);
Pattern *p = pattern_store_get(ps, id);
if (!p) {
result = -1;
} else if (p->note_count >= MAX_NOTES_PER_PAT) {
result = -2;
} else {
Note_Event *ev = &p->notes[p->note_count++];
ev->step = step;
ev->note = note;
ev->velocity = velocity;
ev->duration_steps = duration_steps;
}
pattern_store_unlock(ps);
return result;
}
int pattern_store_add_sub_ref(Pattern_Store *ps, uint16_t id, uint8_t step, uint16_t sub_id) {
int result = 0;
pattern_store_lock(ps);
Pattern *p = pattern_store_get(ps, id);
if (!p) {
result = -1;
} else if (p->sub_ref_count >= MAX_SUB_REFS_PER_PAT) {
result = -2;
} else {
Sub_Ref *ref = &p->sub_refs[p->sub_ref_count++];
ref->step = step;
ref->sub_pattern_id = sub_id;
}
pattern_store_unlock(ps);
return result;
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <stdint.h>
#include <pthread.h>
#define PATTERN_STORE_SIZE 256
#define MAX_NOTES_PER_PAT 64
#define MAX_SUB_REFS_PER_PAT 16
typedef struct {
uint8_t step;
uint8_t note;
uint8_t velocity;
uint8_t duration_steps;
} Note_Event;
typedef struct {
uint8_t step;
uint16_t sub_pattern_id;
} Sub_Ref;
typedef struct {
uint16_t id;
uint8_t steps;
uint8_t channel;
int defined;
Note_Event notes[MAX_NOTES_PER_PAT];
int note_count;
Sub_Ref sub_refs[MAX_SUB_REFS_PER_PAT];
int sub_ref_count;
} Pattern;
typedef struct {
Pattern entries[PATTERN_STORE_SIZE];
int count;
pthread_mutex_t mutex;
} Pattern_Store;
/* Lifecycle */
void pattern_store_init(Pattern_Store *ps);
void pattern_store_destroy(Pattern_Store *ps);
/* Thread-safe operations */
void pattern_store_define(Pattern_Store *ps, uint16_t id, uint8_t steps, uint8_t channel);
void pattern_store_clear(Pattern_Store *ps, uint16_t id);
int pattern_store_add_note(Pattern_Store *ps, uint16_t id, uint8_t step,
uint8_t note, uint8_t velocity, uint8_t duration_steps);
int pattern_store_add_sub_ref(Pattern_Store *ps, uint16_t id, uint8_t step, uint16_t sub_id);
/* NOT thread-safe: caller must hold the lock */
Pattern *pattern_store_get(Pattern_Store *ps, uint16_t id);
void pattern_store_lock(Pattern_Store *ps);
void pattern_store_unlock(Pattern_Store *ps);

301
c-backend/src/sequencer.c Normal file
View File

@@ -0,0 +1,301 @@
#include "sequencer.h"
#include "../generated/protocol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
/* ── ALSA helpers ─────────────────────────────────────────────────── */
static void alsa_note_on(Sequencer *seq, uint8_t ch, uint8_t note, uint8_t vel) {
snd_seq_event_t ev;
snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, seq->port_id);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_set_direct(&ev);
snd_seq_ev_set_noteon(&ev, ch, note, vel);
snd_seq_event_output_direct(seq->seq, &ev);
}
static void alsa_note_off(Sequencer *seq, uint8_t ch, uint8_t note) {
snd_seq_event_t ev;
snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, seq->port_id);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_set_direct(&ev);
snd_seq_ev_set_noteoff(&ev, ch, note, 0);
snd_seq_event_output_direct(seq->seq, &ev);
}
static void alsa_all_notes_off(Sequencer *seq) {
for (int ch = 0; ch < 16; ch++) {
for (int n = 0; n < 128; n++) {
alsa_note_off(seq, (uint8_t)ch, (uint8_t)n);
}
}
}
/* ── Note-off queue ───────────────────────────────────────────────── */
static void enqueue_note_off(Sequencer *seq, uint8_t ch, uint8_t note, uint64_t delay_ns) {
if (seq->note_off_count >= NOTE_OFF_QUEUE_SIZE) {
fprintf(stderr, "sequencer: note-off queue full\n");
return;
}
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
uint64_t fire_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec + delay_ns;
Pending_Note_Off *pno = &seq->note_offs[seq->note_off_count++];
pno->channel = ch;
pno->note = note;
pno->fire_at.tv_sec = (time_t)(fire_ns / 1000000000ULL);
pno->fire_at.tv_nsec = (long)(fire_ns % 1000000000ULL);
}
/* Fire any due note-offs; compact the queue. */
static void drain_note_offs(Sequencer *seq) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
uint64_t now_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec;
int write_idx = 0;
for (int i = 0; i < seq->note_off_count; i++) {
Pending_Note_Off *pno = &seq->note_offs[i];
uint64_t fire_ns = (uint64_t)pno->fire_at.tv_sec * 1000000000ULL
+ (uint64_t)pno->fire_at.tv_nsec;
if (fire_ns <= now_ns) {
alsa_note_off(seq, pno->channel, pno->note);
} else {
seq->note_offs[write_idx++] = *pno;
}
}
seq->note_off_count = write_idx;
}
/* ── Tempo helpers ────────────────────────────────────────────────── */
/* Returns step duration in nanoseconds.
* One step = one 16th note.
* step_ns = 60,000,000,000 / (bpm_x10/10) / 4
* = 150,000,000,000 / bpm_x10
* At 120.0 BPM (bpm_x10=1200): 125,000,000 ns = 125 ms */
static uint64_t get_step_ns(Sequencer *seq) {
pthread_mutex_lock(&seq->tempo_mutex);
uint16_t bpm_x10 = seq->bpm_x10;
pthread_mutex_unlock(&seq->tempo_mutex);
return 150000000000ULL / (uint64_t)bpm_x10;
}
/* ── Step processing ──────────────────────────────────────────────── */
/* Process notes and sub-refs at a given step within a pattern.
* is_sub: non-zero means this is a sub-pattern call (don't recurse sub-refs). */
static void process_step(Sequencer *seq, Pattern *pat, uint8_t step,
uint64_t step_ns, int is_sub) {
/* Fire notes */
for (int i = 0; i < pat->note_count; i++) {
Note_Event *ev = &pat->notes[i];
if (ev->step != step) continue;
alsa_note_on(seq, pat->channel, ev->note, ev->velocity);
enqueue_note_off(seq, pat->channel, ev->note,
step_ns * (uint64_t)ev->duration_steps);
}
/* Start sub-patterns (root level only; subs don't nest further in this pass) */
if (!is_sub) {
for (int i = 0; i < pat->sub_ref_count; i++) {
Sub_Ref *ref = &pat->sub_refs[i];
if (ref->step != step) continue;
/* Find an empty slot */
for (int j = 0; j < MAX_PENDING_SUBS; j++) {
if (!seq->pending_subs[j].active) {
seq->pending_subs[j].pattern_id = ref->sub_pattern_id;
seq->pending_subs[j].local_step = 0;
seq->pending_subs[j].active = 1;
if (j >= seq->pending_sub_count) {
seq->pending_sub_count = j + 1;
}
break;
}
}
}
}
}
/* ── Tick thread ──────────────────────────────────────────────────── */
static void *tick_fn(void *arg) {
Sequencer *seq = (Sequencer *)arg;
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
while (atomic_load(&seq->running)) {
uint64_t sn = get_step_ns(seq);
/* Fire due note-offs */
drain_note_offs(seq);
/* Lock pattern store for the whole tick */
pattern_store_lock(seq->ps);
Pattern *root = pattern_store_get(seq->ps, seq->root_pattern_id);
if (!root) {
pattern_store_unlock(seq->ps);
fprintf(stderr, "sequencer: root pattern %u not found, stopping\n",
seq->root_pattern_id);
atomic_store(&seq->running, 0);
break;
}
uint8_t step = seq->current_step;
/* Process root */
process_step(seq, root, step, sn, 0);
/* Process active sub-patterns */
for (int i = 0; i < seq->pending_sub_count; i++) {
Pending_Sub *ps = &seq->pending_subs[i];
if (!ps->active) continue;
Pattern *subpat = pattern_store_get(seq->ps, ps->pattern_id);
if (!subpat) {
ps->active = 0;
continue;
}
process_step(seq, subpat, ps->local_step, sn, 1);
ps->local_step++;
if (ps->local_step >= subpat->steps) {
ps->active = 0;
/* Notify sub-pattern end */
uint8_t buf[16];
Msg_Pattern_End msg = { .pattern_id = ps->pattern_id };
int len = proto_encode_pattern_end(buf, &msg);
pattern_store_unlock(seq->ps);
seq->send_fn(seq->send_ctx, buf, len);
pattern_store_lock(seq->ps);
/* Re-fetch root after unlock */
root = pattern_store_get(seq->ps, seq->root_pattern_id);
if (!root) {
pattern_store_unlock(seq->ps);
atomic_store(&seq->running, 0);
return NULL;
}
}
}
/* Send BEAT_TICK */
{
uint8_t buf[16];
Msg_Beat_Tick tick = {
.pattern_id = seq->root_pattern_id,
.step = step,
.beat = seq->current_beat,
};
int len = proto_encode_beat_tick(buf, &tick);
pattern_store_unlock(seq->ps);
seq->send_fn(seq->send_ctx, buf, len);
pattern_store_lock(seq->ps);
root = pattern_store_get(seq->ps, seq->root_pattern_id);
if (!root) {
pattern_store_unlock(seq->ps);
atomic_store(&seq->running, 0);
return NULL;
}
}
/* Advance step */
seq->current_step++;
if (seq->current_step >= root->steps) {
seq->current_step = 0;
seq->current_beat = (uint8_t)(seq->current_beat + 1);
uint8_t buf[16];
Msg_Pattern_End msg = { .pattern_id = seq->root_pattern_id };
int len = proto_encode_pattern_end(buf, &msg);
pattern_store_unlock(seq->ps);
seq->send_fn(seq->send_ctx, buf, len);
} else {
pattern_store_unlock(seq->ps);
}
/* Advance absolute deadline — drift-free */
uint64_t next_abs = (uint64_t)next.tv_sec * 1000000000ULL
+ (uint64_t)next.tv_nsec + sn;
next.tv_sec = (time_t)(next_abs / 1000000000ULL);
next.tv_nsec = (long)(next_abs % 1000000000ULL);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
}
return NULL;
}
/* ── Public API ───────────────────────────────────────────────────── */
int sequencer_init(Sequencer *seq, Pattern_Store *ps, const char *alsa_name,
Send_Fn send_fn, void *send_ctx) {
memset(seq, 0, sizeof(*seq));
seq->ps = ps;
seq->send_fn = send_fn;
seq->send_ctx = send_ctx;
seq->bpm_x10 = 1200; /* 120.0 BPM */
atomic_store(&seq->running, 0);
pthread_mutex_init(&seq->tempo_mutex, NULL);
if (snd_seq_open(&seq->seq, "default", SND_SEQ_OPEN_OUTPUT, 0) < 0) {
fprintf(stderr, "sequencer: failed to open ALSA sequencer\n");
return -1;
}
snd_seq_set_client_name(seq->seq, alsa_name ? alsa_name : "midi-sequencer");
seq->client_id = snd_seq_client_id(seq->seq);
seq->port_id = snd_seq_create_simple_port(seq->seq, "output",
SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ,
SND_SEQ_PORT_TYPE_APPLICATION | SND_SEQ_PORT_TYPE_MIDI_GENERIC);
if (seq->port_id < 0) {
fprintf(stderr, "sequencer: failed to create ALSA port\n");
snd_seq_close(seq->seq);
return -1;
}
fprintf(stderr, "sequencer: ALSA client %d port %d\n", seq->client_id, seq->port_id);
return 0;
}
void sequencer_destroy(Sequencer *seq) {
sequencer_stop(seq);
snd_seq_close(seq->seq);
pthread_mutex_destroy(&seq->tempo_mutex);
}
void sequencer_play(Sequencer *seq, uint16_t pattern_id) {
sequencer_stop(seq);
seq->root_pattern_id = pattern_id;
seq->current_step = 0;
seq->current_beat = 0;
seq->note_off_count = 0;
seq->pending_sub_count = 0;
memset(seq->pending_subs, 0, sizeof(seq->pending_subs));
atomic_store(&seq->running, 1);
pthread_create(&seq->tick_thread, NULL, tick_fn, seq);
}
void sequencer_stop(Sequencer *seq) {
if (!atomic_load(&seq->running)) return;
atomic_store(&seq->running, 0);
pthread_join(seq->tick_thread, NULL);
alsa_all_notes_off(seq);
drain_note_offs(seq);
}
void sequencer_set_tempo(Sequencer *seq, uint16_t bpm_x10) {
pthread_mutex_lock(&seq->tempo_mutex);
seq->bpm_x10 = bpm_x10 > 0 ? bpm_x10 : 1;
pthread_mutex_unlock(&seq->tempo_mutex);
}

67
c-backend/src/sequencer.h Normal file
View File

@@ -0,0 +1,67 @@
#pragma once
#include <stdint.h>
#include <pthread.h>
#include <stdatomic.h>
#include <time.h>
#include <alsa/asoundlib.h>
#include "pattern_store.h"
#define NOTE_OFF_QUEUE_SIZE 256
#define MAX_PENDING_SUBS 64
typedef struct {
struct timespec fire_at;
uint8_t channel;
uint8_t note;
} Pending_Note_Off;
typedef struct {
uint16_t pattern_id;
uint8_t local_step;
int active;
} Pending_Sub;
/* Function pointer used to write frames back to the Node client */
typedef void (*Send_Fn)(void *ctx, const uint8_t *buf, int len);
typedef struct {
/* ALSA */
snd_seq_t *seq;
int port_id;
int client_id;
/* Pattern store (shared with socket thread) */
Pattern_Store *ps;
/* Callback for sending frames to Node */
Send_Fn send_fn;
void *send_ctx;
/* Playback state — owned exclusively by tick thread when running */
uint16_t root_pattern_id;
uint8_t current_step;
uint8_t current_beat;
/* Note-off queue — tick thread only, no mutex needed */
Pending_Note_Off note_offs[NOTE_OFF_QUEUE_SIZE];
int note_off_count;
/* Pending sub-patterns — tick thread only */
Pending_Sub pending_subs[MAX_PENDING_SUBS];
int pending_sub_count;
/* Tempo — protected by tempo_mutex */
uint16_t bpm_x10;
pthread_mutex_t tempo_mutex;
/* Thread control */
atomic_int running;
pthread_t tick_thread;
} Sequencer;
int sequencer_init(Sequencer *seq, Pattern_Store *ps, const char *alsa_name,
Send_Fn send_fn, void *send_ctx);
void sequencer_destroy(Sequencer *seq);
void sequencer_play(Sequencer *seq, uint16_t pattern_id);
void sequencer_stop(Sequencer *seq);
void sequencer_set_tempo(Sequencer *seq, uint16_t bpm_x10);

View File

@@ -0,0 +1,153 @@
#include "socket_server.h"
#include "../generated/protocol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
/* Read exactly len bytes, return 0 on success, -1 on EOF/error */
static int read_exact(int fd, uint8_t *buf, size_t len) {
size_t done = 0;
while (done < len) {
ssize_t n = read(fd, buf + done, len - done);
if (n <= 0) return -1;
done += (size_t)n;
}
return 0;
}
/* Read loop for the connected client */
static void *read_thread_fn(void *arg) {
Socket_Server *srv = (Socket_Server *)arg;
uint8_t hdr[FRAME_HEADER_SIZE];
uint8_t payload[65535];
while (atomic_load(&srv->running)) {
int fd = atomic_load(&srv->client_fd);
if (fd < 0) {
/* No client yet, busy-wait briefly */
struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 };
nanosleep(&ts, NULL);
continue;
}
if (read_exact(fd, hdr, FRAME_HEADER_SIZE) < 0) goto disconnected;
uint8_t record_type = hdr[0];
uint16_t payload_len = (uint16_t)(hdr[1] | ((uint16_t)hdr[2] << 8));
if (payload_len > 0) {
if (read_exact(fd, payload, payload_len) < 0) goto disconnected;
}
if (srv->on_frame) {
srv->on_frame(record_type, payload, payload_len, srv->ctx);
}
continue;
disconnected:
close(fd);
atomic_store(&srv->client_fd, -1);
if (srv->on_disconnect) {
srv->on_disconnect(srv->ctx);
}
fprintf(stderr, "socket_server: client disconnected\n");
}
return NULL;
}
/* Accept loop */
static void *accept_thread_fn(void *arg) {
Socket_Server *srv = (Socket_Server *)arg;
while (atomic_load(&srv->running)) {
int fd = accept(srv->sock_fd, NULL, NULL);
if (fd < 0) {
if (errno == EINTR || errno == EBADF) break;
perror("socket_server: accept");
continue;
}
/* Close any existing client */
int old_fd = atomic_exchange(&srv->client_fd, fd);
if (old_fd >= 0) {
close(old_fd);
if (srv->on_disconnect) srv->on_disconnect(srv->ctx);
}
fprintf(stderr, "socket_server: client connected\n");
if (srv->on_connect) {
srv->on_connect(fd, srv->ctx);
}
}
return NULL;
}
int socket_server_start(Socket_Server *srv, const char *path,
Frame_Handler on_frame,
Connect_Handler on_connect,
Disconnect_Handler on_disconnect,
void *ctx) {
memset(srv, 0, sizeof(*srv));
strncpy(srv->sock_path, path, sizeof(srv->sock_path) - 1);
srv->on_frame = on_frame;
srv->on_connect = on_connect;
srv->on_disconnect = on_disconnect;
srv->ctx = ctx;
atomic_store(&srv->client_fd, -1);
atomic_store(&srv->running, 1);
pthread_mutex_init(&srv->write_mutex, NULL);
unlink(path); /* Remove stale socket */
srv->sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (srv->sock_fd < 0) { perror("socket"); return -1; }
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
if (bind(srv->sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); close(srv->sock_fd); return -1;
}
if (listen(srv->sock_fd, 1) < 0) {
perror("listen"); close(srv->sock_fd); return -1;
}
pthread_create(&srv->read_thread, NULL, read_thread_fn, srv);
pthread_create(&srv->accept_thread, NULL, accept_thread_fn, srv);
return 0;
}
void socket_server_stop(Socket_Server *srv) {
atomic_store(&srv->running, 0);
close(srv->sock_fd);
int fd = atomic_exchange(&srv->client_fd, -1);
if (fd >= 0) close(fd);
pthread_join(srv->accept_thread, NULL);
pthread_join(srv->read_thread, NULL);
pthread_mutex_destroy(&srv->write_mutex);
unlink(srv->sock_path);
}
void socket_server_send(Socket_Server *srv, const uint8_t *buf, int len) {
int fd = atomic_load(&srv->client_fd);
if (fd < 0) return;
pthread_mutex_lock(&srv->write_mutex);
ssize_t done = 0;
while (done < len) {
ssize_t n = write(fd, buf + done, (size_t)(len - done));
if (n <= 0) {
atomic_store(&srv->client_fd, -1);
break;
}
done += n;
}
pthread_mutex_unlock(&srv->write_mutex);
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
#include <stdatomic.h>
#include <pthread.h>
typedef void (*Frame_Handler)(uint8_t record_type, const uint8_t *payload,
uint16_t payload_len, void *ctx);
typedef void (*Connect_Handler)(int client_fd, void *ctx);
typedef void (*Disconnect_Handler)(void *ctx);
typedef struct {
int sock_fd;
atomic_int client_fd;
pthread_t accept_thread;
pthread_t read_thread;
pthread_mutex_t write_mutex;
Frame_Handler on_frame;
Connect_Handler on_connect;
Disconnect_Handler on_disconnect;
void *ctx;
atomic_int running;
char sock_path[256];
} Socket_Server;
int socket_server_start(Socket_Server *srv, const char *path,
Frame_Handler on_frame,
Connect_Handler on_connect,
Disconnect_Handler on_disconnect,
void *ctx);
void socket_server_stop(Socket_Server *srv);
/* Thread-safe write to current client; no-op if no client connected */
void socket_server_send(Socket_Server *srv, const uint8_t *buf, int len);