feat: render bounded effect tails and reset internal history on transport changes

This commit is contained in:
Johannes Findeisen 2026-11-05 23:55:56 +01:00
commit 836cdedc6b
22 changed files with 284 additions and 18 deletions

View file

@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
CORE_SOURCES := src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)

View file

@ -152,3 +152,39 @@ energy, bounded extreme feedback/diffusion and graph/direct stereo output
(including the engine's subnormal-to-zero policy). GUI tests save/reopen both
identities; callback guards and ASan/UBSan cover processing. `make builtin-preview`
also writes files 5/6 for repeatable listening; human listening remains pending.
## Phase 44 history and tails
Internal effects reset on a transport discontinuity (seek/loop wrap), stopped
backend, or newly prepared plan. Pause uses backend Stop and therefore also
resets history. Continuous playback retains tails. Reset invalidates delay
history with a valid-frame counter and clears only fixed-size filter/voice state;
it neither allocates nor clears a large delay buffer in the callback. External
plugins retain their existing history contract. MIDI discontinuities continue
to chase notes with fresh envelopes.
**File → Export WAV** renders the song plus the Preferences / transport
`tail_ms` duration (0..60000, default 3000). Output is stereo float32 WAV at the
offline sample rate. The destination must be a new absolute path. Rendering
prepares an independent snapshot, then advances in 256-frame control-thread
steps with a Cancel button; the project and playback instance stay independent.
Only a complete synced file is published, without replacing an existing file.
Cancellation/errors remove the temporary. Missing media rejects export.
RIFF's 32-bit size limit is enforced before rendering. Rendering does not apply
loop preferences, normalize, automatically detect silence or stop infinite
oscillator/feedback sources early: the configured endpoint always bounds work.
The public `xtc_export_create/step/progress/destroy` API supports incremental
rendering and cancellation by destruction. A failed step must be followed by
destruction. `xtc_project_render_length` returns the exact sample endpoint.
Set `stop_after_tail=1` in Preferences to stop ordinary playback after the song
and configured tail. Looping and recording disable this automatic stop. Playback
stop is serviced by the GUI refresh timer and normal stop fade, so it can exceed
the endpoint by a refresh interval plus fade; export has the exact sample length.
Uncheck the setting to retain indefinite playback. Stop always remains available.
Tests compare stereo export against direct rendering in 31-frame blocks, verify
nonzero tails, exact length, cancellation, existing-file retention, tail bounds,
history reset and callback-safe reset. Preview generation and manual listening
procedures remain available; no human listening result is claimed.

View file

@ -66,3 +66,8 @@ Validation covers settings roundtrips, malformed/duplicate/overflow/NUL input,
private permissions, save failure, GUI Apply/Close/reload and plugin listing,
plus JACK callback wrap positions and existing GUI/core regressions. Tests use
temporary configuration paths and do not write the user's configuration.
Phase 44 adds `[transport] tail_ms=3000` (0..60000) and
`stop_after_tail=0` (0/1). Export always includes the configured tail; optional
playback stopping uses the GUI timer and is disabled during looping/recording.
See BUILTIN_DEVICES.md for endpoint, cancellation and reset semantics.

View file

@ -1239,15 +1239,19 @@ documented; human listening is unverified. See BUILTIN_DEVICES.md.
Make discontinuities and effect tails explicit in playback and export.
### Tasks and acceptance criteria
- [ ] Define retained/reset effect history for seek, stop, loop wrap and project/plan replacement.
- [ ] Implement the chosen history policy without callback allocation or unsafe state sharing.
- [ ] Add bounded automatic or user-configurable tail rendering after the arrangement/export endpoint.
- [ ] Expose tail limits and cancellation, including silent input and non-decaying/high-feedback cases.
- [ ] Test seek/loop reproducibility and complete delay/reverb tails across processing block sizes.
- [x] Define retained/reset effect history for seek, stop, loop wrap and project/plan replacement.
- [x] Implement the chosen history policy without callback allocation or unsafe state sharing.
- [x] Add bounded automatic or user-configurable tail rendering after the arrangement/export endpoint.
- [x] Expose tail limits and cancellation, including silent input and non-decaying/high-feedback cases.
- [x] Test seek/loop reproducibility and complete delay/reverb tails across processing block sizes.
### Implementation status
Planned. Seeks currently retain effect history; tails need continued rendering
and do not automatically extend playback/export.
Implemented: constant-cost internal history invalidation on discontinuity/stop,
plus incremental stereo WAV export with 0..60-second tails, progress/cancel and
no-clobber publication. Preferences optionally stop playback after the tail
(with GUI-timer/fade precision); loops/recording retain their existing behavior.
Core, reliability, export/direct partition, GUI and sanitizer checks pass.
External-plugin history remains retained. See BUILTIN_DEVICES.md for limits.
---

View file

@ -8,6 +8,16 @@ extern "C" {
#endif
typedef struct xtc_audio_plan xtc_audio_plan;
typedef struct xtc_export xtc_export;
/* Control thread only: bounded 256-frame incremental stereo float32 WAV export.
* tail_ms 0..60000, rate 8000..192000. Destination must be absolute and absent.
* No destination is published on error/cancel; destroy cancels/removes temporary.
* Snapshot preparation may execute trusted plugins. Missing media rejects export. */
xtc_result xtc_project_render_length(const xtc_project *project,unsigned int rate,unsigned int tail_ms,uint64_t *out_frames);
xtc_result xtc_export_create(const xtc_project *project,const char *path,unsigned int rate,unsigned int tail_ms,xtc_export **out);
xtc_result xtc_export_step(xtc_export *job,int *done);
xtc_result xtc_export_progress(const xtc_export *job,uint64_t *position,uint64_t *total);
void xtc_export_destroy(xtc_export *job);
typedef struct xtc_recorder xtc_recorder;
/* Control-thread metadata edits; audio/silent clips only. No source-file writes.

View file

@ -30,6 +30,7 @@ typedef struct {
typedef struct xtc_plan xtc_plan;
/* Single owner: set absolute automation/native-event position, no allocation. */
void xtc_plan_reset_history(xtc_plan *plan); /* Single owner; internal effects only. */
xtc_result xtc_plan_position(xtc_plan *plan, uint64_t position);
typedef enum {

View file

@ -69,6 +69,9 @@ xtc_result xtc_plugin_parameters(const xtc_plugin_spec *spec, double rate, xtc_p
/* Metadata-only validation: never opens libraries. */
xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *spec);
xtc_result xtc_plugin_create(const xtc_plugin_spec *spec, double rate, size_t frames, xtc_plugin **out);
/* Single audio owner or quiescent control owner. Internal state resets in bounded
* constant work; external-plugin history remains retained. No allocation/I/O. */
void xtc_plugin_reset_history(xtc_plugin *plugin);
size_t xtc_plugin_sample_frames(const xtc_plugin *plugin);
size_t xtc_plugin_asset_issues(const xtc_plugin *plugin);
void xtc_plugin_destroy(xtc_plugin *plugin); /* control, after quiescence */

View file

@ -9,6 +9,7 @@ extern "C" {
typedef struct xtc_realtime xtc_realtime;
/* Single audio owner, or stopped/joined control owner: position current/future plans. */
void xtc_realtime_reset_history(xtc_realtime *engine); /* Quiescent control only. */
xtc_result xtc_realtime_position(xtc_realtime *engine, uint64_t position);
typedef struct {

View file

@ -14,7 +14,7 @@ typedef struct {
int64_t sequencer_snap, piano_snap, sequencer_zoom;
int64_t clip_ticks, note_ticks, note_velocity, note_channel, filter_channel, filter_low, filter_high;
int64_t tempo, volume_cc, pan_cc, sustain_cc;
int64_t loop_start, loop_end, loop_enabled, follow_playhead;
int64_t loop_start, loop_end, loop_enabled, follow_playhead, tail_ms, stop_after_tail;
} xtc_settings;
typedef struct {
const char *section, *key, *label;

88
src/audio/export.c Normal file
View file

@ -0,0 +1,88 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_audio.h"
#include "xtc/xtc_midi.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
struct xtc_export {
xtc_audio_plan *audio;xtc_midi_plan *midi;xtc_plan *graph;
FILE *file;char *temporary,*destination;
uint64_t position,total;unsigned int rate;int complete;
float buffers[6][256];
};
static void put32(unsigned char *p,uint32_t v)
{unsigned int i;for(i=0;i<4;++i){p[i]=(unsigned char)(v&255);v>>=8;}}
static int header(FILE *file,unsigned int rate,uint32_t frames)
{
unsigned char h[44]={0};memcpy(h,"RIFF",4);put32(h+4,36+frames*8);memcpy(h+8,"WAVEfmt ",8);
put32(h+16,16);h[20]=3;h[22]=2;put32(h+24,rate);put32(h+28,rate*8);h[32]=8;h[34]=32;
memcpy(h+36,"data",4);put32(h+40,frames*8);return fwrite(h,1,sizeof(h),file)==sizeof(h);
}
void xtc_export_destroy(xtc_export *job)
{
if(!job)return;
if(job->file)fclose(job->file);
if(job->temporary)unlink(job->temporary);
free(job->temporary);free(job->destination);xtc_audio_plan_destroy(job->audio);
xtc_midi_plan_destroy(job->midi);xtc_plan_destroy(job->graph);free(job);
}
xtc_result xtc_project_render_length(const xtc_project *project,unsigned int rate,unsigned int tail_ms,uint64_t *out)
{
size_t tracks,clips,i;uint64_t end=0,samples;xtc_result r;
if(!out || rate<8000 || rate>192000 || tail_ms>60000)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_get_counts(project,&tracks,&clips);if(r!=XTC_OK)return r;
for(i=0;i<clips;++i){xtc_clip_info clip;r=xtc_project_get_clip(project,i,&clip);if(r!=XTC_OK)return r;
if(clip.start_tick+clip.length_tick>end)end=clip.start_tick+clip.length_tick;}
r=xtc_project_tick_to_sample(project,end,rate,&samples);if(r!=XTC_OK)return r;
*out=samples+((uint64_t)rate*tail_ms+999)/1000;return XTC_OK;
}
xtc_result xtc_export_create(const xtc_project *project,const char *path,unsigned int rate,unsigned int tail_ms,xtc_export **out)
{
xtc_export *job;xtc_result r;int fd;uint64_t total;
if(!out)return XTC_ERR_INVALID_ARGUMENT;
*out=NULL;
if(!path || path[0]!='/' || strlen(path)>4000)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_render_length(project,rate,tail_ms,&total);if(r!=XTC_OK)return r;
if(total>(UINT32_MAX-36U)/8U)return XTC_ERR_CAPACITY;
job=calloc(1,sizeof(*job));if(!job)return XTC_ERR_OUT_OF_MEMORY;
job->total=total;job->rate=rate;
r=xtc_audio_plan_create(project,rate,&job->audio);
if(r==XTC_OK)r=xtc_midi_plan_create(project,rate,&job->midi);
if(r==XTC_OK && (xtc_audio_plan_issues(job->audio,NULL) || xtc_midi_plan_asset_issues(job->midi)))r=XTC_ERR_NOT_FOUND;
if(r==XTC_OK)r=xtc_graph_compile(xtc_project_graph(project),rate,256,&job->graph);
if(r!=XTC_OK){xtc_export_destroy(job);return r;}
job->destination=strdup(path);job->temporary=malloc(strlen(path)+16);
if(!job->destination || !job->temporary){xtc_export_destroy(job);return XTC_ERR_OUT_OF_MEMORY;}
snprintf(job->temporary,strlen(path)+16,"%s.tmp.XXXXXX",path);
fd=mkstemp(job->temporary);if(fd<0){xtc_export_destroy(job);return XTC_ERR_IO;}
job->file=fdopen(fd,"wb");if(!job->file){close(fd);xtc_export_destroy(job);return XTC_ERR_IO;}
if(!header(job->file,rate,(uint32_t)total)){xtc_export_destroy(job);return XTC_ERR_IO;}
*out=job;return XTC_OK;
}
xtc_result xtc_export_step(xtc_export *job,int *done)
{
size_t n,i;xtc_result r;unsigned char bytes[256*8];
if(!job || !done)return XTC_ERR_INVALID_ARGUMENT;
if(job->complete){*done=1;return XTC_OK;}
if(!job->file)return XTC_ERR_INVALID_STATE;
n=job->total-job->position<256 ? (size_t)(job->total-job->position) : 256;
r=xtc_audio_plan_render_stereo(job->audio,job->position,job->buffers[0],job->buffers[1],n);
if(r==XTC_OK)r=xtc_midi_plan_render_stereo(job->midi,job->position,NULL,0,job->buffers[2],job->buffers[3],n);
if(r==XTC_OK)r=xtc_plan_process_stereo(job->graph,NULL,job->buffers[0],job->buffers[1],job->buffers[2],job->buffers[3],job->buffers[4],job->buffers[5],n);
if(r!=XTC_OK)return r;
for(i=0;i<n;++i){uint32_t a,b;memcpy(&a,job->buffers[4]+i,4);memcpy(&b,job->buffers[5]+i,4);put32(bytes+i*8,a);put32(bytes+i*8+4,b);}
if(n && fwrite(bytes,8,n,job->file)!=n)return XTC_ERR_IO;
job->position+=n;*done=0;
if(job->position==job->total) {
if(fflush(job->file) || fsync(fileno(job->file)))return XTC_ERR_IO;
if(fclose(job->file)){job->file=NULL;return XTC_ERR_IO;}job->file=NULL;
/* No clobber: destination appears only after a complete, synced render. */
if(link(job->temporary,job->destination))return errno==EEXIST ? XTC_ERR_INVALID_STATE : XTC_ERR_IO;
unlink(job->temporary);job->complete=1;*done=1;
}
return XTC_OK;
}
xtc_result xtc_export_progress(const xtc_export *job,uint64_t *position,uint64_t *total)
{if(!job || !position || !total)return XTC_ERR_INVALID_ARGUMENT;*position=job->position;*total=job->total;return XTC_OK;}

View file

@ -447,6 +447,7 @@ xtc_result xtc_backend_stop(xtc_backend *backend)
if (!backend->ready)
return XTC_ERR_INVALID_STATE;
if (!backend->running) {
xtc_realtime_reset_history(backend->engine);
xtc_midi_plan_reset(backend->midi);
return XTC_OK;
}
@ -456,6 +457,7 @@ xtc_result xtc_backend_stop(xtc_backend *backend)
__atomic_store_n(&backend->stop_requested, 0U, __ATOMIC_RELAXED);
__atomic_store_n(&backend->stop_done, 0U, __ATOMIC_RELAXED);
xtc_midi_plan_reset(backend->midi);
xtc_realtime_reset_history(backend->engine);
xtc_realtime_collect(backend->engine);
return XTC_OK;
}
@ -475,6 +477,7 @@ xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample)
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE))
return XTC_ERR_INVALID_STATE;
xtc_midi_plan_reset(backend->midi);
xtc_realtime_reset_history(backend->engine);
(void)xtc_realtime_position(backend->engine,sample);
__atomic_store_n(&backend->sample_position, sample, __ATOMIC_RELAXED);
return XTC_OK;

View file

@ -380,5 +380,7 @@ xtc_result xtc_plan_process_sources(xtc_plan *p, const float *in, const float *a
return xtc_plan_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames);
}
void xtc_plan_reset_history(xtc_plan *p)
{size_t i;if(p)for(i=0;i<p->node_count*2;++i)xtc_plugin_reset_history(p->plugins[i]);}
xtc_result xtc_plan_position(xtc_plan *p, uint64_t position)
{ if (!p || position > UINT64_MAX-65536U) return XTC_ERR_INVALID_ARGUMENT; p->position = position; return XTC_OK; }
{ if (!p || position > UINT64_MAX-65536U) return XTC_ERR_INVALID_ARGUMENT; if(p->position!=position)xtc_plan_reset_history(p); p->position = position; return XTC_OK; }

View file

@ -240,3 +240,10 @@ xtc_result xtc_realtime_process_sources(xtc_realtime *p, const float *in, const
xtc_result xtc_realtime_position(xtc_realtime *p, uint64_t position)
{ if (!p || position > UINT64_MAX-65536U) return XTC_ERR_INVALID_ARGUMENT; p->position = position; return XTC_OK; }
void xtc_realtime_reset_history(xtc_realtime *p)
{
if(!p)return;
xtc_plan_reset_history(p->active);
xtc_plan_reset_history(__atomic_load_n(&p->pending,__ATOMIC_ACQUIRE));
}

View file

@ -111,6 +111,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/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/Import WAV", FL_CTRL + 'i', audio_file_callback, this);
menu->add("File/Relink selected audio", 0, audio_file_callback, this);
@ -1053,6 +1054,12 @@ void MainWindow::timer_callback(void *context)
if (r != XTC_OK || status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK)
window->stop_engine();
}
if(window->settings_.stop_after_tail && !window->settings_.loop_enabled && !window->recorder_) {
xtc_backend_status status{};uint64_t end=0;
if(xtc_backend_get_status(window->backend_,&status)==XTC_OK && status.running &&
xtc_project_render_length(window->project_.get(),status.sample_rate,static_cast<unsigned int>(window->settings_.tail_ms),&end)==XTC_OK && status.sample_position>=end)
window->stop_engine();
}
window->refresh_status();
window->follow_playhead();
if (!window->closing_)
@ -1861,6 +1868,30 @@ 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;
}
void MainWindow::export_audio()
{
if(recorder_){report_error("Stop recording before export",XTC_ERR_INVALID_STATE);return;}
const char *answer=fl_input("Export stereo WAV to a new absolute path (tail configured in Preferences)","");
if(!answer)return;
std::string path=answer;xtc_export *job=nullptr;
xtc_result r=xtc_export_create(project_.get(),path.c_str(),static_cast<unsigned int>(settings_.offline_rate),static_cast<unsigned int>(settings_.tail_ms),&job);
if(r==XTC_OK) {
Fl_Double_Window dialog(460,130,"Export WAV");dialog.set_modal();dialog.begin();
Fl_Box progress(12,12,436,45,"Preparing export");Fl_Button cancel(330,80,110,30,"Cancel export");
cancel.callback([](Fl_Widget *,void *c){static_cast<Fl_Window *>(c)->hide();},&dialog);
dialog.callback([](Fl_Widget *,void *c){static_cast<Fl_Window *>(c)->hide();},&dialog);dialog.end();xtc_ui::style(&dialog);dialog.show();
int done=0;
while(dialog.shown() && !done && r==XTC_OK) {
r=xtc_export_step(job,&done);uint64_t position=0,total=0;xtc_export_progress(job,&position,&total);
std::string text=std::to_string(position)+" / "+std::to_string(total)+" frames";progress.copy_label(text.c_str());Fl::check();
}
if(r==XTC_OK && !done)set_label(status_,"Export cancelled; destination retained");
else if(r==XTC_OK)set_label(status_,"Export complete");
dialog.hide();
}
xtc_export_destroy(job);
if(r!=XTC_OK)report_error("Export failed; destination must be new and media available",r);
}
xtc_result MainWindow::load_sampler(uint64_t track,const char *path)
{
if(!path || path[0]!='/' || std::strlen(path)>=sizeof(xtc_plugin_spec{}.sample_path))return XTC_ERR_INVALID_ARGUMENT;

View file

@ -49,6 +49,7 @@ class MainWindow : public Fl_Double_Window {
Fl_Widget *velocity_lane() const { return velocity_lane_; }
/* Control-thread plugin commands, transactional model adoption. */
xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index);
void export_audio();
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

@ -29,7 +29,7 @@ struct xtc_builtin {
xtc_plugin_spec spec;
double rate;
float attack, release;
size_t max_frames, length, cursor;
size_t max_frames, length, cursor,history_frames;
float *line, damp[4];
size_t comb[4];
voice voices[32];
@ -107,6 +107,14 @@ xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t fram
}
size_t xtc_builtin_sample_frames(const xtc_builtin *p) {return p ? p->sample.frames : 0;}
size_t xtc_builtin_asset_issues(const xtc_builtin *p) {return p ? p->sample_issue : 0;}
/* Lazy invalidation: reset cost is independent of delay-buffer capacity. */
void xtc_builtin_reset(xtc_builtin *p)
{
if(!p)return;
p->cursor=p->history_frames=0;p->delay_started=0;p->delay_remaining=0;
memset(p->damp,0,sizeof(p->damp));memset(p->previous,0,sizeof(p->previous));
memset(p->voices,0,sizeof(p->voices));memset(p->sustain,0,sizeof(p->sustain));memset(p->bend,0,sizeof(p->bend));
}
void xtc_builtin_destroy(xtc_builtin *p) { if(p) {free(p->sample.samples);free(p->line);free(p);} }
static float parameter(const xtc_builtin *p, unsigned int port, uint64_t position)
{
@ -131,17 +139,17 @@ xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size
float a=parameter(p,0,position+i), b=parameter(p,1,position+i), mix=parameter(p,2,position+i);
if(p->spec.index==2) {
size_t delay=(size_t)(a*p->rate/1000); if(!delay) delay=1;
wet=p->line[(p->cursor+p->length-delay)%p->length];
wet=p->history_frames>=delay ? p->line[(p->cursor+p->length-delay)%p->length] : 0;
p->line[p->cursor]=x+wet*b;
} else {
for(j=0;j<4;++j) {
float *line=p->line+j*p->length;
float y=line[(p->cursor+p->length-p->comb[j])%p->length];
float y=p->history_frames>=p->comb[j] ? line[(p->cursor+p->length-p->comb[j])%p->length] : 0;
p->damp[j]=(1-b)*y+b*p->damp[j];
line[p->cursor]=x+a*p->damp[j]; wet+=y*.25f;
}
}
out[i]=(1-mix)*x+mix*wet; p->cursor=(p->cursor+1)%p->length;
out[i]=(1-mix)*x+mix*wet; if(p->history_frames<p->length)++p->history_frames; p->cursor=(p->cursor+1)%p->length;
}
return XTC_OK;
}
@ -170,7 +178,8 @@ xtc_result xtc_builtin_stereo(xtc_builtin *p,const float *left,const float *righ
for(head=0;head<2;++head) {
size_t delay=(size_t)delays[head];
size_t n=(p->cursor+p->length-delay)%p->length,m=(n+p->length-1)%p->length;
values[head]=line[n]+(line[m]-line[n])*(float)(delays[head]-(double)delay);
{float near=p->history_frames>=delay ? line[n] : 0,far=p->history_frames>delay ? line[m] : 0;
values[head]=near+(far-near)*(float)(delays[head]-(double)delay);}
}
{float blend=p->delay_remaining ? (float)(p->delay_transition-p->delay_remaining+1)/(float)p->delay_transition : 0;
wet[side]=values[0]+(values[1]-values[0])*blend;}
@ -183,7 +192,7 @@ xtc_result xtc_builtin_stereo(xtc_builtin *p,const float *left,const float *righ
for(side=0;side<2;++side)for(j=0;j<4;++j) {
float *line=p->line+(side*4+j)*p->length;
size_t delay=(size_t)((double)p->comb[j]*(.5+room*1.5)*(side ? 1.071 : 1));
float y=line[(p->cursor+p->length-delay)%p->length];
float y=p->history_frames>=delay ? line[(p->cursor+p->length-delay)%p->length] : 0;
next[side][j]=(1-b)*y+b*p->previous[side][j];
line[p->cursor]=input[side]+a*((1-.5f*diffusion)*next[side][j]+.5f*diffusion*p->previous[1-side][j]);
wet[side]+=y*.25f;
@ -192,6 +201,7 @@ xtc_result xtc_builtin_stereo(xtc_builtin *p,const float *left,const float *righ
}
if(out_right) {out[i]=(1-mix)*input[0]+mix*wet[0];out_right[i]=(1-mix)*input[1]+mix*wet[1];}
else out[i]=(1-mix)*input[0]+mix*(wet[0]+wet[1])*.5f;
if(p->history_frames<p->length)++p->history_frames;
p->cursor=(p->cursor+1)%p->length;
}
return XTC_OK;

View file

@ -8,6 +8,7 @@ xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *ou
xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_builtin **out);
size_t xtc_builtin_sample_frames(const xtc_builtin *p);
size_t xtc_builtin_asset_issues(const xtc_builtin *p);
void xtc_builtin_reset(xtc_builtin *p);
void xtc_builtin_destroy(xtc_builtin *p);
xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size_t frames, uint64_t position);
xtc_result xtc_builtin_stereo(xtc_builtin *p,const float *left,const float *right,float *out,float *out_right,size_t frames,uint64_t position);

View file

@ -53,6 +53,7 @@ xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double rate, xtc_plugin
snprintf(out->copyright,sizeof(out->copyright),"See plugin license");
return XTC_OK;
}
void xtc_plugin_reset_history(xtc_plugin *p) {if(p){xtc_builtin_reset(p->builtin);p->next=0;}}
size_t xtc_plugin_sample_frames(const xtc_plugin *p) {return p ? xtc_builtin_sample_frames(p->builtin) : 0;}
size_t xtc_plugin_asset_issues(const xtc_plugin *p) {return p ? xtc_builtin_asset_issues(p->builtin) : 0;}
void xtc_plugin_destroy(xtc_plugin *p)

View file

@ -38,6 +38,8 @@ static const xtc_setting_info schema[] = {
NUM("project_defaults",sustain_cc,"New project sustain CC (128 disables)",0,128),
NUM("transport",loop_start,"Loop start, ticks",0,38399999), NUM("transport",loop_end,"Loop end, ticks",1,38400000),
NUM("transport",loop_enabled,"Loop enabled: 0 or 1",0,1),
NUM("transport",tail_ms,"Playback/export tail, ms",0,60000),
NUM("transport",stop_after_tail,"Stop playback after song and tail: 0 or 1",0,1),
NUM("transport",follow_playhead,"Follow playhead: 0 or 1",0,1)
};
#define COUNT (sizeof(schema)/sizeof(schema[0]))
@ -54,7 +56,7 @@ void xtc_settings_defaults(xtc_settings *s)
s->width=1280; s->height=800; s->piano_width=900; s->piano_height=620; s->refresh_ms=100;
s->sequencer_snap=s->piano_snap=240; s->sequencer_zoom=48; s->clip_ticks=3840; s->note_ticks=960;
s->note_velocity=100; s->filter_channel=-1; s->filter_high=127; s->tempo=120000;
s->volume_cc=7; s->pan_cc=10; s->sustain_cc=64; s->loop_end=3840;
s->volume_cc=7; s->pan_cc=10; s->sustain_cc=64; s->loop_end=3840;s->tail_ms=3000;
}
static int snap(int64_t v) { return v==1 || v==120 || v==160 || v==240 || v==320 || v==480 || v==960 || v==1920 || v==3840; }
xtc_result xtc_settings_validate(const xtc_settings *s)

View file

@ -1046,6 +1046,11 @@ int main(int argc, char **argv)
CHECK(window.settings().loop_enabled==1 && window.settings().loop_start==960);
CHECK(window.set_loop(4000,2000,true)==XTC_ERR_INVALID_ARGUMENT);
settings=window.settings();CHECK(window.apply_preferences(settings)==XTC_OK);
settings=window.settings();settings.loop_enabled=0;settings.stop_after_tail=1;settings.tail_ms=0;
CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(window.create_track(true)==XTC_OK);press(find(&window,"Play"));
engine_status.sample_position=1000000;pump();CHECK(!handle.running);
settings.loop_enabled=1;settings.stop_after_tail=0;CHECK(window.apply_preferences(settings)==XTC_OK);
settings.note_velocity=128;CHECK(window.apply_preferences(settings)==XTC_ERR_INVALID_ARGUMENT);
PreferencesDialog dialog(window.settings(),path,[&](const xtc_settings &v){return window.apply_preferences(v);});
auto *input=dynamic_cast<Fl_Input *>(find(&dialog,"New note velocity"));CHECK(input);

View file

@ -170,6 +170,7 @@ static void builtin_failures(void)
assert(xtc_plugin_process_midi(plugin,&event,1,out,256)==XTC_OK);
} else if(id>=5) {float right[256];assert(xtc_plugin_process_stereo_at(plugin,in,in,out,right,256,0)==XTC_OK);}
else assert(xtc_plugin_process(plugin,in,out,256)==XTC_OK);
xtc_plugin_reset_history(plugin);
processing=0;xtc_plugin_destroy(plugin);succeeded=1;break;
}
assert(succeeded);

