feat(audio): crossfade overlapping clips in one undoable edit (phase 98)
This commit is contained in:
parent
b7a4a1acd4
commit
083ac05ae6
10 changed files with 82 additions and 6 deletions
2
Makefile
2
Makefile
|
|
@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
|
|||
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
|
||||
LDFLAGS ?=
|
||||
|
||||
CORE_SOURCES := src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c 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
|
||||
CORE_SOURCES := src/project/crossfade.c src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c 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)
|
||||
|
|
|
|||
|
|
@ -71,3 +71,14 @@ are ignored. Each accepted tempo is an undoable edit and stops playback before
|
|||
preparation; tapping is unavailable while recording. New/Open resets tap history.
|
||||
The estimator uses monotonic control-thread time, with no audio-callback clock
|
||||
access or allocation. Tap timing is not a beat detector or MIDI clock sync.
|
||||
|
||||
## Audio crossfades
|
||||
|
||||
Select two partially overlapping audio clips on the same track and choose
|
||||
**Edit → Audio tools → Crossfade two selected clips**. The earlier clip fades
|
||||
out across the overlap while the later clip fades in. Gains, positions, source
|
||||
files and the opposite fades are preserved. Equal boundaries, containment,
|
||||
adjacency, cross-track selections and MIDI clips reject without partial edits.
|
||||
This is a linear tick-based crossfade using existing clip envelopes; it does
|
||||
not move clips, create overlaps or offer equal-power curves. Discrete sample
|
||||
endpoints retain the engine's existing one-sample boundary convention.
|
||||
|
|
|
|||
|
|
@ -2178,12 +2178,12 @@ Implemented: transport Tap uses a bounded C99 four-interval estimator, 20–300
|
|||
Apply matching linear boundary fades to two partially overlapping audio clips on one track, preserving source files and opposite fades. Reject ambiguous layouts transactionally and provide one undo step.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [ ] Implement the workflow with bounded validation, safe preparation and documented limits.
|
||||
- [ ] Pass meaningful core and/or GUI checks, including error and undo behavior.
|
||||
- [ ] Document results and commit this phase separately.
|
||||
- [x] Implement the workflow with bounded validation, safe preparation and documented limits.
|
||||
- [x] Pass meaningful core and/or GUI checks, including error and undo behavior.
|
||||
- [x] Document results and commit this phase separately.
|
||||
|
||||
### Implementation status
|
||||
Planned.
|
||||
Implemented: allocation-free C99 validation applies paired linear overlap fades atomically, retaining opposite fades, media, gains and positions. Ambiguous layouts reject. Exact overlapping-wave render and invalid-selection preservation checks passed, as did GUI two-clip crossfade/undo and warning-enabled builds.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ typedef struct {
|
|||
unsigned int timing_variation,velocity_variation; /* 0..960 ticks, 0..127 velocity; source overflow rejects, velocity clamps. */
|
||||
} xtc_midi_transform;
|
||||
xtc_result xtc_project_transform_midi(xtc_project *project,const uint64_t *ids,size_t count,const xtc_midi_transform *edit);
|
||||
/* Matching linear boundary fades for two partially overlapping non-MIDI clips
|
||||
* on one track. Order is inferred. Equal starts/ends, containment and adjacency
|
||||
* reject; opposite fades, media, gains and positions remain unchanged.
|
||||
* No allocation or I/O; control-thread only, errors leave both clips intact. */
|
||||
xtc_result xtc_project_crossfade(xtc_project *project,uint64_t first,uint64_t second);
|
||||
/* Session static mixer comparison. Capture owns an independent snapshot of
|
||||
* gain/pan/mute/solo for all tracks/buses. Recall requires the exact same stable
|
||||
* track ID set (order may change), and publishes atomically. Routing, automation,
|
||||
|
|
|
|||
|
|
@ -365,6 +365,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
menu->add("Edit/Select all clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->select_all_clips();},sequencer_);
|
||||
menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_);
|
||||
menu->add("Edit/Split clip at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->split_selection(); }, sequencer_);
|
||||
menu->add("Edit/Audio tools/Crossfade two selected clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->crossfade_selection();},sequencer_);
|
||||
menu->add("Edit/Audio clip gain and fades", 0, [](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->audio_properties_dialog(); }, sequencer_);
|
||||
menu->add("Edit/Rename clip or track",FL_F+2,[](Fl_Widget *, void *c) {
|
||||
const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) (void)static_cast<SequencerWidget *>(c)->rename_selection(name);
|
||||
|
|
|
|||
|
|
@ -931,3 +931,11 @@ xtc_result SequencerWidget::set_zoom(int scale)
|
|||
if(scale!=24 && scale!=48 && scale!=96 && scale!=192 && scale!=384) return XTC_ERR_INVALID_ARGUMENT;
|
||||
scale_=scale;extent();redraw();return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::crossfade_selection()
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=ids.size()==2 ? XTC_OK : XTC_ERR_INVALID_ARGUMENT;
|
||||
if(r==XTC_OK && before_edit)r=before_edit(context_);
|
||||
if(r==XTC_OK)r=xtc_project_crossfade(project_,ids[0],ids[1]);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class SequencerWidget : public Fl_Group {
|
|||
xtc_result normalize_audio(float target);
|
||||
void normalize_dialog();
|
||||
void fades_dialog();
|
||||
xtc_result crossfade_selection();
|
||||
xtc_result repeat_selection(unsigned int copies);
|
||||
void repeat_dialog();
|
||||
xtc_result copy_selection();
|
||||
|
|
|
|||
19
src/project/crossfade.c
Normal file
19
src/project/crossfade.c
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#include "project_internal.h"
|
||||
#include "xtc/xtc_editing.h"
|
||||
/* Validate both sides before either write; no allocation or source-file access. */
|
||||
xtc_result xtc_project_crossfade(xtc_project *p,uint64_t first,uint64_t second)
|
||||
{
|
||||
size_t i,a=SIZE_MAX,b=SIZE_MAX;xtc_clip_info left,right;uint64_t left_end,right_end,overlap;
|
||||
if(!p || first==second)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<p->clip_count;++i){if(p->clips[i].id==first)a=i;if(p->clips[i].id==second)b=i;}
|
||||
if(a==SIZE_MAX || b==SIZE_MAX)return XTC_ERR_NOT_FOUND;
|
||||
if(p->clips[a].start_tick>p->clips[b].start_tick){i=a;a=b;b=i;}
|
||||
left=p->clips[a];right=p->clips[b];
|
||||
if(left.midi || right.midi)return XTC_ERR_INVALID_STATE;
|
||||
left_end=left.start_tick+left.length_tick;right_end=right.start_tick+right.length_tick;
|
||||
if(left.track_id!=right.track_id || left.start_tick>=right.start_tick || right.start_tick>=left_end || left_end>=right_end)return XTC_ERR_INVALID_ARGUMENT;
|
||||
overlap=left_end-right.start_tick;
|
||||
if(overlap>left.source_length_tick || overlap>right.source_length_tick)return XTC_ERR_INVALID_ARGUMENT;
|
||||
left.fade_out_tick=overlap;right.fade_in_tick=overlap;
|
||||
p->clips[a]=left;p->clips[b]=right;return XTC_OK;
|
||||
}
|
||||
|
|
@ -266,6 +266,10 @@ int creative_workflow_tests()
|
|||
CHECK(find(&window,"Tap"));CHECK(window.tap_tempo(0)==XTC_ERR_NOT_FOUND && window.tap_tempo(.4)==XTC_OK);
|
||||
CHECK(xtc_project_tempo(s->project())==150000 && window.undo()==XTC_OK && xtc_project_tempo(s->project())==120000);
|
||||
CHECK(window.new_project()==XTC_OK && window.tap_tempo(.9)==XTC_ERR_NOT_FOUND);
|
||||
CHECK(window.create_track(false)==XTC_OK);auto audio_track=s->selected_track();s->position(0);CHECK(s->add_region()==XTC_OK);auto left_clip=s->selected_clip();
|
||||
engine_status.sample_position=48000;s->position(1920);CHECK(s->add_region()==XTC_OK);auto right_clip=s->selected_clip();CHECK(s->select(audio_track,left_clip) && s->toggle_clip(right_clip));
|
||||
CHECK(s->crossfade_selection()==XTC_OK);xtc_clip_info audio{};CHECK(xtc_project_find_clip(s->project(),left_clip,&audio)==XTC_OK && audio.fade_out_tick==1920);
|
||||
CHECK(window.undo()==XTC_OK && xtc_project_find_clip(s->project(),left_clip,&audio)==XTC_OK && !audio.fade_out_tick);
|
||||
CHECK(!panel->comparison_ready(0) && !panel->comparison_ready(1));
|
||||
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_plugin.h"
|
||||
#include "xtc/xtc_tempo.h"
|
||||
#include "xtc/xtc_editing.h"
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
|
@ -18,9 +23,31 @@ static void tap_tests(void)
|
|||
assert(xtc_tap_tempo_push(&s,10.25,&tempo)==XTC_OK && tempo==240000);
|
||||
xtc_tap_tempo_reset(&s);assert(!s.started && !s.count);
|
||||
}
|
||||
static void crossfade_tests(void)
|
||||
{
|
||||
xtc_project *p=NULL;xtc_recorder *rec=NULL;xtc_audio_plan *plan=NULL;uint64_t track,a,b;xtc_clip_info left,right,before;
|
||||
char wave[]="/tmp/xtc-crossfade-XXXXXX";int fd=mkstemp(wave);float ones[1000],output[32];assert(fd>=0);close(fd);unlink(wave);
|
||||
for(size_t i=0;i<1000;++i)ones[i]=1;
|
||||
assert(xtc_recorder_create(wave,8000,8000,&rec)==XTC_OK);
|
||||
for(size_t i=0;i<8;++i)xtc_recorder_push(rec,ones,1000);
|
||||
assert(xtc_recorder_finish(rec)==XTC_OK);xtc_recorder_destroy(rec);
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Audio",&track)==XTC_OK);
|
||||
assert(xtc_project_add_clip(p,track,"Left",0,1920,&a)==XTC_OK && xtc_project_add_clip(p,track,"Right",960,1920,&b)==XTC_OK);
|
||||
assert(xtc_project_set_clip_asset(p,a,wave)==XTC_OK && xtc_project_set_clip_asset(p,b,wave)==XTC_OK);
|
||||
assert(xtc_project_set_clip_audio(p,a,1,16,0)==XTC_OK && xtc_project_set_clip_audio(p,b,1,0,16)==XTC_OK);
|
||||
assert(xtc_project_crossfade(p,b,a)==XTC_OK && xtc_project_find_clip(p,a,&left)==XTC_OK && xtc_project_find_clip(p,b,&right)==XTC_OK);
|
||||
assert(left.fade_in_tick==16 && left.fade_out_tick==960 && right.fade_in_tick==960 && right.fade_out_tick==16);
|
||||
assert(xtc_audio_plan_create(p,8000,&plan)==XTC_OK && xtc_audio_plan_set_fade_frames(plan,0)==XTC_OK && xtc_audio_plan_render(plan,4000,output,32)==XTC_OK);
|
||||
for(size_t i=0;i<32;++i)assert(fabsf(output[i]-.99975f)<.00001f);
|
||||
xtc_audio_plan_destroy(plan);before=left;
|
||||
assert(xtc_project_crossfade(p,a,a)==XTC_ERR_INVALID_ARGUMENT && xtc_project_find_clip(p,a,&left)==XTC_OK && !memcmp(&before,&left,sizeof(left)));
|
||||
assert(xtc_project_edit_clip(p,b,track,1920,1920,0)==XTC_OK && xtc_project_crossfade(p,a,b)==XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_find_clip(p,a,&left)==XTC_OK && !memcmp(&before,&left,sizeof(left)));
|
||||
xtc_project_destroy(p);unlink(wave);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
tap_tests();
|
||||
tap_tests();crossfade_tests();
|
||||
xtc_track_chain c={0},out={0};
|
||||
c.output_gain=.8f;c.count=1;c.slots[0].enabled=1;c.slots[0].wet=.4f;
|
||||
assert(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,2,48000,&c.slots[0].plugin)==XTC_OK);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue