feat(midi): add device routing and bounded external note output (phase 101)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:23:37 +01:00
commit 75dee16b6a
9 changed files with 251 additions and 15 deletions

View file

@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/project/crossfade.c src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
CORE_SOURCES := src/midi/output.c src/project/crossfade.c src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)

View file

@ -13,3 +13,22 @@ All persistent preference fields are listed by the shared C schema and available
in Preferences. Existing project tempo/controller maps remain project data;
Preferences project defaults apply to New. No configuration I/O runs in an audio
callback. Tests and screenshot captures use isolated configuration paths.
## MIDI backend
xtc exposes `midi_in` and `midi_out` JACK ports. The output sends scheduled MIDI
clip notes from unmuted, solo-eligible tracks, retaining source channels and
velocities, with clipped note boundaries and notes chased after seeks/loops.
Tracks share one output; matching pitches/channels on different tracks can
conflict on receiving devices. Audio faders, effects and bus processing do not
alter external MIDI. MIDI CC automation, program changes, clock and SysEx output
are outside this note-output workflow.
Input can filter a single channel, and output can preserve or force a channel.
Optional thru forwards supported live notes, controllers and pitch bend while
transport is active. Thru defaults off to avoid accidental feedback. Callback
work and event buffers are bounded; overflow requests all-notes-off and increments
the MIDI-drop counter. Panic and stop send CC123 on all channels. Immediate stop
waits up to 100 ms for callback acknowledgement before deactivation; delivery
cannot be guaranteed after server/device loss or to devices ignoring CC123.
No device is connected unless a user chooses it or an external patchbay routes it.

View file

@ -2241,12 +2241,12 @@ Implemented: ~/.xtc.conf is loaded before project/backend creation and saved ato
Expose JACK MIDI source/destination discovery, routing, input filtering and external MIDI output with bounded callback work and panic safety.
### Tasks and acceptance criteria
- [ ] Implement and document the workflow with bounded validation and safe ownership.
- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo.
- [ ] Commit this phase separately after validation.
- [x] Implement and document the workflow with bounded validation and safe ownership.
- [x] Pass meaningful core and GUI checks, including errors and persistence or undo.
- [x] Commit this phase separately after validation.
### Implementation status
Planned.
Implemented: public JACK MIDI discovery/naming/routing/options APIs, a prepared bounded note-output schedule, optional filtered live thru and output channel override. Seek/loop discontinuities chase notes; panic/stop/overflow send all-notes-off. Core and allocation-failure suites and real JACK input/thru/receiver checks passed; deterministic backend checks cover event offsets, filtering, routing, panic and seek/note-off. See MIDI_WORKFLOWS.md for shared-output, transport-active and device-loss limits.
---

View file

