feat(midi): export selected clip as an atomic Standard MIDI File (phase 103)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:26:20 +01:00
commit 02ee1818a3
9 changed files with 178 additions and 5 deletions

View file

@ -6,6 +6,7 @@
#include "../../src/gui/mixer_widget.hpp"
#include "../../src/gui/plugin_dialog.hpp"
#include "../../src/gui/preferences_dialog.hpp"
#include "xtc/xtc_midi_file.h"
#include "../../src/gui/command_palette.hpp"
#include <FL/Fl_Value_Input.H>
#include "../../src/gui/plugin_library.hpp"
@ -366,6 +367,12 @@ int midi_workflow_tests()
CHECK(window.settings().midi_output_channel==4);
auto missing=settings;strcpy(missing.midi_input_port,"missing:port");CHECK(window.apply_preferences(missing)==XTC_OK);
CHECK(window.start_configured_backend()==XTC_ERR_NOT_FOUND && !handle.running);CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(window.create_track(true)==XTC_OK && window.open_piano_roll()==XTC_OK);
CHECK(window.piano_roll()->add_note(0,240,64,90,2)==XTC_OK);
std::string midi=std::string(dir)+"/clip.mid";auto *original=s->project();
CHECK(window.export_midi_clip(midi.c_str())==XTC_OK && s->project()==original);
{char signature[4];FILE *f=fopen(midi.c_str(),"rb");CHECK(f && fread(signature,1,4,f)==4 && !memcmp(signature,"MThd",4));if(f)fclose(f);}
unlink(midi.c_str());window.detach_piano(false);
s->grid(480);window.show_track_inspector(true);window.resize(20,30,1300,820);
CHECK(window.set_loop(960,3840,true)==XTC_OK);window.do_callback();CHECK(!window.shown());
}

View file

@ -0,0 +1,52 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_midi_file.h"
#include "../../src/midi/output.h"
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static unsigned int read_vlq(const unsigned char *data,size_t size,size_t *at)
{
unsigned int value=0;unsigned char byte;int n=0;
do{assert(*at<size && ++n<=4);byte=data[(*at)++];value=(value<<7)|(byte&127);}while(byte&128);
return value;
}
static void output_tests(void)
{
xtc_project *p=NULL;xtc_midi_output_plan *plan=NULL;uint64_t t,c,n;xtc_midi_event events[XTC_MIDI_MAX_EVENTS];size_t count=999;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Output",&t)==XTC_OK);
assert(xtc_project_add_midi_clip(p,t,0,1920,&c)==XTC_OK);
for(size_t i=0;i<129;++i)assert(xtc_project_add_note(p,c,i*2,1,60,100,0,&n)==XTC_OK);
assert(xtc_midi_output_create(p,8000,&plan)==XTC_OK);
assert(xtc_midi_output_events(plan,0,4000,events,&count)==XTC_ERR_CAPACITY && count==999);
assert(xtc_midi_output_events(plan,0,1,events,&count)==XTC_OK && count==17 && events[16].type==XTC_MIDI_NOTE_ON);
assert(xtc_midi_output_events(plan,1,4,events,&count)==XTC_OK && count==1 && events[0].type==XTC_MIDI_NOTE_OFF && events[0].offset==3);
xtc_midi_output_destroy(plan);xtc_project_destroy(p);
}
int main(void)
{
xtc_project *p=NULL;uint64_t t,c,n,audio;char path[]="/tmp/xtc-midi-file-XXXXXX";int fd=mkstemp(path);FILE *f;unsigned char bytes[1024];size_t size,at=22,notes=0;unsigned int tick=0;
assert(fd>=0);close(fd);assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Keys",&t)==XTC_OK);
assert(xtc_project_set_tempo(p,150000)==XTC_OK && xtc_project_add_midi_clip(p,t,1000,1920,&c)==XTC_OK);
assert(xtc_project_add_note(p,c,0,480,60,80,2,&n)==XTC_OK && xtc_project_add_note(p,c,480,960,64,100,3,&n)==XTC_OK);
assert(xtc_project_add_note(p,c,1600,100,72,70,0,&n)==XTC_OK);
assert(xtc_project_edit_clip(p,c,t,1000,960,240)==XTC_OK);
assert(xtc_project_export_midi_clip(p,c,path)==XTC_OK);
f=fopen(path,"rb");assert(f);size=fread(bytes,1,sizeof(bytes),f);assert(fclose(f)==0);
assert(size>22 && !memcmp(bytes,"MThd\0\0\0\6\0\0\0\1\3\300MTrk",18));
while(at<size){unsigned int status,len;tick+=read_vlq(bytes,size,&at);assert(at<size);status=bytes[at++];
if(status==0xff){unsigned int type=bytes[at++];len=read_vlq(bytes,size,&at);assert(len<=size-at);
if(type==0x51)assert(len==3 && bytes[at]==6 && bytes[at+1]==0x1a && bytes[at+2]==0x80);
if(type==0x2f)assert(!len && tick==960 && at==size);
at+=len;
}else{unsigned int expected_tick[]={0,240,240,960},statuses[]={0x92,0x82,0x93,0x83},pitches[]={60,60,64,64},velocities[]={80,0,100,0};
assert(notes<4 && tick==expected_tick[notes] && status==statuses[notes] && at+2<=size && bytes[at]==pitches[notes] && bytes[at+1]==velocities[notes]);at+=2;++notes;
}
}
assert(notes==4);
assert(xtc_project_add_clip(p,t,"Audio",3840,960,&audio)==XTC_OK && xtc_project_export_midi_clip(p,audio,path)==XTC_ERR_INVALID_STATE);
{unsigned char preserved[1024];f=fopen(path,"rb");assert(f && fread(preserved,1,sizeof(preserved),f)==size && !memcmp(bytes,preserved,size));fclose(f);}
assert(xtc_project_export_midi_clip(p,c,"/no-such-xtc-directory/test.mid")==XTC_ERR_IO);
xtc_project_destroy(p);unlink(path);output_tests();puts("MIDI file and bounded external scheduling checks passed.");return 0;
}