feat(midi): import Standard MIDI Files with transactional undo (phase 104)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:28:25 +01:00
commit f37ad8987a
9 changed files with 234 additions and 5 deletions

View file

@ -60,3 +60,25 @@ are cropped and notes outside the trim are omitted. Trailing silence remains.
Export does not edit the project or undo history. Audio clips reject. Only note
messages are exported: instruments, opaque plugin data, controller maps,
automation and audio are excluded. Export is unavailable during recording.
## Import MIDI files
File → Import MIDI (also in the sequencer Create menu) adds a clip at the cursor
on the selected non-bus track, or creates a track when none is selected. Import
is one undoable prepared edit; malformed files, capacity errors and failed
backend preparation retain the old project and IDs.
The reader supports Standard MIDI File types 0 and 1 with PPQN timing, including
running status and velocity-zero note-offs. File tracks merge into one clip;
channels and velocities remain. Timing rounds to 960 ticks per quarter note,
with positive sub-tick notes given one tick. Trailing silence remains. The project
tempo is retained: tempo maps, time signatures, CC/sustain, program changes,
pitch bend, SysEx and other metadata are not imported. This is a notes workflow,
not full-song or instrument reconstruction.
Limits are 1 MiB per file, 64 file tracks, 65,536 parsed events and 4,096 notes
within the project's total note capacity. SMPTE timing and type 2 reject explicitly.
Unterminated notes, zero source duration and malformed events reject rather than
silently dropping notes. Unmatched note-offs are harmless and ignored. Overlapping
same-pitch notes pair FIFO within each file track/channel. Project save/reload
uses the existing format; no format migration or extra dependency is required.

View file

@ -2286,12 +2286,12 @@ Implemented: a C99 atomic type-0/960-PPQN SMF exporter and GUI save chooser expo
Import type 0/1 PPQN MIDI notes onto the selected track or a new track, with safe parsing, explicit limits, tempo policy and one undoable prepared adoption.
### 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: bounded C99 type 0/1 PPQN notes import with running status, channel/velocity preservation, tick conversion, track merging and retained project tempo. File/Import MIDI and Create/Import MIDI add at the cursor on the current or a new track through one prepared undo edit. Export/import roundtrips, independent type-1 fixture, every truncation prefix, malformed/unsupported/bus/overflow rejection, allocation-failure rollback and GUI import/undo/redo/new-track/failed-adoption checks passed, together with the full core/reliability suite. Notes-only limits are explicit in MIDI_WORKFLOWS.md.
---

View file

@ -10,6 +10,13 @@ extern "C" {
* Includes clip name and trailing silence. No CC/program/plugin/audio data.
* Existing destination survives validation/write failures before atomic rename. */
xtc_result xtc_project_export_midi_clip(const xtc_project *,uint64_t clip,const char *path);
/* Transactional notes-only type 0/1 PPQN import into an existing non-bus track.
* Merges file tracks, preserving channels, rounds timing to 960 PPQN and retains
* trailing silence. Keeps project tempo; ignores meta/CC/program/bend/SysEx data.
* Max 1 MiB, 64 tracks, 65536 events and 4096 notes. SMPTE/type 2 unsupported.
* Unclosed notes or malformed data reject; same-pitch overlap pairs FIFO per
* file track/channel. IDs/project/out_clip remain unchanged on every error. */
xtc_result xtc_project_import_midi(xtc_project *,uint64_t track,uint64_t start,const char *path,uint64_t *out_clip);
#ifdef __cplusplus
}
#endif

View file

@ -129,6 +129,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
menu->add("File/New", FL_CTRL + 'n', file_callback, this);
menu->add("File/Open", FL_CTRL + 'o', file_callback, this);
menu->add("File/Save", FL_CTRL + 's', file_callback, this);
menu->add("File/Import MIDI...",0,[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->import_midi_dialog();},this);
menu->add("File/Export selected clip as MIDI...",0,[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->export_midi_dialog();},this);
menu->add("File/Export WAV",0,[](Fl_Widget *,void *context){static_cast<MainWindow *>(context)->export_audio();},this);
menu->add("File/Save As", FL_CTRL + FL_SHIFT + 's', file_callback, this);
@ -526,6 +527,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
add_track->add("Empty track",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->add_track();},sequencer_);
add_track->add("New MIDI track",0,midi_create,this);
add_track->add("New audio track",0,audio_create,this);
add_track->add("Import MIDI",0,[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->import_midi_dialog();},this);
add_track->add("Import WAV",0,audio_file_callback,this);
add_track->add("Load instrument plugin",0,instrument_callback,this);
add_track->add("Edit MIDI notes",0,piano_callback,this);
@ -2081,6 +2083,27 @@ xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsig
if (r != XTC_OK) xtc_project_destroy(candidate); else { sequencer_->select(track,clip); mixer_->select_track(track); mark_dirty(); }
return r;
}
xtc_result MainWindow::import_midi_file(const char *path)
{
if(recorder_)return XTC_ERR_INVALID_STATE;
uint64_t track=sequencer_->selected_track(),start=sequencer_->position(),clip=0;xtc_project *candidate=nullptr;
xtc_result r=before_arrangement_edit(this);
if(r==XTC_OK)r=xtc_project_clone(project_.get(),&candidate);
if(r==XTC_OK && track==UINT64_MAX)r=xtc_project_add_track(candidate,"Imported MIDI",&track);
if(r==XTC_OK)r=xtc_project_import_midi(candidate,track,start,path,&clip);
if(r==XTC_OK)r=install_project(candidate,false);
if(r!=XTC_OK)xtc_project_destroy(candidate);
else{sequencer_->select(track,clip);mixer_->select_track(track);mark_dirty();refresh_status();}
return r;
}
void MainWindow::import_midi_dialog()
{
if(recorder_){report_error("Stop recording before importing MIDI",XTC_ERR_INVALID_STATE);return;}
Fl_Native_File_Chooser chooser;chooser.title("Import MIDI notes (keeps project tempo)");chooser.type(Fl_Native_File_Chooser::BROWSE_FILE);chooser.filter("Standard MIDI File\t*.{mid,midi}");
if(chooser.show()!=0)return;
auto r=import_midi_file(chooser.filename());if(r!=XTC_OK)report_error("MIDI import failed (type 0/1 PPQN notes required)",r);
else set_label(error_,"Imported MIDI notes at cursor; file tracks merged and project tempo retained. Undo reverses import.");
}
xtc_result MainWindow::export_midi_clip(const char *path)
{
if(recorder_)return XTC_ERR_INVALID_STATE;

View file

@ -80,6 +80,8 @@ class MainWindow : public Fl_Double_Window {
void export_audio();
xtc_result export_midi_clip(const char *path);
void export_midi_dialog();
xtc_result import_midi_file(const char *path);
void import_midi_dialog();
xtc_result load_sampler(uint64_t track, const char *path);
xtc_result set_instrument_spec(uint64_t track, const xtc_plugin_spec &spec);
xtc_result select_live_instrument(uint64_t track);

View file

@ -58,3 +58,106 @@ xtc_result xtc_project_export_midi_clip(const xtc_project *p,uint64_t id,const c
if(r!=XTC_OK)unlink(temporary);
return r;
}
#define SMF_INPUT_BYTES (1024U*1024U)
#define SMF_MAX_EVENTS 65536U
typedef struct {
xtc_note_info notes[XTC_PROJECT_MAX_NOTES];
int next[XTC_PROJECT_MAX_NOTES],head[16][128],tail[16][128];
size_t count,events;
uint64_t end;
} smf_import;
static uint32_t get32(const unsigned char *p){return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|((uint32_t)p[2]<<8)|p[3];}
static unsigned int get16(const unsigned char *p){return ((unsigned int)p[0]<<8)|p[1];}
static int get_vlq(const unsigned char *data,size_t end,size_t *at,uint32_t *value)
{
unsigned int i;uint32_t v=0;
for(i=0;i<4;++i){unsigned char byte;if(*at>=end)return 0;byte=data[(*at)++];v=(v<<7)|(byte&127);if(!(byte&128)){*value=v;return 1;}}
return 0;
}
static xtc_result parse_track(smf_import *s,const unsigned char *data,size_t begin,size_t end,unsigned int division)
{
size_t at=begin,i;uint64_t tick=0;unsigned int running=0,ch,pitch;int eot=0;
for(ch=0;ch<16;++ch)for(pitch=0;pitch<128;++pitch)s->head[ch][pitch]=s->tail[ch][pitch]=-1;
while(at<end){
uint32_t delta,length;unsigned int status,a,b=0,type;
if(++s->events>SMF_MAX_EVENTS)return XTC_ERR_CAPACITY;
if(!get_vlq(data,end,&at,&delta) || at>=end)return XTC_ERR_FORMAT;
tick+=delta;if(tick>XTC_PROJECT_MAX_TICK*division/960)return XTC_ERR_CAPACITY;
status=data[at];
if(status&128){++at;if(status<0xf0)running=status;else running=0;}
else {if(!running)return XTC_ERR_FORMAT;status=running;}
if(status==0xff){
if(at>=end)return XTC_ERR_FORMAT;
type=data[at++];if(type>127 || !get_vlq(data,end,&at,&length) || length>end-at)return XTC_ERR_FORMAT;
if(type==0x2f){if(length || at!=end)return XTC_ERR_FORMAT;eot=1;break;}
if(type==0x51 && (length!=3 || !(data[at]|data[at+1]|data[at+2])))return XTC_ERR_FORMAT;
at+=length;continue;
}
if(status==0xf0 || status==0xf7){if(!get_vlq(data,end,&at,&length) || length>end-at)return XTC_ERR_FORMAT;at+=length;continue;}
if(status<0x80 || status>=0xf0 || at>=end)return XTC_ERR_FORMAT;
a=data[at++];if(a>127)return XTC_ERR_FORMAT;
type=status&0xf0;ch=status&15;
if(type!=0xc0 && type!=0xd0){if(at>=end)return XTC_ERR_FORMAT;b=data[at++];if(b>127)return XTC_ERR_FORMAT;}
if(type==0x90 && b){
xtc_note_info *note;int index;
if(s->count==XTC_PROJECT_MAX_NOTES)return XTC_ERR_CAPACITY;
index=(int)s->count;note=s->notes+s->count++;*note=(xtc_note_info){0,0,tick,0,a,b,ch};s->next[index]=-1;
if(s->head[ch][a]<0)s->head[ch][a]=index;else s->next[s->tail[ch][a]]=index;
s->tail[ch][a]=index;
}else if(type==0x80 || (type==0x90 && !b)){
int index=s->head[ch][a];if(index<0)continue; /* Harmless unmatched release. */
if(tick<=s->notes[index].start_tick)return XTC_ERR_FORMAT;
s->notes[index].length_tick=tick-s->notes[index].start_tick;
s->head[ch][a]=s->next[index];if(s->head[ch][a]<0)s->tail[ch][a]=-1;
}
}
if(!eot)return XTC_ERR_FORMAT;
for(i=0;i<16;++i)for(pitch=0;pitch<128;++pitch)if(s->head[i][pitch]>=0)return XTC_ERR_FORMAT;
if(tick>s->end)s->end=tick;
return XTC_OK;
}
xtc_result xtc_project_import_midi(xtc_project *p,uint64_t track,uint64_t start,const char *path,uint64_t *out_clip)
{
unsigned char *data;smf_import *s;xtc_project *copy=NULL;size_t size,at,i;FILE *f;xtc_result r=XTC_OK;
unsigned int format,tracks,division;uint64_t clip=0,length;size_t track_index;
if(!p || !path || !*path || !out_clip || start>XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT;
for(track_index=0;track_index<p->track_count;++track_index)if(p->tracks[track_index].id==track)break;
if(track_index==p->track_count)return XTC_ERR_NOT_FOUND;
if(p->tracks[track_index].is_bus)return XTC_ERR_INVALID_STATE;
f=fopen(path,"rb");if(!f)return errno==ENOENT ? XTC_ERR_NOT_FOUND : XTC_ERR_IO;
data=malloc(SMF_INPUT_BYTES+1);s=calloc(1,sizeof(*s));
if(!data || !s){fclose(f);free(data);free(s);return XTC_ERR_OUT_OF_MEMORY;}
size=fread(data,1,SMF_INPUT_BYTES+1,f);if(ferror(f))r=XTC_ERR_IO;if(fclose(f)!=0)r=XTC_ERR_IO;
if(r==XTC_OK && size>SMF_INPUT_BYTES)r=XTC_ERR_CAPACITY;
if(r==XTC_OK && (size<14 || memcmp(data,"MThd",4) || get32(data+4)<6 || get32(data+4)>size-8))r=XTC_ERR_FORMAT;
if(r!=XTC_OK)goto done;
format=get16(data+8);tracks=get16(data+10);division=get16(data+12);at=8+get32(data+4);
if(format>1 || (division&0x8000)){r=XTC_ERR_UNSUPPORTED;goto done;}
if(!division || !tracks || (format==0 && tracks!=1)){r=XTC_ERR_FORMAT;goto done;}
if(tracks>64){r=XTC_ERR_CAPACITY;goto done;}
for(i=0;r==XTC_OK && i<tracks;++i){
uint32_t bytes;if(size-at<8 || memcmp(data+at,"MTrk",4)){r=XTC_ERR_FORMAT;break;}
bytes=get32(data+at+4);at+=8;if(bytes>size-at){r=XTC_ERR_FORMAT;break;}
r=parse_track(s,data,at,at+bytes,division);at+=bytes;
}
if(r==XTC_OK && at!=size)r=XTC_ERR_FORMAT;
if(r==XTC_OK && !s->count)r=XTC_ERR_NOT_FOUND;
if(r!=XTC_OK)goto done;
length=(s->end*960+division/2)/division;if(!length)length=1;
for(i=0;i<s->count;++i){
xtc_note_info *note=s->notes+i;uint64_t end=((note->start_tick+note->length_tick)*960+division/2)/division;
note->start_tick=(note->start_tick*960+division/2)/division;
if(end<=note->start_tick)end=note->start_tick+1;
note->length_tick=end-note->start_tick;if(end>length)length=end;
}
if(length>XTC_PROJECT_MAX_TICK-start){r=XTC_ERR_CAPACITY;goto done;}
r=xtc_project_clone(p,&copy);
if(r==XTC_OK)r=xtc_project_add_midi_clip(copy,track,start,length,&clip);
for(i=0;r==XTC_OK && i<s->count;++i){xtc_note_info *note=s->notes+i;uint64_t id;
r=xtc_project_add_note(copy,clip,note->start_tick,note->length_tick,note->pitch,note->velocity,note->channel,&id);
}
if(r==XTC_OK){xtc_graph *old=p->graph;*p=*copy;copy->graph=old;*out_clip=clip;}
done:
xtc_project_destroy(copy);free(s);free(data);return r;
}

View file

@ -372,6 +372,17 @@ int midi_workflow_tests()
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);}
size_t tracks=0,clips=0;CHECK(xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK);auto original_clips=clips;
s->position(3840);CHECK(window.import_midi_file(midi.c_str())==XTC_OK);
CHECK(xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && clips==original_clips+1);
xtc_clip_info imported{};CHECK(xtc_project_find_clip(s->project(),s->selected_clip(),&imported)==XTC_OK && imported.start_tick==3840 && imported.midi);
CHECK(window.undo()==XTC_OK && xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && clips==original_clips);
original=s->project();submit_result=XTC_ERR_INVALID_STATE;CHECK(window.import_midi_file(midi.c_str())==XTC_ERR_INVALID_STATE && s->project()==original);submit_result=XTC_OK;
CHECK(window.import_midi_file("/no-such-xtc-file.mid")==XTC_ERR_NOT_FOUND && s->project()==original);
CHECK(window.new_project()==XTC_OK && window.import_midi_file(midi.c_str())==XTC_OK);
CHECK(xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && tracks==1 && clips==1);
CHECK(window.undo()==XTC_OK && xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && !tracks && !clips);
CHECK(window.undo(true)==XTC_OK && xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && tracks==1 && clips==1);
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

@ -8,6 +8,7 @@
#include "xtc/xtc_editing.h"
#include "xtc/xtc_audio.h"
#include "xtc/xtc_sample.h"
#include "xtc/xtc_midi_file.h"
#include <dirent.h>
#include <assert.h>
#include <stdio.h>
@ -294,6 +295,26 @@ static void sample_failures(void)
}
xtc_sample_destroy(source);xtc_project_destroy(p);assert(rmdir(root)==0);
}
static void midi_file_failures(void)
{
xtc_project *p=NULL;uint64_t t,c,n;char path[]="/tmp/xtc-midi-failure-XXXXXX";int fd=mkstemp(path);
assert(fd>=0);close(fd);assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"MIDI",&t)==XTC_OK);
assert(xtc_project_add_midi_clip(p,t,0,1920,&c)==XTC_OK && xtc_project_add_note(p,c,0,480,60,100,2,&n)==XTC_OK);
assert(xtc_project_export_midi_clip(p,c,path)==XTC_OK);
for(int operation=0;operation<2;++operation){int succeeded=0;
for(int failure=0;failure<128;++failure){
xtc_project *copy=NULL;uint64_t out=999;size_t tracks,clips,notes;xtc_result r;
assert(xtc_project_clone(p,&copy)==XTC_OK);fail_after=failure;
r=operation ? xtc_project_import_midi(copy,t,3840,path,&out) : xtc_project_export_midi_clip(copy,c,path);fail_after=-1;
if(r==XTC_OK)succeeded=1;
else{assert(r==XTC_ERR_OUT_OF_MEMORY && out==999);
assert(xtc_project_get_counts(copy,&tracks,&clips)==XTC_OK && tracks==1 && clips==1 && xtc_project_note_count(copy,&notes)==XTC_OK && notes==1);
{FILE *f=fopen(path,"rb");char signature[4];assert(f && fread(signature,1,4,f)==4 && !memcmp(signature,"MThd",4));fclose(f);}
}
xtc_project_destroy(copy);if(succeeded)break;
}assert(succeeded);
}xtc_project_destroy(p);unlink(path);
}
static void strum_failures(void)
{
xtc_project *p=NULL;uint64_t t,c,n;int succeeded=0;
@ -340,6 +361,7 @@ static void track_insert_failures(void)
}
int main(void)
{
midi_file_failures();
strum_failures();
track_insert_failures();
sample_failures();

View file

@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_midi_file.h"
#include "xtc/xtc_mixer.h"
#include "../../src/midi/output.h"
#include <assert.h>
#include <stdlib.h>
@ -24,6 +25,39 @@ static void output_tests(void)
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);
}
static void write_bytes(const char *path,const unsigned char *data,size_t size)
{
FILE *f=fopen(path,"wb");assert(f && fwrite(data,1,size,f)==size && fclose(f)==0);
}
static void import_tests(void)
{
static const unsigned char song[]={
'M','T','h','d',0,0,0,6,0,1,0,2,1,0xe0,
'M','T','r','k',0,0,0,12,0,0xff,0x51,3,7,0xa1,0x20,0x87,0x40,0xff,0x2f,0,
'M','T','r','k',0,0,0,27,
0,0xc1,10,0,0xf0,2,1,0xf7,
0,0x91,60,90,0,64,80,0x81,0x70,60,0,0,64,0,0x81,0x70,0xff,0x2f,0
};
unsigned char bad[sizeof(song)];char path[]="/tmp/xtc-midi-import-XXXXXX";int fd=mkstemp(path);
xtc_project *p=NULL;uint64_t t,c=999;xtc_clip_info clip;xtc_note_info note;size_t tracks,clips,notes;
assert(fd>=0);close(fd);assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Import",&t)==XTC_OK);
assert(xtc_project_set_tempo(p,135000)==XTC_OK);write_bytes(path,song,sizeof(song));
assert(xtc_project_import_midi(p,t,3840,path,&c)==XTC_OK && xtc_project_tempo(p)==135000);
assert(xtc_project_find_clip(p,c,&clip)==XTC_OK && clip.start_tick==3840 && clip.length_tick==1920);
assert(xtc_project_note_count(p,&notes)==XTC_OK && notes==2);
assert(xtc_project_get_note(p,0,&note)==XTC_OK && note.start_tick==0 && note.length_tick==480 && note.channel==1 && note.pitch==60 && note.velocity==90);
assert(xtc_project_get_note(p,1,&note)==XTC_OK && note.pitch==64 && note.length_tick==480 && note.velocity==80);
for(size_t size=0;size<sizeof(song);++size){
c=999;write_bytes(path,song,size);assert(xtc_project_import_midi(p,t,0,path,&c)!=XTC_OK && c==999);
assert(xtc_project_get_counts(p,&tracks,&clips)==XTC_OK && tracks==1 && clips==1 && xtc_project_note_count(p,&notes)==XTC_OK && notes==2);
}
memcpy(bad,song,sizeof(song));bad[12]=0xe7;write_bytes(path,bad,sizeof(bad));assert(xtc_project_import_midi(p,t,0,path,&c)==XTC_ERR_UNSUPPORTED);
memcpy(bad,song,sizeof(song));bad[9]=2;write_bytes(path,bad,sizeof(bad));assert(xtc_project_import_midi(p,t,0,path,&c)==XTC_ERR_UNSUPPORTED);
memcpy(bad,song,sizeof(song));bad[sizeof(bad)-6]=0x90;write_bytes(path,bad,sizeof(bad));assert(xtc_project_import_midi(p,t,0,path,&c)!=XTC_OK);
write_bytes(path,song,sizeof(song));assert(xtc_project_import_midi(p,t,XTC_PROJECT_MAX_TICK-10,path,&c)==XTC_ERR_CAPACITY && c==999);
{uint64_t bus;assert(xtc_project_add_bus(p,"Bus",&bus)==XTC_OK && xtc_project_import_midi(p,bus,0,path,&c)==XTC_ERR_INVALID_STATE);}
xtc_project_destroy(p);unlink(path);
}
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;
@ -48,5 +82,10 @@ int main(void)
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;
{uint64_t imported;xtc_note_info note;xtc_clip_info clip;
assert(xtc_project_import_midi(p,t,7680,path,&imported)==XTC_OK && xtc_project_tempo(p)==150000);
assert(xtc_project_find_clip(p,imported,&clip)==XTC_OK && clip.length_tick==960 && !clip.offset_tick);
assert(xtc_project_get_note(p,3,&note)==XTC_OK && note.start_tick==0 && note.length_tick==240 && note.pitch==60 && note.channel==2);
assert(xtc_project_get_note(p,4,&note)==XTC_OK && note.start_tick==240 && note.length_tick==720 && note.pitch==64 && note.channel==3);}
xtc_project_destroy(p);unlink(path);output_tests();import_tests();puts("MIDI file and bounded external scheduling checks passed.");return 0;
}