diff --git a/Makefile b/Makefile index 04d08ce..cc73a68 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude LDFLAGS ?= -CORE_SOURCES := 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/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) @@ -151,14 +151,14 @@ endif ifeq ($(GUI_AVAILABLE),yes) gui: $(BUILD_DIR)/xtc -$(BUILD_DIR)/test_gui: $(BUILD_DIR)/tests/integration/test_gui.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a +$(BUILD_DIR)/test_gui: $(BUILD_DIR)/tests/integration/test_gui.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/src/gui/sample_editor.o $(BUILD_DIR)/libxtc.a $(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm -ldl -Wl,--wrap=xtc_history_reserve test-gui: $(BUILD_DIR)/test_gui xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui ifeq ($(JACK_AVAILABLE),yes) -$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a +$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/src/gui/sample_editor.o $(BUILD_DIR)/libxtc.a $(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl test-gui-jack: $(BUILD_DIR)/test_gui_jack @@ -169,7 +169,7 @@ test-gui-jack: @exit 1 endif -$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a +$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/src/gui/sample_editor.o $(BUILD_DIR)/libxtc.a $(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl else test-gui-jack test-gui: @@ -264,7 +264,7 @@ analyze: .PHONY: test-workflow ifeq ($(GUI_AVAILABLE)$(JACK_AVAILABLE),yesyes) -$(BUILD_DIR)/test_workflow: $(BUILD_DIR)/tests/integration/test_workflow.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a +$(BUILD_DIR)/test_workflow: $(BUILD_DIR)/tests/integration/test_workflow.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/src/gui/sample_editor.o $(BUILD_DIR)/libxtc.a $(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl test-workflow: $(BUILD_DIR)/test_workflow @@ -556,7 +556,7 @@ test-gui-next-daw: $(BUILD_DIR)/test_gui XTC_TEST_MODERN_EDITING_ONLY=1 XTC_TEST_NEXT_DAW_ONLY=1 xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui # Optional documentation capture; requires Xvfb, ImageMagick import and xdotool. -$(BUILD_DIR)/capture_screenshots: $(BUILD_DIR)/tools/capture_screenshots.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a +$(BUILD_DIR)/capture_screenshots: $(BUILD_DIR)/tools/capture_screenshots.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/src/gui/sample_editor.o $(BUILD_DIR)/libxtc.a $(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl .PHONY: screenshots @@ -564,3 +564,13 @@ screenshots: $(BUILD_DIR)/capture_screenshots $(BUILD_DIR)/plugin-fixture.so $(B xvfb-run -a -s "-screen 0 1440x960x24" ./$(BUILD_DIR)/capture_screenshots "$(CURDIR)/screenshots" -include $(BUILD_DIR)/tools/capture_screenshots.d + +$(BUILD_DIR)/test_sample: $(BUILD_DIR)/tests/unit/test_sample.o $(BUILD_DIR)/libxtc.a + $(CC) $(LDFLAGS) -o $@ $^ -lm -ldl +.PHONY: test-sample test-gui-sample +test-sample: $(BUILD_DIR)/test_sample + ./$(BUILD_DIR)/test_sample +test: test-sample +test-gui-sample: $(BUILD_DIR)/test_gui + XTC_TEST_SAMPLE_EDITOR_ONLY=1 xvfb-run -a -s "-screen 0 1280x900x24" ./$(BUILD_DIR)/test_gui +-include $(BUILD_DIR)/src/gui/sample_editor.d $(BUILD_DIR)/tests/unit/test_sample.d diff --git a/docs/AUDIO_EDITOR.md b/docs/AUDIO_EDITOR.md new file mode 100644 index 0000000..fc0fe7c --- /dev/null +++ b/docs/AUDIO_EDITOR.md @@ -0,0 +1,15 @@ +# Audio source editor workflows + +Phases 69–79 use the existing bounded mono WAV reader. Stereo input is downmixed; +these are offline control-thread tools, not stereo restoration or time stretching. +The project format remains 16. No new dependencies are required. + +## Phase 69 — Source inspection + +Select an audio clip and use View / Sample editor or double-click it. The modal +editor loads an independent snapshot of the complete source, up to 4,194,304 frames +(about 87 seconds at 48 kHz). Drag or enter half-open native frame bounds; zoom +1..64 and View start control the visible region. Statistics show raw mono min/max, +RMS and mean/DC before clip gain, fades or mixer processing. Display clamps to +±1; sample statistics retain finite over-range values. Editing project MIDI still +opens the piano roll. This editor does not audition or stream long files. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f777ca0..effe385 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1681,6 +1681,171 @@ real FLTK widgets and isolated demonstration data. --- +## Phase 69 — Sample Editor and Source Inspection + +### Goal +Open selected audio in a dedicated modal waveform editor with frame-based selection, zoom/scroll and peak/RMS/DC statistics. Preload bounded mono samples outside callbacks; originals and project remain unchanged. + +### Tasks and acceptance criteria +- [x] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [x] Preserve source files, atomic error handling, history and callback safety. +- [x] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Implemented: bounded C99 source snapshots, per-range min/max/RMS/DC analysis and a real modal waveform editor with frame selection, zoom/scroll and audio double-click integration. Warning-enabled full core and focused GUI range/view validation pass; no callback or source file changes. + +--- + +## Phase 70 — Nondestructive Source Range Trimming + +### Goal +Apply sample-editor selection as audible clip trims with conservative frame-to-musical-tick rounding, preserved source content and one project undo step. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 71 — Sample Selection Export and Edited Copy Adoption + +### Goal +Export selected frames or save the complete edited source into a new mono float32 WAV; optionally relink the selected clip with project undo and cleanup on failed preparation. Never overwrite source/destination files. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 72 — Reversible Sample Range Reversal + +### Goal +Reverse a selected source frame range in an independent sample snapshot, with bounded local undo/redo and save-copy adoption. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 73 — Sample Polarity Inversion + +### Goal +Invert selected samples without altering frames outside the selection, with reversible local processing and no implicit clipping. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 74 — DC Offset Removal + +### Goal +Measure and subtract the selected range mean using double precision accumulation, preserve other frames and reject invalid ranges atomically. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 75 — Offline Sample Fade Curves + +### Goal +Apply linear or equal-power fade-in/out over selected source frames with exact endpoint behavior, local undo and explicit distinction from nondestructive clip fades. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 76 — Sample Range Gain and Normalization + +### Goal +Process selected sample gain or peak normalization with finite bounded parameters, whole-result validation, silence rejection and reversible edits. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 77 — Silence Boundary Detection + +### Goal +Find audible selected-range bounds using a user threshold and padding; select detected bounds for export or nondestructive clip trimming. Silence is an explicit no-result error. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 78 — Zero Crossing Selection Snapping + +### Goal +Find nearest bounded mono zero crossings around selection boundaries with deterministic ties; adjust selection without crossing or inventing samples. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + +## Phase 79 — Transient Detection and Transactional Audio Slicing + +### Goal +Detect threshold attacks with a refractory gap, inspect their source positions, and split the original clip at representable interior source transients in one undoable transactional operation. Qualify integrated rollback, persistence, rendering and GUI behavior. + +### Tasks and acceptance criteria +- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [ ] Preserve source files, atomic error handling, history and callback safety. +- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. + +### Implementation status +Planned. + +--- + ## Recommended Execution Order Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification. diff --git a/include/xtc/xtc_sample.h b/include/xtc/xtc_sample.h new file mode 100644 index 0000000..422e63b --- /dev/null +++ b/include/xtc/xtc_sample.h @@ -0,0 +1,21 @@ +#ifndef XTC_SAMPLE_H +#define XTC_SAMPLE_H +#include "xtc_audio.h" +#ifdef __cplusplus +extern "C" { +#endif +/* Control-thread sample editor snapshots. Same WAV mono downmix as playback. + * <=4194304 source frames. No file writes or realtime use. Ranges are half-open, + * nonempty native source frames; failures preserve outputs. */ +#define XTC_SAMPLE_MAX_FRAMES 4194304U +typedef struct xtc_sample xtc_sample; +typedef struct { float minimum,maximum; double mean,rms; } xtc_sample_stats; +xtc_result xtc_sample_load(const char *path,xtc_sample **out); +void xtc_sample_destroy(xtc_sample *sample); +xtc_result xtc_sample_info(const xtc_sample *sample,unsigned int *rate,size_t *frames); +xtc_result xtc_sample_read(const xtc_sample *sample,size_t frame,float *out); +xtc_result xtc_sample_analyze(const xtc_sample *sample,size_t begin,size_t end,xtc_sample_stats *out); +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/audio/sample.c b/src/audio/sample.c new file mode 100644 index 0000000..03144c6 --- /dev/null +++ b/src/audio/sample.c @@ -0,0 +1,27 @@ +#include "xtc/xtc_sample.h" +#include "wave_internal.h" +#include +#include +struct xtc_sample {xtc_wave wave;}; +static int range(const xtc_sample *s,size_t begin,size_t end){return s && beginwave.frames;} +void xtc_sample_destroy(xtc_sample *s){if(s){free(s->wave.samples);free(s);}} +xtc_result xtc_sample_load(const char *path,xtc_sample **out) +{ + xtc_sample *s;xtc_result r;if(!path || !out)return XTC_ERR_INVALID_ARGUMENT; + s=calloc(1,sizeof(*s));if(!s)return XTC_ERR_OUT_OF_MEMORY; + r=xtc_wave_load(path,&s->wave); + if(r==XTC_OK && s->wave.frames>XTC_SAMPLE_MAX_FRAMES)r=XTC_ERR_CAPACITY; + if(r!=XTC_OK){xtc_sample_destroy(s);return r;}*out=s;return XTC_OK; +} +xtc_result xtc_sample_info(const xtc_sample *s,unsigned int *rate,size_t *frames) +{if(!s || !rate || !frames)return XTC_ERR_INVALID_ARGUMENT;*rate=s->wave.rate;*frames=s->wave.frames;return XTC_OK;} +xtc_result xtc_sample_read(const xtc_sample *s,size_t frame,float *out) +{if(!s || !out || frame>=s->wave.frames)return XTC_ERR_INVALID_ARGUMENT;*out=s->wave.samples[frame];return XTC_OK;} +xtc_result xtc_sample_analyze(const xtc_sample *s,size_t begin,size_t end,xtc_sample_stats *out) +{ + size_t i;double sum=0,square=0;xtc_sample_stats result; + if(!range(s,begin,end) || !out)return XTC_ERR_INVALID_ARGUMENT; + result.minimum=result.maximum=s->wave.samples[begin]; + for(i=begin;iwave.samples[i];if(vresult.maximum)result.maximum=v;sum+=v;square+=(double)v*v;} + result.mean=sum/(double)(end-begin);result.rms=sqrt(square/(double)(end-begin));*out=result;return XTC_OK; +} diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index ca4ea27..ba8b520 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -4,6 +4,7 @@ #include "piano_roll.hpp" #include "mixer_widget.hpp" #include "plugin_dialog.hpp" +#include "sample_editor.hpp" #include "theme.hpp" #include "snap.hpp" #include "velocity_lane.hpp" @@ -309,7 +310,8 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) timeline_scroll->end(); sequencer_->before_edit = before_arrangement_edit; sequencer_->loop_select=[](void *c,uint64_t start,uint64_t end){auto *w=static_cast(c);return w->set_loop(start,end,w->settings_.loop_enabled!=0);}; - sequencer_->open_clip = [](void *c) { piano_callback(nullptr,c); }; + sequencer_->open_clip = [](void *c) {auto *w=static_cast(c);xtc_clip_info clip{}; + if(xtc_project_find_clip(w->project_.get(),w->sequencer_->selected_clip(),&clip)==XTC_OK && !clip.midi){auto r=w->open_sample_editor();if(r!=XTC_OK)w->report_error("Sample editor unavailable",r);}else piano_callback(nullptr,c);}; sequencer_->instrument_select = [](void *c, uint64_t track, const xtc_plugin_spec *spec, bool browse) { auto *w = static_cast(c); w->sequencer_->select(track); @@ -330,6 +332,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) Fl_Group::current(parent); menu->add("Edit/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast(c)->nudge_selection(-1);},sequencer_); menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast(c)->nudge_selection(1);},sequencer_); + menu->add("View/Sample editor",0,[](Fl_Widget *,void *c){auto *w=static_cast(c);auto r=w->open_sample_editor();if(r!=XTC_OK)w->report_error("Sample editor unavailable",r);},this); menu->add("Edit/Audio tools/Normalize source peak...",0,[](Fl_Widget *,void *c){static_cast(c)->normalize_dialog();},sequencer_); menu->add("Edit/Audio tools/Set boundary fades...",0,[](Fl_Widget *,void *c){static_cast(c)->fades_dialog();},sequencer_); menu->add("Edit/Audio tools/Remove boundary fades",0,[](Fl_Widget *,void *c){static_cast(c)->transform_selection(XTC_CLIPS_FADE,0);},sequencer_); @@ -2594,6 +2597,15 @@ xtc_result MainWindow::collect_project(const char *directory) BusyOperation busy(*this,status_,"Collecting project media…"); return xtc_project_collect(project_.get(),directory); } +xtc_result MainWindow::open_sample_editor() +{ + if(recorder_)return XTC_ERR_INVALID_STATE; + xtc_clip_info clip{};auto r=xtc_project_find_clip(project_.get(),sequencer_->selected_clip(),&clip); + if(r!=XTC_OK)return r; + if(clip.midi||!clip.asset_path[0])return XTC_ERR_INVALID_STATE; + xtc_sample *sample=nullptr;r=xtc_sample_load(clip.asset_path,&sample);if(r!=XTC_OK)return r; + SampleEditor editor(sample);editor.run();return XTC_OK; +} void MainWindow::show_commands() { std::vector commands; diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index 7d9ffc8..e987f9f 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -61,6 +61,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); + xtc_result open_sample_editor(); 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); diff --git a/src/gui/sample_editor.cpp b/src/gui/sample_editor.cpp new file mode 100644 index 0000000..69c2f84 --- /dev/null +++ b/src/gui/sample_editor.cpp @@ -0,0 +1,46 @@ +#include "sample_editor.hpp" +#include "theme.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +class SampleCanvas : public Fl_Widget { + SampleEditor &editor_;size_t anchor_=0; + size_t frame(int px) const {size_t width=std::max(size_t(1),editor_.frames()/editor_.zoom());return std::min(editor_.frames()-1,editor_.origin()+static_cast(std::max(0,std::min(w()-1,px-x())))*width/static_cast(w()));} +public: + SampleCanvas(int x,int y,int w,int h,SampleEditor &e):Fl_Widget(x,y,w,h),editor_(e){} + int handle(int event) override { + if(event==FL_PUSH && Fl::event_button()==FL_LEFT_MOUSE){anchor_=frame(Fl::event_x());editor_.select(anchor_,anchor_+1);return 1;} + if(event==FL_DRAG){size_t f=frame(Fl::event_x());editor_.select(std::min(f,anchor_),std::max(f,anchor_)+1);return 1;} + return Fl_Widget::handle(event); + } + void draw() override { + fl_push_clip(x(),y(),w(),h());fl_color(xtc_ui::background());fl_rectf(x(),y(),w(),h()); + size_t width=std::max(size_t(1),editor_.frames()/editor_.zoom()); + for(int px=0;px(px)*width/static_cast(w());size_t e=std::min(editor_.frames(),editor_.origin()+static_cast(px+1)*width/static_cast(w()));if(b>=editor_.frames())break;if(e<=b)e=b+1; + if(beditor_.begin()){fl_color(fl_rgb_color(45,76,80));fl_rectf(x()+px,y(),1,h());} + xtc_sample_stats stats{};if(xtc_sample_analyze(editor_.sample(),b,e,&stats)==XTC_OK){fl_color(fl_rgb_color(95,204,182));int a=static_cast(std::max(-1.f,std::min(1.f,stats.maximum))*(h()/2-8));int z=static_cast(std::max(-1.f,std::min(1.f,stats.minimum))*(h()/2-8));fl_line(x()+px,y()+h()/2-a,x()+px,y()+h()/2-z);} + } + fl_color(xtc_ui::border());fl_line(x(),y()+h()/2,x()+w(),y()+h()/2);fl_pop_clip(); + } +}; +SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc — Sample editor"),sample_(sample,xtc_sample_destroy) +{ + xtc_sample_info(sample_.get(),&rate_,&frames_);end_=frames_;set_modal();Fl_Group::begin(); + auto *hint=new Fl_Box(16,12,1068,34,"Source frames / mono downmix — drag a range; numeric endpoints are exclusive. Original file unchanged.");hint->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE); + begin_input_=new Fl_Value_Input(90,58,140,28,"Start frame");end_input_=new Fl_Value_Input(330,58,140,28,"End frame");origin_input_=new Fl_Value_Input(580,58,140,28,"View start"); + for(auto *input:{begin_input_,end_input_,origin_input_}){input->step(1);input->bounds(0,static_cast(frames_));input->when(FL_WHEN_RELEASE);input->callback([](Fl_Widget *,void *c){auto *e=static_cast(c);double b=e->begin_input_->value(),z=e->end_input_->value(),o=e->origin_input_->value();if(std::isfinite(b)&&std::isfinite(z)&&std::isfinite(o)&&b>=0&&z>=0&&o>=0&&b<=e->frames_&&z<=e->frames_&&oframes_){e->select(static_cast(b),static_cast(z));e->view(static_cast(o),e->zoom_);}e->refresh();},this);} + auto *plus=new Fl_Button(750,58,100,28,"Zoom in");plus->callback([](Fl_Widget *,void *c){auto *e=static_cast(c);e->view(e->begin_,std::min(64U,e->zoom_*2));},this); + auto *minus=new Fl_Button(860,58,100,28,"Zoom out");minus->callback([](Fl_Widget *,void *c){auto *e=static_cast(c);e->view(e->origin_,std::max(1U,e->zoom_/2));},this); + canvas_=new SampleCanvas(16,104,1068,340,*this);stats_=new Fl_Box(16,452,1068,50);stats_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE); + auto *close=new Fl_Button(970,604,110,30,"Close");close->callback([](Fl_Widget *,void *c){static_cast(c)->hide();},this);callback([](Fl_Widget *,void *c){static_cast(c)->hide();},this); + Fl_Group::end();xtc_ui::style(this);refresh(); +} +xtc_result SampleEditor::select(size_t b,size_t e){if(b>=e||e>frames_)return XTC_ERR_INVALID_ARGUMENT;begin_=b;end_=e;refresh();return XTC_OK;} +xtc_result SampleEditor::view(size_t b,unsigned int zoom){if(b>=frames_||!zoom||zoom>64)return XTC_ERR_INVALID_ARGUMENT;origin_=b;zoom_=zoom;refresh();return XTC_OK;} +void SampleEditor::refresh(){begin_input_->value(static_cast(begin_));end_input_->value(static_cast(end_));origin_input_->value(static_cast(origin_));xtc_sample_stats s{};xtc_sample_analyze(sample_.get(),begin_,end_,&s);char text[256];std::snprintf(text,sizeof(text),"%u Hz / %zu source frames / selection %zu..%zu (%0.6f s) / zoom %ux\nMin %.6f Max %.6f RMS %.6f DC %.6f",rate_,frames_,begin_,end_,static_cast(end_-begin_)/rate_,zoom_,s.minimum,s.maximum,s.rms,s.mean);stats_->copy_label(text);canvas_->redraw();} +void SampleEditor::run(){show();while(shown())Fl::wait();} diff --git a/src/gui/sample_editor.hpp b/src/gui/sample_editor.hpp new file mode 100644 index 0000000..2221ae6 --- /dev/null +++ b/src/gui/sample_editor.hpp @@ -0,0 +1,27 @@ +#ifndef XTC_SAMPLE_EDITOR_HPP +#define XTC_SAMPLE_EDITOR_HPP +#include "xtc/xtc_sample.h" +#include +#include +#include +class Fl_Value_Input; +class Fl_Box; +class SampleCanvas; +class SampleEditor : public Fl_Double_Window { +public: + /* Takes ownership of loaded snapshot. Modal edits never touch audio callbacks. */ + explicit SampleEditor(xtc_sample *sample); + void run(); + xtc_result select(size_t begin,size_t end); + xtc_result view(size_t begin,unsigned int zoom); + const xtc_sample *sample() const {return sample_.get();} + size_t begin() const {return begin_;} size_t end() const {return end_;} + size_t frames() const {return frames_;} size_t origin() const {return origin_;} + unsigned int zoom() const {return zoom_;} + void refresh(); +private: + std::unique_ptr sample_{nullptr,xtc_sample_destroy}; + size_t frames_=0,begin_=0,end_=0,origin_=0;unsigned int rate_=0,zoom_=1; + Fl_Value_Input *begin_input_,*end_input_,*origin_input_;Fl_Box *stats_;SampleCanvas *canvas_; +}; +#endif diff --git a/src/gui/sequencer_widget.cpp b/src/gui/sequencer_widget.cpp index 19083a2..e03ba5c 100644 --- a/src/gui/sequencer_widget.cpp +++ b/src/gui/sequencer_widget.cpp @@ -432,7 +432,7 @@ int SequencerWidget::handle(int event) uint64_t hit=clip_;clip_=previous_clip;toggle_clip(hit);mode_=0;return 1; } selection_.clear(); - if (Fl::event_button() == FL_LEFT_MOUSE && Fl::event_clicks() && clip_ != UINT64_MAX && original_.midi) { + if (Fl::event_button() == FL_LEFT_MOUSE && Fl::event_clicks() && clip_ != UINT64_MAX) { mode_ = 0; if (open_clip) open_clip(context_); return 1; diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index cf5f835..8e2599e 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -1,4 +1,5 @@ #include "../../src/gui/main_window.hpp" +#include "../../src/gui/sample_editor.hpp" #include "../../src/gui/graph_widget.hpp" #include "../../src/gui/sequencer_widget.hpp" #include "../../src/gui/piano_roll.hpp" @@ -188,8 +189,17 @@ xtc_result xtc_backend_set_recorder(xtc_backend *, xtc_recorder *recorder) void xtc_backend_collect(xtc_backend *backend) { CHECK(backend == &handle && alive == 1U); ++collects; } } +static int sample_editor_tests() +{ + reset(); + MainWindow window(false,"");window.show();auto *s=timeline(&window);char path[4096];CHECK(realpath("examples/projects/original-pluck.wav",path)); + CHECK(window.import_audio(path)==XTC_OK); + Fl::add_timeout(.1,[](void *){auto *e=dynamic_cast(Fl::modal());CHECK(e!=nullptr);if(e){CHECK(e->select(10,100)==XTC_OK);CHECK(e->select(100,10)==XTC_ERR_INVALID_ARGUMENT);CHECK(e->view(10,8)==XTC_OK);CHECK(e->view(e->frames(),1)==XTC_ERR_INVALID_ARGUMENT);e->hide();}},nullptr); + CHECK(window.open_sample_editor()==XTC_OK);CHECK(s->selected_clip()!=UINT64_MAX);window.do_callback();return failures ? 1 : 0; +} int main(int argc, char **argv) { + if(std::getenv("XTC_TEST_SAMPLE_EDITOR_ONLY"))return sample_editor_tests(); if(!std::getenv("XTC_TEST_MODERN_EDITING_ONLY")) { reset(); { diff --git a/tests/unit/test_sample.c b/tests/unit/test_sample.c new file mode 100644 index 0000000..dc7a581 --- /dev/null +++ b/tests/unit/test_sample.c @@ -0,0 +1,22 @@ +#define _POSIX_C_SOURCE 200809L +#include "xtc/xtc_sample.h" +#include +#include +#include +#include +#include +int main(void) +{ + char root[]="/tmp/xtc-sample-test-XXXXXX",path[256];float values[]={0,.25f,-.5f,.75f,0,0,.5f,-.25f}; + xtc_recorder *rec=NULL;xtc_sample *s=NULL,*sentinel=NULL;xtc_sample_stats stats;size_t frames;unsigned int rate;float v=123; + assert(mkdtemp(root));snprintf(path,sizeof(path),"%s/source.wav",root); + assert(xtc_recorder_create(path,8000,8,&rec)==XTC_OK);xtc_recorder_push(rec,values,8);assert(xtc_recorder_finish(rec)==XTC_OK);xtc_recorder_destroy(rec); + assert(xtc_sample_load(path,&s)==XTC_OK && xtc_sample_info(s,&rate,&frames)==XTC_OK && rate==8000 && frames==8); + assert(xtc_sample_analyze(s,0,8,&stats)==XTC_OK && stats.minimum==-.5f && stats.maximum==.75f && stats.mean==.09375); + assert(fabs(stats.rms-sqrt(1.1875/8))<.000001); + assert(xtc_sample_read(s,8,&v)==XTC_ERR_INVALID_ARGUMENT && v==123); + assert(xtc_sample_read(s,2,&v)==XTC_OK && v==-.5f); + assert(xtc_sample_analyze(s,2,2,&stats)==XTC_ERR_INVALID_ARGUMENT); + assert(xtc_sample_load("/missing/xtc.wav",&sentinel)==XTC_ERR_IO && sentinel==NULL); + xtc_sample_destroy(s);assert(unlink(path)==0 && rmdir(root)==0);puts("Sample editor core tests passed.");return 0; +}