@ -21,6 +21,7 @@ typedef struct {
} xtc_meter_snapshot;
typedef struct xtc_backend xtc_backend;
typedef int (*xtc_backend_port_visitor)(const char *name, void *context);
typedef enum {
XTC_BACKEND_JACK = 1
@ -157,6 +158,16 @@ xtc_result xtc_backend_panic(xtc_backend *backend);
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out_name);
/* MIDI counterparts. direction is local for naming/routing, remote for visiting.
* Routing requires an active backend. No server starts or implicit device routes.
* Port names are JACK/PipeWire names; physical ALSA devices need host exposure. */
xtc_result xtc_backend_midi_name(const xtc_backend *,xtc_port_direction,const char **);
xtc_result xtc_backend_visit_midi_ports(xtc_backend *,xtc_port_direction,xtc_backend_port_visitor,void *);
xtc_result xtc_backend_route_midi(xtc_backend *,xtc_port_direction,const char *,int disconnect);
/* Stopped only. Input channel/output channel -1 preserves all/original channels,
* otherwise 0..15. thru=1 forwards supported live messages to MIDI output. */
xtc_result xtc_backend_set_midi_options(xtc_backend *,int input_channel,int output_channel,int thru);
/**
* @brief Attaches or detaches a borrowed recorder while the backend is stopped.
*
@ -210,7 +221,7 @@ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction
* @param context Opaque pointer supplied to xtc_backend_visit_ports().
* @return Nonzero to stop enumeration, zero to continue.
*/
typedef int (*xtc_backend_port_visitor)(const char *name, void *context);
/**
* @brief Visits available audio ports matching the requested direction until the visitor stops.
*

View file

@ -1,7 +1,10 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_backend.h"
#include "xtc/xtc_realtime.h"
#include "xtc/xtc_dsp.h"
#include "../mixer/mix_internal.h"
#include "../midi/output.h"
#include <time.h>
#include <stdlib.h>
#include <string.h>
@ -13,10 +16,13 @@
struct xtc_backend {
jack_client_t *client;
jack_port_t *input, *output, *midi_input, *output_right;
jack_port_t *input, *output, *midi_input, *output_right, *midi_output;
xtc_realtime *engine;
xtc_audio_plan *audio;
xtc_midi_plan *midi;
xtc_midi_output_plan *midi_out;
unsigned int output_seen,output_muted;
int input_channel,output_channel,midi_thru;
unsigned int panic_requested, panic_seen, midi_dropped, peak_left, peak_right;
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
float *timeline, *instrument, *monitor;
@ -72,6 +78,7 @@ static size_t decode_midi(xtc_backend *backend, uint64_t position, jack_nframes_
if (!buffer || total > XTC_MIDI_MAX_EVENTS) {
(void)__atomic_fetch_add(&backend->midi_dropped, total ? total : 1U, __ATOMIC_RELAXED);
xtc_midi_plan_panic(backend->midi, position);
(void)__atomic_fetch_add(&backend->panic_requested,1U,__ATOMIC_RELEASE);
} else
for (i = 0; i < total; ++i) {
jack_midi_event_t raw;
@ -79,6 +86,7 @@ static size_t decode_midi(xtc_backend *backend, uint64_t position, jack_nframes_
if (jack_midi_event_get(&raw, buffer, i) != 0 || raw.time >= frames) {
(void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED);
xtc_midi_plan_panic(backend->midi, position);
(void)__atomic_fetch_add(&backend->panic_requested,1U,__ATOMIC_RELEASE);
count = 0;
break;
}
@ -88,14 +96,43 @@ static size_t decode_midi(xtc_backend *backend, uint64_t position, jack_nframes_
if (result != XTC_OK || (count && events[count].offset < events[count - 1].offset)) {
(void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED);
xtc_midi_plan_panic(backend->midi, position);
(void)__atomic_fetch_add(&backend->panic_requested,1U,__ATOMIC_RELEASE);
count = 0;
break;
}
++count;
if(backend->input_channel<0 || events[count].channel==(unsigned int)backend->input_channel)++count;
}
return count;
}
static int write_midi(xtc_backend *b,void *buffer,xtc_midi_event e,unsigned int offset)
{
unsigned char bytes[3];
unsigned int channel=b->output_channel<0 ? e.channel : (unsigned int)b->output_channel;
bytes[0]=(unsigned char)((e.type==XTC_MIDI_NOTE_ON ? 0x90 : e.type==XTC_MIDI_NOTE_OFF ? 0x80 : e.type==XTC_MIDI_BEND ? 0xe0 : 0xb0)|channel);
bytes[1]=(unsigned char)(e.type==XTC_MIDI_ALL_OFF ? 123 : e.pitch);bytes[2]=(unsigned char)e.velocity;
return jack_midi_event_write(buffer,offset,bytes,3);
}
static void output_panic(xtc_backend *b,void *buffer,unsigned int offset)
{
unsigned int ch;
for(ch=0;ch<16;++ch){unsigned char bytes[3]={(unsigned char)(0xb0|ch),123,0};
if(jack_midi_event_write(buffer,offset,bytes,3)) (void)__atomic_fetch_add(&b->midi_dropped,1U,__ATOMIC_RELAXED);
}
}
static void output_segment(xtc_backend *b,void *buffer,uint64_t position,size_t frames,unsigned int offset,
const xtc_midi_event *live,size_t live_count)
{
xtc_midi_event scheduled[XTC_MIDI_MAX_EVENTS];size_t count=0,i=0,j=0;
if(__atomic_load_n(&b->output_muted,__ATOMIC_ACQUIRE))return;
xtc_result r=b->midi_out ? xtc_midi_output_events(b->midi_out,position,frames,scheduled,&count) : XTC_OK;
if(!b->midi_thru)live_count=0;
while(r==XTC_OK && (i<count || j<live_count)){
xtc_midi_event e=j==live_count || (i<count && scheduled[i].offset<=live[j].offset) ? scheduled[i++] : live[j++];
if(write_midi(b,buffer,e,offset+e.offset))r=XTC_ERR_CAPACITY;
}
if(r!=XTC_OK){jack_midi_clear_buffer(buffer);output_panic(b,buffer,0);(void)__atomic_fetch_add(&b->midi_dropped,1U,__ATOMIC_RELAXED);}
}
/**
* @brief Runs the JACK audio callback, rendering sources, processing the graph, and publishing status.
*
@ -109,7 +146,15 @@ static int process(jack_nframes_t frames, void *context)
float *output = jack_port_get_buffer(backend->output, frames);
float *right = backend->output_right ? jack_port_get_buffer(backend->output_right, frames) : NULL;
xtc_result result = XTC_OK;
void *midi_buffer=jack_port_get_buffer(backend->midi_output,frames);
unsigned int panic=__atomic_load_n(&backend->panic_requested,__ATOMIC_ACQUIRE);
if(midi_buffer){
jack_midi_clear_buffer(midi_buffer);
if(panic!=__atomic_load_n(&backend->output_seen,__ATOMIC_RELAXED) || __atomic_load_n(&backend->stop_done,__ATOMIC_ACQUIRE))output_panic(backend,midi_buffer,0);
__atomic_store_n(&backend->output_seen,panic,__ATOMIC_RELEASE);
}
if (output == NULL || (backend->output_right && !right)) {
if(midi_buffer){jack_midi_clear_buffer(midi_buffer);output_panic(backend,midi_buffer,0);}
if (output)
memset(output, 0, (size_t)frames * sizeof(*output));
if (right)
@ -156,6 +201,7 @@ static int process(jack_nframes_t frames, void *context)
for (i = 0; i < count; ++i) if (events[i].offset >= offset && events[i].offset < offset+n) {
segment_events[queued] = events[i]; segment_events[queued++].offset -= (unsigned int)offset;
}
if(midi_buffer)output_segment(backend,midi_buffer,position,n,(unsigned int)offset,segment_events,queued);
if (result == XTC_OK && backend->midi)
result = xtc_midi_plan_render_stereo(backend->midi,position,segment_events,queued,backend->instrument,
right ? backend->instrument+backend->max_frames : NULL,n);
@ -192,6 +238,7 @@ static int process(jack_nframes_t frames, void *context)
__atomic_store_n(&backend->stop_done, 1U, __ATOMIC_RELEASE);
}
if (result != XTC_OK) {
if(midi_buffer){jack_midi_clear_buffer(midi_buffer);output_panic(backend,midi_buffer,0);}
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
if (backend->recorder)
xtc_recorder_push(backend->recorder, NULL, 1U);
@ -344,12 +391,14 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_
goto fail;
backend->input = jack_port_register(backend->client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0U);
backend->output = jack_port_register(backend->client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U);
backend->input_channel=backend->output_channel=-1;
backend->midi_output=jack_port_register(backend->client,"midi_out",JACK_DEFAULT_MIDI_TYPE,JackPortIsOutput,0U);
backend->midi_input = jack_port_register(backend->client, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0U);
if (config->channel_count == 2U)
backend->output_right =
jack_port_register(backend->client, "output_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U);
if ((config->channel_count == 2U && !backend->output_right) || backend->input == NULL || backend->output == NULL ||
backend->midi_input == NULL) {
backend->midi_input == NULL || !backend->midi_output) {
result = XTC_ERR_IO;
goto fail;
}
@ -374,6 +423,7 @@ fail:
}
xtc_realtime_destroy(backend->engine);
xtc_audio_plan_destroy(backend->audio);
xtc_midi_output_destroy(backend->midi_out);
xtc_midi_plan_destroy(backend->midi);
free(backend->monitor);
free(backend->instrument);
@ -392,11 +442,13 @@ xtc_result xtc_backend_close(xtc_backend *backend)
{
if (backend == NULL)
return XTC_OK;
if(backend->ready && backend->running){xtc_result r=xtc_backend_stop(backend);if(r!=XTC_OK)return r;}
/* JACK removes this client and joins callbacks before returning success. */
if (jack_client_close(backend->client) != 0)
return XTC_ERR_IO;
xtc_realtime_destroy(backend->engine);
xtc_audio_plan_destroy(backend->audio);
xtc_midi_output_destroy(backend->midi_out);
xtc_midi_plan_destroy(backend->midi);
free(backend->monitor);
free(backend->instrument);
@ -421,8 +473,11 @@ xtc_result xtc_backend_start(xtc_backend *backend)
return XTC_ERR_INVALID_STATE;
if (backend->running)
return XTC_ERR_INVALID_STATE;
__atomic_store_n(&backend->output_muted,0U,__ATOMIC_RELEASE);
xtc_midi_output_reset(backend->midi_out);
xtc_midi_plan_reset(backend->midi);
backend->panic_seen = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE);
__atomic_store_n(&backend->output_seen,backend->panic_seen,__ATOMIC_RELEASE);
__atomic_store_n(&backend->stop_requested, 0U, __ATOMIC_RELAXED);
__atomic_store_n(&backend->stop_done, 0U, __ATOMIC_RELAXED);
backend->stop_seen = 0;
@ -451,6 +506,9 @@ xtc_result xtc_backend_stop(xtc_backend *backend)
xtc_midi_plan_reset(backend->midi);
return XTC_OK;
}
__atomic_store_n(&backend->output_muted,1U,__ATOMIC_RELEASE);
{unsigned int request=__atomic_add_fetch(&backend->panic_requested,1U,__ATOMIC_RELEASE);unsigned int tries;
for(tries=0;tries<100 && __atomic_load_n(&backend->output_seen,__ATOMIC_ACQUIRE)!=request;++tries){struct timespec pause={0,1000000};nanosleep(&pause,NULL);}}
if (jack_deactivate(backend->client) != 0)
return XTC_ERR_IO;
backend->running = 0;
@ -530,6 +588,7 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *
{
xtc_audio_plan *candidate = NULL;
xtc_midi_plan *midi = NULL;
xtc_midi_output_plan *midi_out=NULL;
float *monitor = NULL;
size_t tracks = 0, clips = 0, i;
xtc_result result;
@ -549,10 +608,12 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *
result = xtc_midi_plan_create(project, backend->expected_rate, &midi);
if (result == XTC_OK)
result = xtc_midi_plan_set_fade_frames(midi, backend->transition_frames);
if(result==XTC_OK)result=xtc_midi_output_create(project,backend->expected_rate,&midi_out);
if (result == XTC_OK)
result = xtc_backend_submit(backend, xtc_project_graph(project));
if (result != XTC_OK) {
xtc_audio_plan_destroy(candidate);
xtc_midi_output_destroy(midi_out);
xtc_midi_plan_destroy(midi);
free(monitor);
return result;
@ -571,7 +632,9 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *
xtc_midi_plan_monitor(midi, monitor, backend->max_frames);
xtc_audio_plan_destroy(backend->audio);
backend->audio = candidate;
xtc_midi_output_destroy(backend->midi_out);
xtc_midi_plan_destroy(backend->midi);
backend->midi_out=midi_out;
backend->midi = midi;
return XTC_OK;
}
@ -845,6 +908,38 @@ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direc
return route(backend, direction, remote, 1);
}
xtc_result xtc_backend_set_midi_options(xtc_backend *b,int input,int output,int thru)
{
if(!b || input < -1 || input>15 || output < -1 || output>15 || (thru!=0 && thru!=1))return XTC_ERR_INVALID_ARGUMENT;
if(!b->ready || b->running)return XTC_ERR_INVALID_STATE;
b->input_channel=input;b->output_channel=output;b->midi_thru=thru;return XTC_OK;
}
xtc_result xtc_backend_midi_name(const xtc_backend *b,xtc_port_direction d,const char **out)
{
if(!b || !out || (d!=XTC_PORT_INPUT && d!=XTC_PORT_OUTPUT))return XTC_ERR_INVALID_ARGUMENT;
if(!b->ready)return XTC_ERR_INVALID_STATE;
*out=jack_port_name(d==XTC_PORT_INPUT ? b->midi_input : b->midi_output);return XTC_OK;
}
xtc_result xtc_backend_visit_midi_ports(xtc_backend *b,xtc_port_direction d,xtc_backend_port_visitor visit,void *context)
{
const char **ports;size_t i;
if(!b || !visit || (d!=XTC_PORT_INPUT && d!=XTC_PORT_OUTPUT))return XTC_ERR_INVALID_ARGUMENT;
if(!b->ready || __atomic_load_n(&b->lost,__ATOMIC_ACQUIRE))return XTC_ERR_INVALID_STATE;
ports=jack_get_ports(b->client,NULL,JACK_DEFAULT_MIDI_TYPE,d==XTC_PORT_INPUT ? JackPortIsInput : JackPortIsOutput);
if(ports){for(i=0;ports[i];++i)if(strcmp(ports[i],jack_port_name(b->midi_input)) && strcmp(ports[i],jack_port_name(b->midi_output)) && visit(ports[i],context))break;jack_free(ports);}
return XTC_OK;
}
xtc_result xtc_backend_route_midi(xtc_backend *b,xtc_port_direction d,const char *remote,int disconnect)
{
const char *own;int code;xtc_result r;
if(!remote || !*remote || (disconnect!=0 && disconnect!=1))return XTC_ERR_INVALID_ARGUMENT;
r=xtc_backend_midi_name(b,d,&own);if(r!=XTC_OK)return r;
if(!b->running)return XTC_ERR_INVALID_STATE;
if(!strcmp(remote,jack_port_name(b->midi_input)) || !strcmp(remote,jack_port_name(b->midi_output)))return XTC_ERR_INVALID_ARGUMENT;
code=disconnect ? jack_disconnect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own) :
jack_connect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own);
return code==0 || (!disconnect && code==EEXIST) ? XTC_OK : XTC_ERR_IO;
}
xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out)
{
size_t i;
@ -1135,4 +1230,8 @@ xtc_result xtc_backend_request_stop(xtc_backend *backend)
xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out)
{ (void)backend; (void)out; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_set_midi_options(xtc_backend *b,int i,int o,int t){(void)b;(void)i;(void)o;(void)t;return XTC_ERR_UNSUPPORTED;}
xtc_result xtc_backend_midi_name(const xtc_backend *b,xtc_port_direction d,const char **o){(void)b;(void)d;(void)o;return XTC_ERR_UNSUPPORTED;}
xtc_result xtc_backend_visit_midi_ports(xtc_backend *b,xtc_port_direction d,xtc_backend_port_visitor v,void *c){(void)b;(void)d;(void)v;(void)c;return XTC_ERR_UNSUPPORTED;}
xtc_result xtc_backend_route_midi(xtc_backend *b,xtc_port_direction d,const char *r,int x){(void)b;(void)d;(void)r;(void)x;return XTC_ERR_UNSUPPORTED;}
#endif

65
src/midi/output.c Normal file
View file

@ -0,0 +1,65 @@
#include "output.h"
#include "../project/project_internal.h"
#include <stdlib.h>
#include <string.h>
typedef struct {uint64_t start,end;unsigned int pitch,velocity,channel;} output_note;
typedef struct {uint64_t sample;xtc_midi_event event;} output_event;
struct xtc_midi_output_plan {
size_t count,event_count;uint64_t next;
output_note notes[XTC_PROJECT_MAX_NOTES];
output_event events[XTC_PROJECT_MAX_NOTES*2];
};
static int compare(const void *a,const void *b)
{
const output_event *x=a,*y=b;
if(x->sample!=y->sample)return x->sample<y->sample ? -1 : 1;
if(x->event.type!=y->event.type)return x->event.type==XTC_MIDI_NOTE_OFF ? -1 : 1;
if(x->event.channel!=y->event.channel)return x->event.channel<y->event.channel ? -1 : 1;
return x->event.pitch<y->event.pitch ? -1 : x->event.pitch!=y->event.pitch;
}
void xtc_midi_output_destroy(xtc_midi_output_plan *p){free(p);}
void xtc_midi_output_reset(xtc_midi_output_plan *p){if(p)p->next=UINT64_MAX;}
xtc_result xtc_midi_output_create(const xtc_project *p,unsigned int rate,xtc_midi_output_plan **out)
{
xtc_midi_output_plan *plan;size_t i,j;int solo=0;xtc_result r;
if(!p || !out || rate<8000 || rate>192000)return XTC_ERR_INVALID_ARGUMENT;
plan=calloc(1,sizeof(*plan));if(!plan)return XTC_ERR_OUT_OF_MEMORY;
for(i=0;i<p->track_count;++i)if(!p->tracks[i].is_bus && p->tracks[i].solo)solo=1;
for(i=0;i<p->note_count;++i){
const xtc_note_info *n=p->notes+i;xtc_clip_info c;uint64_t begin,end,a,b;output_note *note;
r=xtc_project_find_clip(p,n->clip_id,&c);if(r!=XTC_OK){free(plan);return r;}
for(j=0;j<p->track_count;++j)if(p->tracks[j].id==c.track_id)break;
if(j==p->track_count || p->tracks[j].mute || (solo && !p->tracks[j].solo))continue;
begin=n->start_tick>c.offset_tick ? n->start_tick : c.offset_tick;
end=n->start_tick+n->length_tick;
if(end>c.offset_tick+c.length_tick)end=c.offset_tick+c.length_tick;
if(begin>=end)continue;
r=xtc_project_tick_to_sample(p,c.start_tick+begin-c.offset_tick,rate,&a);
if(r==XTC_OK)r=xtc_project_tick_to_sample(p,c.start_tick+end-c.offset_tick,rate,&b);
if(r!=XTC_OK){free(plan);return r;}
if(b<=a)b=a+1;
note=plan->notes+plan->count++;*note=(output_note){a,b,n->pitch,n->velocity,n->channel};
plan->events[plan->event_count++]=(output_event){a,{0,XTC_MIDI_NOTE_ON,n->channel,n->pitch,n->velocity}};
plan->events[plan->event_count++]=(output_event){b,{0,XTC_MIDI_NOTE_OFF,n->channel,n->pitch,0}};
}
qsort(plan->events,plan->event_count,sizeof(*plan->events),compare);plan->next=UINT64_MAX;*out=plan;return XTC_OK;
}
xtc_result xtc_midi_output_events(xtc_midi_output_plan *p,uint64_t position,size_t frames,xtc_midi_event *events,size_t *count)
{
size_t n=0,i,lo=0,hi;
if(!p || !events || !count || !frames || frames>65536 || position>UINT64_MAX-frames)return XTC_ERR_INVALID_ARGUMENT;
if(p->next!=position){
for(i=0;i<16;++i)events[n++]=(xtc_midi_event){0,XTC_MIDI_ALL_OFF,(unsigned int)i,123,0};
for(i=0;i<p->count;++i)if(p->notes[i].start<position && p->notes[i].end>position){
output_note *note=p->notes+i;if(n==XTC_MIDI_MAX_EVENTS)return XTC_ERR_CAPACITY;
events[n++]=(xtc_midi_event){0,XTC_MIDI_NOTE_ON,note->channel,note->pitch,note->velocity};
}
}
hi=p->event_count;
while(lo<hi){size_t mid=lo+(hi-lo)/2;if(p->events[mid].sample<position)lo=mid+1;else hi=mid;}
for(i=lo;i<p->event_count && p->events[i].sample<position+frames;++i){
if(n==XTC_MIDI_MAX_EVENTS)return XTC_ERR_CAPACITY;
events[n]=p->events[i].event;events[n++].offset=(unsigned int)(p->events[i].sample-position);
}
p->next=position+frames;*count=n;return XTC_OK;
}

11
src/midi/output.h Normal file
View file

@ -0,0 +1,11 @@
#ifndef XTC_MIDI_OUTPUT_H
#define XTC_MIDI_OUTPUT_H
#include "xtc/xtc_midi.h"
/* Prepared on control thread, single audio owner after publication. */
typedef struct xtc_midi_output_plan xtc_midi_output_plan;
xtc_result xtc_midi_output_create(const xtc_project *,unsigned int,xtc_midi_output_plan **);
void xtc_midi_output_destroy(xtc_midi_output_plan *);
void xtc_midi_output_reset(xtc_midi_output_plan *);
/* Discontinuities emit all-off plus chased notes. Capacity errors retain count; discard scratch events on error. */
xtc_result xtc_midi_output_events(xtc_midi_output_plan *,uint64_t,size_t,xtc_midi_event *,size_t *);
#endif

View file

@ -10,8 +10,8 @@
#include <time.h>
#include <unistd.h>
typedef struct {
jack_port_t *input, *right, *midi;
unsigned int command, seen, loud, right_loud, heard, bad;
jack_port_t *input, *right, *midi, *receive;
unsigned int command, seen, loud, right_loud, heard, bad, midi_on, midi_off;
} probe;
static int process(jack_nframes_t frames, void *context)
{
@ -23,6 +23,11 @@ static int process(jack_nframes_t frames, void *context)
unsigned int command = __atomic_load_n(&p->command, __ATOMIC_ACQUIRE), loud = 0;
jack_nframes_t i;
if (!input || !right || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
{void *received=jack_port_get_buffer(p->receive,frames);uint32_t count=jack_midi_get_event_count(received),j;
for(j=0;j<count;++j){jack_midi_event_t e;if(jack_midi_event_get(&e,received,j)==0 && e.size==3){
if((e.buffer[0]&0xf0)==0x90 && e.buffer[2])__atomic_fetch_add(&p->midi_on,1U,__ATOMIC_RELAXED);
if((e.buffer[0]&0xf0)==0x80 || ((e.buffer[0]&0xf0)==0xb0 && e.buffer[1]==123))__atomic_fetch_add(&p->midi_off,1U,__ATOMIC_RELAXED);
}}}
jack_midi_clear_buffer(midi);
if (command && command != p->seen) {
unsigned char bytes[3] = {0x90, 69, command % 2 ? 127 : 0};
@ -68,7 +73,9 @@ int main(void)
state.input = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
state.right = jack_port_register(client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
state.midi = jack_port_register(client, "midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);
REQUIRE(state.input && state.right && state.midi);
state.receive=jack_port_register(client,"receive",JACK_DEFAULT_MIDI_TYPE,JackPortIsInput,0);
REQUIRE(state.input && state.right && state.midi && state.receive);
REQUIRE(xtc_backend_set_midi_options(backend,-1,-1,1)==XTC_OK);
REQUIRE(jack_set_process_callback(client, process, &state) == 0 && jack_activate(client) == 0);
REQUIRE(xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_midi_port_name(backend, &destination) == XTC_OK);
@ -83,10 +90,12 @@ int main(void)
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
REQUIRE(xtc_backend_route_midi(backend,XTC_PORT_OUTPUT,jack_port_name(state.receive),0)==XTC_OK);
__atomic_store_n(&state.command, 1U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
__atomic_store_n(&state.command, 2U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(__atomic_load_n(&state.midi_on,__ATOMIC_RELAXED)>0 && __atomic_load_n(&state.midi_off,__ATOMIC_RELAXED)>0);
__atomic_store_n(&state.command, 3U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(xtc_backend_panic(backend) == XTC_OK); wait_blocks();

View file

@ -20,7 +20,8 @@ static JackXRunCallback xrun_cb;
static JackShutdownCallback shutdown_cb;
static void *context;
static float input[128], output[128], output_right[128];
static int client_token, input_token, output_token, right_token, midi_token;
static int client_token, input_token, output_token, right_token, midi_token, midi_output_token;
static unsigned char sent[512][3];static unsigned int sent_time[512],sent_count;
static unsigned int midi_count;
static jack_midi_event_t midi_events[256];
static int open_fail, register_fail, callback_fail, activate_fail, deactivate_fail, close_fail;
@ -46,6 +47,7 @@ jack_port_t *jack_port_register(jack_client_t *client, const char *name, const c
++port_calls;
if (register_fail == port_calls) return NULL;
if (!strcmp(name, "output_right")) return (jack_port_t *)&right_token;
if(!strcmp(name,"midi_out"))return (jack_port_t *)&midi_output_token;
if (!strcmp(type, JACK_DEFAULT_MIDI_TYPE)) return (jack_port_t *)&midi_token;
return (jack_port_t *)(flags == JackPortIsInput ? &input_token : &output_token);
}
@ -68,12 +70,15 @@ int jack_activate(jack_client_t *client)
int jack_deactivate(jack_client_t *client)
{ (void)client; if (deactivate_fail) return -1; active = 0; return 0; }
void *jack_port_get_buffer(jack_port_t *port, jack_nframes_t frames)
{ (void)frames; if (null_buffer) return NULL; if (port == (jack_port_t *)&midi_token) return &midi_token; if (port == (jack_port_t *)&right_token) return output_right; return port == (jack_port_t *)&input_token ? input : output; }
{ (void)frames; if (null_buffer) return NULL; if(port==(jack_port_t *)&midi_output_token)return &midi_output_token; if (port == (jack_port_t *)&midi_token) return &midi_token; if (port == (jack_port_t *)&right_token) return output_right; return port == (jack_port_t *)&input_token ? input : output; }
void jack_midi_clear_buffer(void *buffer){(void)buffer;sent_count=0;}
int jack_midi_event_write(void *buffer,jack_nframes_t time,const jack_midi_data_t *data,size_t size)
{(void)buffer;if(size!=3 || sent_count>=512)return -1;memcpy(sent[sent_count],data,3);sent_time[sent_count++]=time;return 0;}
uint32_t jack_midi_get_event_count(void *buffer) { (void)buffer; return midi_count; }
int jack_midi_event_get(jack_midi_event_t *event, void *buffer, uint32_t index)
{ (void)buffer; if (index >= midi_count || index >= 256) return -1; *event = midi_events[index]; return 0; }
const char *jack_port_name(const jack_port_t *port)
{ if (port == (jack_port_t *)&right_token) return "test:output_right"; if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
{ if(port==(jack_port_t *)&midi_output_token)return "test:midi_out"; if (port == (jack_port_t *)&right_token) return "test:output_right"; if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
const char **jack_get_ports(jack_client_t *client, const char *pattern, const char *type, unsigned long flags)
{
const char **list = malloc(2U * sizeof(*list));
@ -402,6 +407,23 @@ int main(void)
xtc_project_destroy(p);
}
CHECK(closed >= 104);
{
xtc_project *p=NULL;uint64_t t,c,n;const char *midi_name=NULL;unsigned char on[]={0x92,64,100};
CHECK(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Out",&t)==XTC_OK);
CHECK(xtc_project_add_midi_clip(p,t,0,3840,&c)==XTC_OK && xtc_project_add_note(p,c,0,960,60,90,1,&n)==XTC_OK);
CHECK(xtc_backend_open(&config,&backend)==XTC_OK && xtc_backend_prepare_project(backend,p)==XTC_OK);
CHECK(xtc_backend_midi_name(backend,XTC_PORT_OUTPUT,&midi_name)==XTC_OK && !strcmp(midi_name,"test:midi_out"));
CHECK(xtc_backend_set_midi_options(backend,2,4,1)==XTC_OK && xtc_backend_start(backend)==XTC_OK);
CHECK(xtc_backend_route_midi(backend,XTC_PORT_OUTPUT,"device:in",0)==XTC_OK);
CHECK(xtc_backend_set_midi_options(backend,-1,-1,0)==XTC_ERR_INVALID_STATE);
midi_count=1;midi_events[0]=(jack_midi_event_t){8,3,on};CHECK(process_cb(32,context)==0);
CHECK(sent_count==18 && sent[16][0]==0x94 && sent[16][1]==60 && sent_time[17]==8 && sent[17][1]==64);
on[0]=0x93;CHECK(process_cb(32,context)==0 && sent_count==0);
midi_count=0;CHECK(xtc_backend_panic(backend)==XTC_OK && process_cb(32,context)==0 && sent_count==16);
CHECK(xtc_backend_stop(backend)==XTC_OK && xtc_backend_seek(backend,23984)==XTC_OK && xtc_backend_start(backend)==XTC_OK);
CHECK(process_cb(32,context)==0 && sent_count==18 && sent[16][0]==0x94 && sent_time[16]==0 && sent[17][0]==0x84 && sent_time[17]==16);
CHECK(xtc_backend_stop(backend)==XTC_OK && xtc_backend_close(backend)==XTC_OK);xtc_project_destroy(p);
}
CHECK(xtc_backend_close(NULL) == XTC_OK);
xtc_graph_destroy(invalid_graph);
xtc_graph_destroy(graph);