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 Export does not edit the project or undo history. Audio clips reject. Only note
messages are exported: instruments, opaque plugin data, controller maps, messages are exported: instruments, opaque plugin data, controller maps,
automation and audio are excluded. Export is unavailable during recording. 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. 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 ### Tasks and acceptance criteria
- [ ] Implement and document the workflow with bounded validation and safe ownership. - [x] Implement and document the workflow with bounded validation and safe ownership.
- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo. - [x] Pass meaningful core and GUI checks, including errors and persistence or undo.
- [ ] Commit this phase separately after validation. - [x] Commit this phase separately after validation.
### Implementation status ### 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. * Includes clip name and trailing silence. No CC/program/plugin/audio data.
* Existing destination survives validation/write failures before atomic rename. */ * 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); 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 #ifdef __cplusplus
} }
#endif #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/New", FL_CTRL + 'n', file_callback, this);
menu->add("File/Open", FL_CTRL + 'o', 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/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 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/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); 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("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 MIDI track",0,midi_create,this);
add_track->add("New audio track",0,audio_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("Import WAV",0,audio_file_callback,this);
add_track->add("Load instrument plugin",0,instrument_callback,this); add_track->add("Load instrument plugin",0,instrument_callback,this);
add_track->add("Edit MIDI notes",0,piano_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(); } if (r != XTC_OK) xtc_project_destroy(candidate); else { sequencer_->select(track,clip); mixer_->select_track(track); mark_dirty(); }
return r; 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) xtc_result MainWindow::export_midi_clip(const char *path)
{ {
if(recorder_)return XTC_ERR_INVALID_STATE; if(recorder_)return XTC_ERR_INVALID_STATE;

View file

@ -80,6 +80,8 @@ class MainWindow : public Fl_Double_Window {
void export_audio(); void export_audio();
xtc_result export_midi_clip(const char *path); xtc_result export_midi_clip(const char *path);
void export_midi_dialog(); 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 load_sampler(uint64_t track, const char *path);
xtc_result set_instrument_spec(uint64_t track, const xtc_plugin_spec &spec); xtc_result set_instrument_spec(uint64_t track, const xtc_plugin_spec &spec);
xtc_result select_live_instrument(uint64_t track); 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); if(r!=XTC_OK)unlink(temporary);
return r; 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(); 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); 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);} {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); unlink(midi.c_str());window.detach_piano(false);
s->grid(480);window.show_track_inspector(true);window.resize(20,30,1300,820); 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()); 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_editing.h"
#include "xtc/xtc_audio.h" #include "xtc/xtc_audio.h"
#include "xtc/xtc_sample.h" #include "xtc/xtc_sample.h"
#include "xtc/xtc_midi_file.h"
#include <dirent.h> #include <dirent.h>
#include <assert.h> #include <assert.h>
#include <stdio.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); 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) static void strum_failures(void)
{ {
xtc_project *p=NULL;uint64_t t,c,n;int succeeded=0; 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) int main(void)
{ {
midi_file_failures();
strum_failures(); strum_failures();
track_insert_failures(); track_insert_failures();
sample_failures(); sample_failures();

View file

@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L #define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_midi_file.h" #include "xtc/xtc_midi_file.h"
#include "xtc/xtc_mixer.h"
#include "../../src/midi/output.h" #include "../../src/midi/output.h"
#include <assert.h> #include <assert.h>
#include <stdlib.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); 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); 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) 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; 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); 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);} {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); 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;
} }