feat: add a bounded WAV sampler with looping, relinking and saved presets

This commit is contained in:
Johannes Findeisen 2026-11-05 23:50:14 +01:00
commit 35bf6a00a1
24 changed files with 213 additions and 32 deletions

View file

@ -748,6 +748,14 @@ int main(int argc, char **argv)
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-internal-gui.xtc")==XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==instrument);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec)==XTC_OK && spec.index==effect && spec.point_count==1);
char sample[1024];CHECK(getcwd(sample,sizeof(sample))!=nullptr);std::strcat(sample,"/examples/projects/original-pluck.wav");
CHECK(window.load_sampler(track,sample)==XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==4 && !std::strcmp(spec.sample_path,sample));
CHECK(window.save_project("/tmp/xtc-sampler-gui.xtc")==XTC_OK);
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-sampler-gui.xtc")==XTC_OK);
CHECK(window.load_sampler(track,"/absent/sample.wav")==XTC_OK);
CHECK(window.load_sampler(track,sample)==XTC_OK); // Relink is one prepared history edit.
CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK);unlink("/tmp/xtc-sampler-gui.xtc");
size_t added=SIZE_MAX;CHECK(window.add_plugin(XTC_BUILTIN_PATH,effect,&added)==XTC_OK);
CHECK(added!=id);CHECK(window.undo()==XTC_OK);
const xtc_project *before=sequence->project();reserve_result=XTC_ERR_OUT_OF_MEMORY;

View file

@ -56,7 +56,7 @@ def main():
run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')])
assert (docs / 'WORKSPACE.md').is_file()
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True)
assert 'Project formats: 1..14' in diagnose and 'JACK compiled:' in diagnose
assert 'Project formats: 1..15' in diagnose and 'JACK compiled:' in diagnose
# Replacing a file symlink must not overwrite an unrelated target.
protected = stage / 'unrelated-target'
protected.write_text('keep unrelated target\n')

View file

@ -84,7 +84,7 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
project.write_text(malformed)
check("--validate", project, 1)
for text, messages in [
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..14")),
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..15")),
("XTC_PROJECT 10\n", ("line 2", "APP")),
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),

View file

