feat: render bounded effect tails and reset internal history on transport changes
This commit is contained in:
parent
7a01e2d2f3
commit
836cdedc6b
22 changed files with 284 additions and 18 deletions
2
Makefile
2
Makefile
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
88
src/audio/export.c
Normal 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;}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,¬e)==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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue