feat: add bounded audio sample editor with waveform selection and source statistics

This commit is contained in:
Johannes Findeisen 2026-11-06 01:01:37 +01:00
commit 7a04c6d922
12 changed files with 364 additions and 8 deletions

View file

@ -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

15
docs/AUDIO_EDITOR.md Normal file
View file

@ -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.

View file

@ -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.

21
include/xtc/xtc_sample.h Normal file
View file

@ -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

27
src/audio/sample.c Normal file
View file

@ -0,0 +1,27 @@
#include "xtc/xtc_sample.h"
#include "wave_internal.h"
#include <stdlib.h>
#include <math.h>
struct xtc_sample {xtc_wave wave;};
static int range(const xtc_sample *s,size_t begin,size_t end){return s && begin<end && end<=s->wave.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;i<end;++i){float v=s->wave.samples[i];if(v<result.minimum)result.minimum=v;if(v>result.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;
}

View file

@ -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<MainWindow *>(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<MainWindow *>(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<MainWindow *>(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<SequencerWidget *>(c)->nudge_selection(-1);},sequencer_);
menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(1);},sequencer_);
menu->add("View/Sample editor",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(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<SequencerWidget *>(c)->normalize_dialog();},sequencer_);
menu->add("Edit/Audio tools/Set boundary fades...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->fades_dialog();},sequencer_);
menu->add("Edit/Audio tools/Remove boundary fades",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(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<std::string> commands;

View file

@ -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);

46
src/gui/sample_editor.cpp Normal file
View file

@ -0,0 +1,46 @@
#include "sample_editor.hpp"
#include "theme.hpp"
#include <FL/Fl.H>
#include <FL/Fl_Value_Input.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Box.H>
#include <FL/fl_draw.H>
#include <algorithm>
#include <cmath>
#include <cstdio>
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<size_t>(std::max(0,std::min(w()-1,px-x())))*width/static_cast<size_t>(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<w();++px){size_t b=editor_.origin()+static_cast<size_t>(px)*width/static_cast<size_t>(w());size_t e=std::min(editor_.frames(),editor_.origin()+static_cast<size_t>(px+1)*width/static_cast<size_t>(w()));if(b>=editor_.frames())break;if(e<=b)e=b+1;
if(b<editor_.end() && e>editor_.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<int>(std::max(-1.f,std::min(1.f,stats.maximum))*(h()/2-8));int z=static_cast<int>(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<double>(frames_));input->when(FL_WHEN_RELEASE);input->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(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_&&o<e->frames_){e->select(static_cast<size_t>(b),static_cast<size_t>(z));e->view(static_cast<size_t>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(c)->hide();},this);callback([](Fl_Widget *,void *c){static_cast<SampleEditor *>(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<double>(begin_));end_input_->value(static_cast<double>(end_));origin_input_->value(static_cast<double>(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<double>(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();}

27
src/gui/sample_editor.hpp Normal file
View file

@ -0,0 +1,27 @@
#ifndef XTC_SAMPLE_EDITOR_HPP
#define XTC_SAMPLE_EDITOR_HPP
#include "xtc/xtc_sample.h"
#include <FL/Fl_Double_Window.H>
#include <functional>
#include <memory>
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<xtc_sample,decltype(&xtc_sample_destroy)> 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

View file

@ -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;

View file

@ -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<SampleEditor *>(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();
{

22
tests/unit/test_sample.c Normal file
View file

@ -0,0 +1,22 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_sample.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
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;
}