@ -9,6 +9,7 @@
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *__real_malloc(size_t size);
void *__real_calloc(size_t count, size_t size);
@ -156,15 +157,16 @@ static void history_reservations(void)
static void builtin_failures(void)
{
unsigned int id;
for(id=0;id<4;++id) {
for(id=0;id<5;++id) {
int failure, succeeded=0; xtc_plugin_spec spec;
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,id,48000,&spec)==XTC_OK);
if(id==4) {assert(getcwd(spec.sample_path,sizeof(spec.sample_path)));strcat(spec.sample_path,"/examples/projects/original-pluck.wav");}
for(failure=0;failure<16;++failure) {
xtc_plugin *plugin=NULL; xtc_result result; float in[256]={0},out[256];
fail_after=failure;result=xtc_plugin_create(&spec,48000,256,&plugin);fail_after=-1;
if(result!=XTC_OK) {assert(result==XTC_ERR_OUT_OF_MEMORY && !plugin);continue;}
processing=1;
if(id<2) {xtc_midi_event event={0,XTC_MIDI_NOTE_ON,0,60,100};
if(spec.instrument) {xtc_midi_event event={0,XTC_MIDI_NOTE_ON,0,60,100};
assert(xtc_plugin_process_midi(plugin,&event,1,out,256)==XTC_OK);
} else assert(xtc_plugin_process(plugin,in,out,256)==XTC_OK);
processing=0;xtc_plugin_destroy(plugin);succeeded=1;break;

View file

@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "../../src/audio/wave_internal.h"
#include <unistd.h>
#include <stdlib.h>
#include "xtc/xtc_midi.h"
@ -165,8 +166,8 @@ static void editable(void)
assert(xtc_plugin_preset_save(&legacy,path)==XTC_OK);
{FILE *f=fopen(path,"r");char lines[32768],line[2048];size_t used=0;assert(f);
while(fgets(line,sizeof(line),f)) {
if(!strncmp(line,"INSTRUMENT_CONTROLS",19) || !strncmp(line,"INSTRUMENT_AUTOMATION",21))continue;
if(!strncmp(line,"XTC_PROJECT 14",14))strcpy(line,"XTC_PROJECT 13\n");
if(!strncmp(line,"SAMPLE ",7) || !strncmp(line,"INSTRUMENT_CONTROLS",19) || !strncmp(line,"INSTRUMENT_AUTOMATION",21))continue;
if(!strncmp(line,"XTC_PROJECT 15",14))strcpy(line,"XTC_PROJECT 13\n");
assert(used+strlen(line)<sizeof(lines));strcpy(lines+used,line);used+=strlen(line);
}fclose(f);f=fopen(path,"w");assert(f && fwrite(lines,1,used,f)==used);fclose(f);}
assert(xtc_plugin_preset_load(path,&t)==XTC_OK && !t.control_count && t.index==0);
@ -187,7 +188,40 @@ static void editable(void)
assert(xtc_midi_decode(bend,3,7,&e)==XTC_OK && e.type==XTC_MIDI_BEND && e.channel==3 && e.velocity==64);}
unlink(path);
}
static void sampler(void)
{
xtc_plugin_spec s,t;xtc_plugin *a=NULL,*b=NULL;float x[256],y[256],data[]={0,.25f,.5f,.75f};
xtc_midi_event e={0,XTC_MIDI_NOTE_ON,0,60,127};size_t i;double envelope=0;
char path[]="/tmp/xtc-sample-XXXXXX",preset[]="/tmp/xtc-sample-preset-XXXXXX";
int fd=mkstemp(path),pd=mkstemp(preset);FILE *file;assert(fd>=0 && pd>=0);close(fd);close(pd);
file=fopen(path,"wb");assert(file && xtc_wave_header(file,24000,4) && xtc_wave_write(file,data,4));fclose(file);
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,4,48000,&s)==XTC_OK && s.instrument);
strcpy(s.sample_path,path);s.values[2]=.1f;
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && !xtc_plugin_asset_issues(a) && xtc_plugin_sample_frames(a)==4);
assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK);
for(i=0;i<8;++i) {
double expected=i<6 ? (double)i*.125 : .75;
envelope+=(1-envelope)*(1-exp(-1/(.1*.001*48000)));
assert(fabs(x[i]-expected*envelope)<1e-5);
}
for(i=8;i<256;++i)assert(x[i]==0);
xtc_plugin_destroy(a);s.values[1]=1;
assert(xtc_plugin_preset_save(&s,preset)==XTC_OK && xtc_plugin_preset_load(preset,&t)==XTC_OK);
assert(!strcmp(t.sample_path,path) && t.values[1]==1);
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && xtc_plugin_create(&t,48000,256,&b)==XTC_OK);
assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK && xtc_plugin_process_midi(b,&e,1,y,128)==XTC_OK);
assert(xtc_plugin_process_midi(b,NULL,0,y+128,128)==XTC_OK && !memcmp(x,y,sizeof(x)));
e.type=XTC_MIDI_NOTE_OFF;assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK);
for(i=0;i<200;++i)assert(xtc_plugin_process_midi(a,NULL,0,x,256)==XTC_OK);
for(i=0;i<256;++i)assert(x[i]==0);
xtc_plugin_destroy(a);xtc_plugin_destroy(b);unlink(path);
assert(xtc_plugin_preset_load(preset,&t)==XTC_OK && !strcmp(t.sample_path,path));
assert(xtc_plugin_create(&t,48000,256,&a)==XTC_OK && xtc_plugin_asset_issues(a)==1);
e.type=XTC_MIDI_NOTE_ON;assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK);
for(i=0;i<256;++i)assert(x[i]==0);
xtc_plugin_destroy(a);unlink(preset);
}
int main(void) {
editable();transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
sampler();editable();transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
synth_partition(0);synth_partition(1);puts("Built-in devices passed.");return 0;
}

View file

@ -81,7 +81,7 @@ int main(void)
}
write_text(bad,"XTC_PROJECT 999\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 &&
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..14"));
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..15"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
!strcmp(diagnostic.record,"TEMPO"));