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:
20
c-backend/Makefile
Normal file
20
c-backend/Makefile
Normal 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)
|
||||
215
c-backend/generated/protocol.c
Normal file
215
c-backend/generated/protocol.c
Normal 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;
|
||||
}
|
||||
116
c-backend/generated/protocol.h
Normal file
116
c-backend/generated/protocol.h
Normal 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
176
c-backend/src/main.c
Normal 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;
|
||||
}
|
||||
102
c-backend/src/pattern_store.c
Normal file
102
c-backend/src/pattern_store.c
Normal 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;
|
||||
}
|
||||
52
c-backend/src/pattern_store.h
Normal file
52
c-backend/src/pattern_store.h
Normal 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
301
c-backend/src/sequencer.c
Normal 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
67
c-backend/src/sequencer.h
Normal 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);
|
||||
153
c-backend/src/socket_server.c
Normal file
153
c-backend/src/socket_server.c
Normal 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);
|
||||
}
|
||||
35
c-backend/src/socket_server.h
Normal file
35
c-backend/src/socket_server.h
Normal 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);
|
||||
Reference in New Issue
Block a user