View file

@ -266,7 +266,61 @@ static void stereo_effect(unsigned int id)
}xtc_plugin_destroy(a);
}
}
static void histories(void)
{
unsigned int ids[]={2,3,5,6},j;size_t k,i;
for(j=0;j<4;++j) {
xtc_plugin_spec s;xtc_plugin *p=NULL;float in[256]={0},out[256];
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,ids[j],48000,&s)==XTC_OK);
assert(xtc_plugin_create(&s,48000,256,&p)==XTC_OK);
in[0]=1;assert(xtc_plugin_process(p,in,out,256)==XTC_OK);in[0]=0;
for(k=0;k<200;++k)assert(xtc_plugin_process(p,in,out,256)==XTC_OK);
xtc_plugin_reset_history(p);
for(k=0;k<600;++k) {
assert(xtc_plugin_process(p,in,out,256)==XTC_OK);
for(i=0;i<256;++i)assert(out[i]==0);
}
xtc_plugin_destroy(p);
}
}
static void exporting(void)
{
xtc_project *project=NULL;xtc_plugin_spec spec;xtc_graph *graph=NULL;xtc_plan *plan=NULL;
xtc_midi_plan *midi=NULL;xtc_export *job=NULL;xtc_wave wave={0};
uint64_t track,clip,note,total,position;size_t source,node,output,i;int done=0;
char path[]="/tmp/xtc-export-XXXXXX";int fd=mkstemp(path);assert(fd>=0);close(fd);unlink(path);
assert(xtc_project_create(&project)==XTC_OK && xtc_project_add_track(project,"Export",&track)==XTC_OK);
assert(xtc_project_add_midi_clip(project,track,0,120,&clip)==XTC_OK);
assert(xtc_project_add_note(project,clip,0,100,60,100,0,&note)==XTC_OK);
assert(xtc_graph_create(3,&graph)==XTC_OK && xtc_graph_add_instrument(graph,&source)==XTC_OK);
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,5,48000,&spec)==XTC_OK);spec.values[0]=100;
assert(xtc_graph_add_plugin(graph,&spec,&node)==XTC_OK && xtc_graph_add_output(graph,&output)==XTC_OK);
assert(xtc_graph_connect(graph,source,node)==XTC_OK && xtc_graph_connect(graph,node,output)==XTC_OK);
assert(xtc_project_set_graph(project,graph)==XTC_OK);
assert(xtc_export_create(project,path,48000,500,&job)==XTC_OK);
assert(xtc_export_step(job,&done)==XTC_OK && !done && access(path,F_OK)!=0);
xtc_export_destroy(job);job=NULL;assert(access(path,F_OK)!=0);
assert(xtc_export_create(project,path,48000,500,&job)==XTC_OK);
while(!done)assert(xtc_export_step(job,&done)==XTC_OK);
assert(xtc_export_progress(job,&position,&total)==XTC_OK && position==total && total==27000);
xtc_export_destroy(job);job=NULL;
assert(xtc_wave_load(path,&wave)==XTC_OK && wave.frames==27000);
{double energy=0;for(i=3000;i<wave.frames;++i)energy+=fabs(wave.samples[i]);assert(energy>1); }
assert(xtc_graph_compile(graph,48000,31,&plan)==XTC_OK && xtc_midi_plan_create(project,48000,&midi)==XTC_OK);
for(position=0;position<total;position+=31) {
float l[31],r[31],ml[31],mr[31];size_t n=total-position<31 ? (size_t)(total-position) : 31;
assert(xtc_midi_plan_render_stereo(midi,position,NULL,0,ml,mr,n)==XTC_OK);
assert(xtc_plan_process_stereo(plan,NULL,NULL,NULL,ml,mr,l,r,n)==XTC_OK);
for(i=0;i<n;++i)assert(fabsf(wave.samples[position+i]-(l[i]+r[i])*.5f)<1e-6f);
}
/* Existing destination survives a complete attempted export. */
done=0;assert(xtc_export_create(project,path,48000,0,&job)==XTC_OK);
while(!done){xtc_result result=xtc_export_step(job,&done);if(result!=XTC_OK){assert(result==XTC_ERR_INVALID_STATE);break;}}
assert(xtc_export_step(job,&done)==XTC_ERR_INVALID_STATE);xtc_export_destroy(job);
assert(xtc_project_render_length(project,48000,60001,&total)==XTC_ERR_INVALID_ARGUMENT);
free(wave.samples);xtc_plan_destroy(plan);xtc_midi_plan_destroy(midi);xtc_graph_destroy(graph);xtc_project_destroy(project);unlink(path);
}
int main(void) {
stereo_effect(5);stereo_effect(6);sampler();editable();transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
histories();exporting();stereo_effect(5);stereo_effect(6);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;
}