feat(midi): import Standard MIDI Files with transactional undo (phase 104)
This commit is contained in:
parent
02ee1818a3
commit
f37ad8987a
9 changed files with 234 additions and 5 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
103
src/midi/file.c
103
src/midi/file.c
|
|
@ -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,©);
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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,©)==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,¬es)==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();
|
||||
|
|
|
|||
|
|
@ -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,¬es)==XTC_OK && notes==2);
|
||||
assert(xtc_project_get_note(p,0,¬e)==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,¬e)==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,¬es)==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,¬e)==XTC_OK && note.start_tick==0 && note.length_tick==240 && note.pitch==60 && note.channel==2);
|
||||
assert(xtc_project_get_note(p,4,¬e)==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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue