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

@ -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;
}