feat: detect and slice audio attacks atomically and qualify sample editor workflows
This commit is contained in:
parent
edeeb8e8bb
commit
9e380d4dd0
20 changed files with 257 additions and 22 deletions
|
|
@ -74,7 +74,8 @@ select a missing clip and use File/Relink selected audio to repair it.
|
|||
Add MIDI clip on an existing track, then open Piano roll (Ctrl+4) to draw notes.
|
||||
Connect a MIDI source to the JACK `midi_in` port for live playing; Panic/F9
|
||||
silences held notes. See [MIDI workflow and limits](docs/MIDI.md).
|
||||
See [audio workflow and limits](docs/AUDIO_FILES.md).
|
||||
See [audio workflow and limits](docs/AUDIO_FILES.md) and the
|
||||
[sample editor workflows](docs/AUDIO_EDITOR.md).
|
||||
Mixer (Ctrl+2) selects a track for gain/pan/mute/solo and automation editing.
|
||||
Ctrl+Z undoes; Ctrl+Shift+Z or Ctrl+Y redoes up to 32 edits.
|
||||
The GUI now exposes left `output` and `output_right` JACK ports; route both
|
||||
|
|
|
|||
|
|
@ -49,3 +49,25 @@ Select audible bounds finds the first and last sample strictly above the chosen
|
|||
## Phase 78
|
||||
|
||||
Snap to zero crossings searches each selected boundary within a source-frame radius up to 65,536. It uses the mono source, chooses earlier positions on equal distance and changes both endpoints only if both searches yield a nonempty range. Source edges qualify only when their adjacent sample is zero. This can reduce boundary discontinuities but does not guarantee click-free output; musical-tick trimming/slicing can move a boundary away from the exact crossing.
|
||||
|
||||
## Phase 79
|
||||
|
||||
Detect attacks uses absolute sample amplitude, an initial armed state, rearming below one quarter of the threshold and a minimum frame gap between retained attacks. It marks up to 128 source positions in orange; hovering the statistics shows exact frame indices. This is a threshold detector, not spectral onset detection, beat detection or tempo estimation. Selection/processing changes invalidate the markers. Slice clip at attacks splits the current clip reference at those positions in one project undo step and closes the editor. Edits in the local buffer must be saved/relinked first if the slices should use processed audio. Source positions round to nearest musical ticks; out-of-trim, edge and coalesced cuts are skipped. No usable cuts rejects. Left ID, source references and outside fades remain; inner explicit fades clear. Long explicit fades and existing automatic region fades can sound different after splitting. Timeline/source rounding may differ by one sample, so this is not sample-accurate timeline slicing. The no-explicit-fade aligned regression fixture remains bit-identical across slicing. No media files are written by slicing.
|
||||
|
||||
## Validation and timing limits
|
||||
|
||||
Every phase passed focused model and GUI checks before its implementation commit.
|
||||
The integrated checks add allocation failure/cleanup, oversized sources, overflow,
|
||||
clip ID rollback, exact selected samples, persistence, no-fade render comparison,
|
||||
plugin fixtures, software JACK, Clang analysis, ASan/UBSan and install layouts.
|
||||
The actual source editor and attack markers are shown in the
|
||||
[screenshot gallery](../screenshots/README.md).
|
||||
|
||||
Trim placement preserves alignment in musical ticks; independent source/timeline
|
||||
conversion can differ by one sample. No sample-accurate timeline cuts or
|
||||
click-free transitions are promised. Clip source data outside a local processing
|
||||
selection remains intact; exported selections contain only their chosen frames.
|
||||
Closing the editor discards unsaved local processing. Successful project Undo
|
||||
keeps generated files for redo/history; only failed adoption removes its owned
|
||||
new output. Rejecting sources above the editor's frame limit may temporarily
|
||||
use the existing WAV reader's larger decode bound before rejecting them.
|
||||
|
|
|
|||
|
|
@ -216,3 +216,7 @@ seeks, shortened media tails, one-frame files, source/project limits, capacity
|
|||
rollback, duplication/fade inheritance, persistence/invalid metadata, undo, peak
|
||||
bins after source removal and byte-identical source preservation. GUI tests
|
||||
cover short-clip selection, shared previews, split/duplicate/delete and undo.
|
||||
|
||||
Phases 69–79 add a modal audio source editor with frame selection, reversible
|
||||
processing, selection export, copy adoption and transient slicing. See
|
||||
[AUDIO_EDITOR.md](AUDIO_EDITOR.md) for commands, undo behavior and mono/size limits.
|
||||
|
|
|
|||
|
|
@ -95,3 +95,15 @@ Screenshots are refreshed, including the command palette. See WORKSPACE.md.
|
|||
Phase 38 adds dependency-free Soft Keys/Sub Bass instruments and Delay/Reverb
|
||||
effects with saved controls and automation. Choose instruments in track headers;
|
||||
insert effects on selected graph wires from Edit. See [BUILTIN_DEVICES.md](BUILTIN_DEVICES.md).
|
||||
|
||||
## Phases 69–79: audio source editing
|
||||
|
||||
Audio clips now open a modal waveform editor with native frame selection,
|
||||
zoom, source statistics, nondestructive timeline trims, selection export and
|
||||
save-copy adoption. Local one-step undo supports reverse, polarity inversion,
|
||||
DC removal, linear/equal-power fades and gain/normalization. Silence boundaries,
|
||||
zero-crossing snapping and amplitude attack markers support transactional clip
|
||||
slicing. Originals are never overwritten; exported media remains available to
|
||||
project undo/redo. Source editing uses the existing mono downmix and bounded
|
||||
snapshots; timeline cuts round to musical ticks. See
|
||||
[AUDIO_EDITOR.md](AUDIO_EDITOR.md) for exact behavior and remaining limits.
|
||||
|
|
|
|||
|
|
@ -1837,12 +1837,40 @@ Implemented: deterministic nearest zero-sample/sign-crossing boundary search wit
|
|||
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.
|
||||
- [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
|
||||
Planned.
|
||||
Implemented: hysteretic amplitude attack detection with refractory gaps, source-frame markers and transactional nearest-tick slicing with one project undo step. Capacity failures preserve clips/IDs; core render comparison and persistence reload, GUI slicing/undo, allocation rollback, sanitizer, static-analysis, plugin/JACK and installation checks pass. See integrated validation and AUDIO_EDITOR.md for explicit mono, timing and detector limits.
|
||||
|
||||
|
||||
### Integrated validation for Phases 69–79
|
||||
|
||||
Each of the eleven phases has its own tested implementation commit. The final
|
||||
warning-enabled core/offline suite and focused sample-editor GUI checks cover
|
||||
native frame selection, trim rounding, local processing/undo, selection export,
|
||||
failed/successful copy adoption, project undo and slicing. The native-rate GUI
|
||||
case trims through the actual editor callback using the fixture's 22.05 kHz rate.
|
||||
Core regressions verify exact range samples, curve endpoints, residual DC,
|
||||
silence/zero-crossing ties, threshold hysteresis, attack capacity, partial-slice
|
||||
capacity rollback/ID preservation, an aligned no-fade render comparison and
|
||||
saved-slice reload. Oversized source, overflow and gain-cap failures are explicit.
|
||||
|
||||
Allocation-failure injection covers every processing operation, source loading,
|
||||
WAV save cleanup and slicing preparation. Full GUI and LADSPA/CLAP/VST2/VST3
|
||||
effect/instrument fixtures, software JACK lifecycle, ASan/UBSan core and focused/
|
||||
full GUI, Clang analysis and both staged install layouts passed. Sanitizer GUI
|
||||
runs retain only the existing system Fontconfig leak suppression. The public
|
||||
sample API, docs and two real editor captures are included in the repository.
|
||||
|
||||
No dependency or project-format migration was added (writer 16, reads 1–16).
|
||||
All sample I/O, allocation and processing run on the control thread. The editor
|
||||
uses the existing mono downmix, bounded snapshots and one local undo/redo step;
|
||||
project trims/slices remain musical-tick based. Long-file streaming, stereo
|
||||
source editing, audition/scrubbing, time stretching, spectral processing,
|
||||
physical listening/hardware and arbitrary third-party plugins remain unqualified.
|
||||
See [AUDIO_EDITOR.md](AUDIO_EDITOR.md) for precise workflows and limits.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -218,3 +218,9 @@ batch audio fades and static mixer A/B recall. See
|
|||
[MODERN_EDITING.md](MODERN_EDITING.md) for commands, scope and limitations.
|
||||
|
||||
See [arrangement and composition workflows](COMPOSITION_WORKFLOWS.md) for markers, MIDI composition, normalization and portable media collection.
|
||||
|
||||
Select an audio clip and open View / Sample editor (or double-click it) to inspect
|
||||
source samples, choose native frame ranges, trim the clip or export a new WAV.
|
||||
Processing and one-level local undo remain in the editor until Save full copy and
|
||||
relink. See [AUDIO_EDITOR.md](AUDIO_EDITOR.md) for all sample tools, transient
|
||||
slicing, musical-tick rounding and source preservation limits.
|
||||
|
|
|
|||
|
|
@ -239,3 +239,7 @@ batch audio fades and static mixer A/B recall. See
|
|||
Phases 58–68 add arrangement markers and sections, ripple edits, complete track
|
||||
duplication, MIDI splitting/consolidation and composition tools, peak normalization
|
||||
and portable media collection. See [COMPOSITION_WORKFLOWS.md](COMPOSITION_WORKFLOWS.md).
|
||||
|
||||
Phases 69–79 add a modal audio source editor with frame selection, reversible
|
||||
processing, selection export, copy adoption and transient slicing. See
|
||||
[AUDIO_EDITOR.md](AUDIO_EDITOR.md) for commands, undo behavior and mono/size limits.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
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,
|
||||
* <=4194304 source frames. Only explicit save writes files; never use in realtime.
|
||||
* Ranges are half-open,
|
||||
* nonempty native source frames; failures preserve outputs. */
|
||||
#define XTC_SAMPLE_MAX_FRAMES 4194304U
|
||||
typedef struct xtc_sample xtc_sample;
|
||||
|
|
@ -18,7 +19,8 @@ xtc_result xtc_sample_analyze(const xtc_sample *sample,size_t begin,size_t end,x
|
|||
typedef enum { XTC_SAMPLE_REVERSE, XTC_SAMPLE_INVERT, XTC_SAMPLE_REMOVE_DC, XTC_SAMPLE_FADE_IN, XTC_SAMPLE_FADE_OUT, XTC_SAMPLE_FADE_IN_POWER, XTC_SAMPLE_FADE_OUT_POWER, XTC_SAMPLE_GAIN, XTC_SAMPLE_NORMALIZE } xtc_sample_operation;
|
||||
/* Independent full-source result; only [begin,end) changes. Never mutates input.
|
||||
* amount: GAIN 0..16, NORMALIZE target 0<peak<=1 (required gain <=16).
|
||||
* Other operations ignore amount. No implicit clipping; nonfinite results reject.
|
||||
* All amounts must be finite; other operations ignore the value. No implicit
|
||||
* clipping; nonfinite results reject.
|
||||
* One-frame fades reject; fade endpoints are exactly 0 and 1. Failure preserves *out. */
|
||||
xtc_result xtc_sample_process(const xtc_sample *sample,size_t begin,size_t end,
|
||||
xtc_sample_operation operation,double amount,xtc_sample **out);
|
||||
|
|
@ -30,8 +32,21 @@ xtc_result xtc_sample_audible_bounds(const xtc_sample *sample,size_t begin,size_
|
|||
* center 0..frames, radius <=65536 frames. Edge boundaries qualify only when
|
||||
* the adjacent sample is zero. NOT_FOUND preserves output. */
|
||||
xtc_result xtc_sample_zero_crossing(const xtc_sample *sample,size_t center,size_t radius,size_t *out);
|
||||
#define XTC_SAMPLE_MAX_TRANSIENTS 128U
|
||||
/* Absolute-amplitude attacks: armed initially, rearm <=threshold/4, trigger
|
||||
* >threshold, minimum gap since last retained attack. threshold >0..1,
|
||||
* gap 1..MAX_FRAMES; includes a loud range start. No spectral/beat inference.
|
||||
* All outputs unchanged on NOT_FOUND/capacity/errors. positions capacity 1..128. */
|
||||
xtc_result xtc_sample_transients(const xtc_sample *sample,size_t begin,size_t end,
|
||||
double threshold,size_t gap,size_t *positions,size_t capacity,size_t *count);
|
||||
/* Sorted unique native source positions round to nearest timeline tick. Skip
|
||||
* boundaries/out-of-trim and coalesced ticks. Split entire clip atomically,
|
||||
* retaining left ID and outer explicit fades; no render auto-fades synthesized.
|
||||
* Source/routing/mix unchanged, count unchanged on failure/no usable cuts. */
|
||||
xtc_result xtc_project_slice_source_frames(xtc_project *project,uint64_t clip,unsigned int rate,
|
||||
const size_t *positions,size_t count,size_t *out_slices);
|
||||
/* Control-thread bounded mono float32 WAV save. Absolute path <512 bytes,
|
||||
* NEW destination only. Failure/cancel publishes nothing and removes temporary. */
|
||||
* NEW destination only. Failure publishes nothing and removes temporary. */
|
||||
xtc_result xtc_sample_write(const xtc_sample *sample,size_t begin,size_t end,const char *path);
|
||||
/* Select native source frame bounds inside the clip's current audible trim.
|
||||
* Start rounds up to a tick; end rounds down. Sub-tick empty ranges reject.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# xtc screenshots
|
||||
|
||||
All captures refreshed from the Phase 68 FLTK application on 2026-11-06.
|
||||
The main gallery was refreshed after Phase 68 on 2026-11-06. Two additional
|
||||
Phase 79 captures show the new sample editor and transient markers.
|
||||
These are actual rendered application windows and menus on a 1440 × 960 virtual
|
||||
X11 screen, without mockups, compositing or image edits.
|
||||
|
||||
|
|
@ -35,6 +36,15 @@ See [COMPOSITION_WORKFLOWS.md](../docs/COMPOSITION_WORKFLOWS.md) for precise
|
|||
behavior, limits and commands. The screenshots illustrate implemented workflows;
|
||||
they do not establish feature parity with other DAWs.
|
||||
|
||||
## Audio source editor (Phases 69–79)
|
||||
|
||||
- [Frame selection, waveform, statistics and processing controls](sample-editor.png)
|
||||
- [Detected source attacks and clip slicing controls](sample-transients.png)
|
||||
|
||||
These captures show the actual modal editor with the repository pluck WAV.
|
||||
Processing is staged locally; Save full copy and relink applies it to the project.
|
||||
See [AUDIO_EDITOR.md](../docs/AUDIO_EDITOR.md) for limits and frame/tick rounding.
|
||||
|
||||
## Refreshed application views
|
||||
|
||||
All pre-existing images below were recaptured. Mixer playback meters come from
|
||||
|
|
@ -102,3 +112,6 @@ prerequisites, a running JACK server, Xvfb (`xvfb-run`), ImageMagick (`import`)
|
|||
PNGs. All demonstration edits and collection outputs use an automatically removed
|
||||
temporary directory; no preferences are saved. It neither installs dependencies
|
||||
nor starts/reconfigures the audio server.
|
||||
|
||||
Set `XTC_CAPTURE_SAMPLE_ONLY=1` for the capture tool (or the make target) to write
|
||||
only the two sample-editor images while retaining the existing gallery.
|
||||
|
|
|
|||
BIN
screenshots/sample-editor.png
Normal file
BIN
screenshots/sample-editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
screenshots/sample-transients.png
Normal file
BIN
screenshots/sample-transients.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
#include "xtc/xtc_sample.h"
|
||||
#include "wave_internal.h"
|
||||
#include "../project/project_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
|
@ -102,3 +103,34 @@ xtc_result xtc_sample_zero_crossing(const xtc_sample *s,size_t center,size_t rad
|
|||
if(best==SIZE_MAX)return XTC_ERR_NOT_FOUND;
|
||||
*out=best;return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_sample_transients(const xtc_sample *s,size_t begin,size_t end,
|
||||
double threshold,size_t gap,size_t *positions,size_t capacity,size_t *count)
|
||||
{
|
||||
size_t i,n=0,found[XTC_SAMPLE_MAX_TRANSIENTS];int armed=1;
|
||||
if(!range(s,begin,end)||!positions||!count||!capacity||capacity>XTC_SAMPLE_MAX_TRANSIENTS||!gap||gap>XTC_SAMPLE_MAX_FRAMES||!isfinite(threshold)||threshold<=0||threshold>1)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=begin;i<end;++i){double value=fabs(s->wave.samples[i]);if(value<=threshold*.25)armed=1;
|
||||
if(armed && value>threshold){armed=0;if(!n||i-found[n-1]>=gap){if(n==capacity)return XTC_ERR_CAPACITY;found[n++]=i;}}
|
||||
}
|
||||
if(!n)return XTC_ERR_NOT_FOUND;
|
||||
memcpy(positions,found,n*sizeof(*positions));*count=n;return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_slice_source_frames(xtc_project *p,uint64_t id,unsigned int rate,
|
||||
const size_t *positions,size_t count,size_t *out_slices)
|
||||
{
|
||||
xtc_clip_info c;xtc_project *copy=NULL;size_t i,n=0;uint64_t current=id,last,factor,denom;xtc_result r;
|
||||
if(!p||!positions||!out_slices||!rate||rate>768000||!count||count>XTC_SAMPLE_MAX_TRANSIENTS)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<count;++i)if(positions[i]>XTC_SAMPLE_MAX_FRAMES||(i&&positions[i]<=positions[i-1]))return XTC_ERR_INVALID_ARGUMENT;
|
||||
r=xtc_project_find_clip(p,id,&c);if(r!=XTC_OK)return r;
|
||||
if(c.midi||!c.asset_path[0])return XTC_ERR_INVALID_STATE;
|
||||
factor=(uint64_t)xtc_project_tempo(p)*XTC_TICKS_PER_QUARTER;denom=(uint64_t)rate*60000U;last=c.offset_tick;
|
||||
r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
for(i=0;r==XTC_OK&&i<count;++i){uint64_t source=((uint64_t)positions[i]*factor+denom/2)/denom,next;
|
||||
if(source<=last||source>=c.offset_tick+c.length_tick)continue;
|
||||
r=xtc_project_split_audio(copy,current,c.start_tick+source-c.offset_tick,&next);
|
||||
if(r==XTC_OK){current=next;last=source;++n;}
|
||||
}
|
||||
if(r==XTC_OK&&!n)r=XTC_ERR_NOT_FOUND;
|
||||
if(r==XTC_OK){xtc_project old=*p;*p=*copy;*copy=old;*out_slices=n;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2615,6 +2615,15 @@ xtc_result MainWindow::adopt_sample_copy(uint64_t id,const xtc_sample *sample,co
|
|||
if(r!=XTC_OK){if(written)std::remove(path);xtc_project_destroy(candidate);return r;}
|
||||
sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK;
|
||||
}
|
||||
xtc_result MainWindow::slice_sample_clip(uint64_t id,unsigned int rate,const std::vector<size_t> &positions)
|
||||
{
|
||||
auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r;
|
||||
xtc_project *candidate=nullptr;size_t count=0;r=xtc_project_clone(project_.get(),&candidate);
|
||||
if(r==XTC_OK)r=xtc_project_slice_source_frames(candidate,id,rate,positions.data(),positions.size(),&count);
|
||||
if(r==XTC_OK)r=install_project(candidate,false);
|
||||
if(r!=XTC_OK){xtc_project_destroy(candidate);return r;}
|
||||
xtc_clip_info clip{};xtc_project_find_clip(project_.get(),id,&clip);sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK;
|
||||
}
|
||||
xtc_result MainWindow::trim_sample_clip(uint64_t id,unsigned int rate,size_t begin,size_t end)
|
||||
{
|
||||
auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r;
|
||||
|
|
@ -2631,13 +2640,16 @@ xtc_result MainWindow::open_sample_editor()
|
|||
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);
|
||||
try {
|
||||
SampleEditor editor(SampleEditor::Data(sample,xtc_sample_destroy));
|
||||
uint64_t first=0,last=0;unsigned int rate;size_t frames;xtc_sample_info(sample,&rate,&frames);
|
||||
if(xtc_project_tick_to_sample(project_.get(),clip.offset_tick,rate,&first)==XTC_OK && xtc_project_tick_to_sample(project_.get(),clip.offset_tick+clip.length_tick,rate,&last)==XTC_OK)
|
||||
editor.select(static_cast<size_t>(std::min(first,static_cast<uint64_t>(frames))),static_cast<size_t>(std::min(last,static_cast<uint64_t>(frames))));
|
||||
editor.slice=[this,id=clip.id](unsigned int sr,const std::vector<size_t> &positions){return slice_sample_clip(id,sr,positions);};
|
||||
editor.adopt=[this,id=clip.id](const xtc_sample *data,const char *path){return adopt_sample_copy(id,data,path);};
|
||||
editor.trim=[this,id=clip.id](unsigned int sr,size_t b,size_t e){return trim_sample_clip(id,sr,b,e);};
|
||||
editor.run();return XTC_OK;
|
||||
} catch(const std::bad_alloc &){return XTC_ERR_OUT_OF_MEMORY;}
|
||||
}
|
||||
void MainWindow::show_commands()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
/* Control-thread plugin commands, transactional model adoption. */
|
||||
xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index);
|
||||
xtc_result adopt_sample_copy(uint64_t clip,const xtc_sample *sample,const char *path);
|
||||
xtc_result slice_sample_clip(uint64_t clip,unsigned int rate,const std::vector<size_t> &positions);
|
||||
xtc_result trim_sample_clip(uint64_t clip,unsigned int rate,size_t begin,size_t end);
|
||||
xtc_result open_sample_editor();
|
||||
void export_audio();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
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()));}
|
||||
|
|
@ -26,11 +28,13 @@ public:
|
|||
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(fl_rgb_color(220,164,92));for(size_t frame:editor_.attacks()){if(frame<editor_.origin()||frame-editor_.origin()>=width)continue;int px=x()+static_cast<int>((frame-editor_.origin())*static_cast<size_t>(w())/width);fl_line(px,y(),px,y()+h());}
|
||||
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)
|
||||
SampleEditor::SampleEditor(Data sample):Fl_Double_Window(1100,650,"xtc — Sample editor"),sample_(std::move(sample))
|
||||
{
|
||||
if(!sample_)throw std::invalid_argument("Sample editor requires a loaded source");
|
||||
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");
|
||||
|
|
@ -48,13 +52,15 @@ SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc
|
|||
auto *gain=new Fl_Button(704,562,160,30,"Gain / normalize...");gain->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);const char *text=fl_input("Mode (0 gain, 1 normalize), linear gain 0..16 or target peak >0..1","1 0.9");if(!text)return;unsigned int mode;double value;char extra;if(std::sscanf(text,"%u %lf %c",&mode,&value,&extra)!=2||mode>1){fl_alert("Enter mode and linear value.");return;}auto r=e->process(mode ? XTC_SAMPLE_NORMALIZE : XTC_SAMPLE_GAIN,value);if(r!=XTC_OK)fl_alert("Gain edit failed: %s",xtc_result_string(r));},this);
|
||||
auto *silence=new Fl_Button(876,562,200,30,"Select audible bounds...");silence->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);const char *text=fl_input("Silence threshold dBFS (-120..0), padding ms (0..10000)","-60 2");if(!text)return;double db,ms;char extra;if(std::sscanf(text,"%lf %lf %c",&db,&ms,&extra)!=2||!std::isfinite(db)||!std::isfinite(ms)||db< -120||db>0||ms<0||ms>10000){fl_alert("Enter a threshold and padding within the shown bounds.");return;}auto r=e->select_audible(std::pow(10.,db/20),static_cast<size_t>(std::min(static_cast<double>(e->frames_),ms*e->rate_/1000)));if(r!=XTC_OK)fl_alert("Audible bounds not found: %s",xtc_result_string(r));},this);
|
||||
auto *zero=new Fl_Button(16,604,180,30,"Snap to zero crossings...");zero->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);const char *text=fl_input("Search radius in source frames (0..65536)","256");if(!text)return;unsigned int radius;char extra;if(std::sscanf(text,"%u %c",&radius,&extra)!=1||radius>65536){fl_alert("Enter a radius 0..65536.");return;}auto r=e->snap_zero_crossings(radius);if(r!=XTC_OK)fl_alert("Selection unchanged: %s",xtc_result_string(r));},this);
|
||||
auto *attacks=new Fl_Button(210,604,180,30,"Detect attacks...");attacks->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);const char *text=fl_input("Attack threshold dBFS (-120..0), minimum gap ms (>0..10000)","-12 80");if(!text)return;double db,ms;char extra;if(std::sscanf(text,"%lf %lf %c",&db,&ms,&extra)!=2||!std::isfinite(db)||!std::isfinite(ms)||db< -120||db>0||ms<=0||ms>10000){fl_alert("Enter a threshold and positive minimum gap.");return;}auto r=e->detect_attacks(std::pow(10.,db/20),std::max(size_t(1),static_cast<size_t>(std::min(static_cast<double>(e->frames_),ms*e->rate_/1000))));if(r!=XTC_OK)fl_alert("Attack detection failed: %s",xtc_result_string(r));},this);
|
||||
auto *slices=new Fl_Button(404,604,180,30,"Slice clip at attacks");slices->callback([](Fl_Widget *,void *c){auto r=static_cast<SampleEditor *>(c)->slice_clip();if(r!=XTC_OK)fl_alert("Clip unchanged: %s",xtc_result_string(r));else static_cast<SampleEditor *>(c)->hide();},this);
|
||||
auto *undo=new Fl_Button(850,604,110,30,"Undo / redo");undo->callback([](Fl_Widget *,void *c){auto r=static_cast<SampleEditor *>(c)->undo_sample();if(r!=XTC_OK)fl_alert("No local sample edit to undo.");},this);
|
||||
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::select(size_t b,size_t e){if(b>=e||e>frames_)return XTC_ERR_INVALID_ARGUMENT;if(begin_!=b||end_!=e)attacks_.clear();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::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);std::string attacks="Detected source frames (nearest tick used for slicing):";for(size_t frame:attacks_)attacks+=" "+std::to_string(frame);stats_->copy_tooltip(attacks.c_str());canvas_->redraw();}
|
||||
void SampleEditor::run(){show();while(shown())Fl::wait();}
|
||||
|
||||
xtc_result SampleEditor::trim_clip(){return trim ? trim(rate_,begin_,end_) : XTC_ERR_INVALID_STATE;}
|
||||
|
|
@ -65,10 +71,13 @@ xtc_result SampleEditor::export_selection(const char *path){return xtc_sample_wr
|
|||
xtc_result SampleEditor::process(xtc_sample_operation operation,double amount)
|
||||
{
|
||||
xtc_sample *next=nullptr;auto r=xtc_sample_process(sample_.get(),begin_,end_,operation,amount,&next);if(r!=XTC_OK)return r;
|
||||
previous_=std::move(sample_);sample_.reset(next);copy_label("xtc — Sample editor / local sample changes; save a copy to keep");refresh();return XTC_OK;
|
||||
previous_=std::move(sample_);sample_.reset(next);attacks_.clear();copy_label("xtc — Sample editor / local sample changes; save a copy to keep");refresh();return XTC_OK;
|
||||
}
|
||||
xtc_result SampleEditor::undo_sample(){if(!previous_)return XTC_ERR_NOT_FOUND;sample_.swap(previous_);refresh();return XTC_OK;}
|
||||
xtc_result SampleEditor::undo_sample(){if(!previous_)return XTC_ERR_NOT_FOUND;sample_.swap(previous_);attacks_.clear();refresh();return XTC_OK;}
|
||||
|
||||
xtc_result SampleEditor::select_audible(double threshold,size_t padding){size_t b=0,e=0;auto r=xtc_sample_audible_bounds(sample_.get(),begin_,end_,threshold,padding,&b,&e);return r==XTC_OK ? select(b,e) : r;}
|
||||
|
||||
xtc_result SampleEditor::snap_zero_crossings(size_t radius){size_t b=0,e=0;auto r=xtc_sample_zero_crossing(sample_.get(),begin_,radius,&b);if(r==XTC_OK)r=xtc_sample_zero_crossing(sample_.get(),end_,radius,&e);return r==XTC_OK ? select(b,e) : r;}
|
||||
|
||||
xtc_result SampleEditor::detect_attacks(double threshold,size_t gap){size_t positions[XTC_SAMPLE_MAX_TRANSIENTS],count=0;auto r=xtc_sample_transients(sample_.get(),begin_,end_,threshold,gap,positions,XTC_SAMPLE_MAX_TRANSIENTS,&count);if(r!=XTC_OK)return r;try{std::vector<size_t> next(positions,positions+count);attacks_.swap(next);}catch(const std::bad_alloc &){return XTC_ERR_OUT_OF_MEMORY;}refresh();return XTC_OK;}
|
||||
xtc_result SampleEditor::slice_clip(){return slice&&!attacks_.empty() ? slice(rate_,attacks_) : XTC_ERR_INVALID_STATE;}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@
|
|||
#include <FL/Fl_Double_Window.H>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
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);
|
||||
using Data=std::unique_ptr<xtc_sample,decltype(&xtc_sample_destroy)>;
|
||||
explicit SampleEditor(Data sample);
|
||||
std::function<xtc_result(unsigned int,size_t,size_t)> trim;
|
||||
xtc_result trim_clip();
|
||||
std::function<xtc_result(const xtc_sample *,const char *)> adopt;
|
||||
|
|
@ -20,6 +22,10 @@ public:
|
|||
xtc_result undo_sample();
|
||||
xtc_result select_audible(double threshold,size_t padding);
|
||||
xtc_result snap_zero_crossings(size_t radius);
|
||||
std::function<xtc_result(unsigned int,const std::vector<size_t> &)> slice;
|
||||
xtc_result detect_attacks(double threshold,size_t gap);
|
||||
xtc_result slice_clip();
|
||||
const std::vector<size_t> &attacks() const {return attacks_;}
|
||||
void run();
|
||||
xtc_result select(size_t begin,size_t end);
|
||||
xtc_result view(size_t begin,unsigned int zoom);
|
||||
|
|
@ -31,6 +37,7 @@ public:
|
|||
private:
|
||||
std::unique_ptr<xtc_sample,decltype(&xtc_sample_destroy)> sample_{nullptr,xtc_sample_destroy};
|
||||
std::unique_ptr<xtc_sample,decltype(&xtc_sample_destroy)> previous_{nullptr,xtc_sample_destroy};
|
||||
std::vector<size_t> attacks_;
|
||||
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_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -199,17 +199,20 @@ static int sample_editor_tests()
|
|||
CHECK(e->process(XTC_SAMPLE_FADE_IN_POWER)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
|
||||
CHECK(e->process(XTC_SAMPLE_REMOVE_DC)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
|
||||
CHECK(e->process(XTC_SAMPLE_INVERT)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
|
||||
CHECK(e->process(XTC_SAMPLE_REVERSE)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);CHECK(xtc_sample_read(e->sample(),10,&after)==XTC_OK && before==after);CHECK(e->undo_sample()==XTC_OK);CHECK(e->select(100,10)==XTC_ERR_INVALID_ARGUMENT);CHECK(e->view(10,8)==XTC_OK);size_t b=e->begin(),z=e->end();CHECK(e->select_audible(1,0)==XTC_ERR_NOT_FOUND && e->begin()==b && e->end()==z);CHECK(e->select(0,e->frames())==XTC_OK);CHECK(e->select_audible(.01,2)==XTC_OK);CHECK(e->select(0,e->frames())==XTC_OK);CHECK(e->snap_zero_crossings(65536)==XTC_OK);CHECK(e->view(e->frames(),1)==XTC_ERR_INVALID_ARGUMENT);e->hide();}},nullptr);
|
||||
CHECK(e->process(XTC_SAMPLE_REVERSE)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);CHECK(xtc_sample_read(e->sample(),10,&after)==XTC_OK && before==after);CHECK(e->undo_sample()==XTC_OK);CHECK(e->select(100,10)==XTC_ERR_INVALID_ARGUMENT);CHECK(e->view(10,8)==XTC_OK);size_t b=e->begin(),z=e->end();CHECK(e->select_audible(1,0)==XTC_ERR_NOT_FOUND && e->begin()==b && e->end()==z);CHECK(e->select(0,e->frames())==XTC_OK);CHECK(e->select_audible(.01,2)==XTC_OK);CHECK(e->select(0,e->frames())==XTC_OK);CHECK(e->snap_zero_crossings(65536)==XTC_OK);CHECK(e->detect_attacks(.1,2400)==XTC_OK && !e->attacks().empty());CHECK(e->view(e->frames(),1)==XTC_ERR_INVALID_ARGUMENT);unsigned int rate=0;size_t frames=0;CHECK(xtc_sample_info(e->sample(),&rate,&frames)==XTC_OK);CHECK(e->select(rate/10,rate*4/10)==XTC_OK);CHECK(e->trim_clip()==XTC_OK);e->hide();}},nullptr);
|
||||
CHECK(window.open_sample_editor()==XTC_OK);CHECK(s->selected_clip()!=UINT64_MAX);
|
||||
uint64_t id=s->selected_clip();CHECK(window.trim_sample_clip(id,48000,2400,12000)==XTC_OK);
|
||||
xtc_clip_info clip{};CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && clip.offset_tick==96 && clip.length_tick==384);
|
||||
uint64_t id=s->selected_clip();
|
||||
xtc_clip_info clip{};CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && clip.offset_tick==192 && clip.length_tick==576);
|
||||
CHECK(window.undo()==XTC_OK);CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && clip.offset_tick==0);
|
||||
{xtc_sample *data=nullptr;CHECK(xtc_sample_load(path,&data)==XTC_OK);std::string copy="/tmp/xtc-sample-gui-"+std::to_string(getpid())+".wav";
|
||||
submit_result=XTC_ERR_OUT_OF_MEMORY;CHECK(window.adopt_sample_copy(id,data,copy.c_str())==XTC_ERR_OUT_OF_MEMORY && access(copy.c_str(),F_OK)!=0);submit_result=XTC_OK;
|
||||
CHECK(window.adopt_sample_copy(id,data,copy.c_str())==XTC_OK);CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && !strcmp(clip.asset_path,copy.c_str()));
|
||||
CHECK(window.undo()==XTC_OK);CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && !strcmp(clip.asset_path,path));
|
||||
xtc_sample_destroy(data);CHECK(unlink(copy.c_str())==0);}
|
||||
window.do_callback();return failures ? 1 : 0;
|
||||
CHECK(window.slice_sample_clip(id,22050,{2400,4800,7200})==XTC_OK);
|
||||
size_t tracks=0,clips=0;CHECK(xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && clips==4);
|
||||
CHECK(window.undo()==XTC_OK);CHECK(xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && clips==1);
|
||||
window.do_callback();std::puts("Sample editor GUI selection/processing/adoption/slicing/undo passed.");return failures ? 1 : 0;
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include "xtc/xtc_plugin.h"
|
||||
#include "xtc/xtc_editing.h"
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include "xtc/xtc_sample.h"
|
||||
#include <dirent.h>
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -268,8 +270,33 @@ static void composition_failures(void)
|
|||
}
|
||||
xtc_project_destroy(p);assert(rmdir(root)==0);
|
||||
}
|
||||
static void sample_failures(void)
|
||||
{
|
||||
xtc_sample *source=NULL;xtc_project *p=NULL;char path[4096],root[]="/tmp/xtc-sample-fail-XXXXXX",dest[256];uint64_t t,id;int op,failure;
|
||||
assert(getcwd(path,sizeof(path)));assert(strlen(path)+40<sizeof(path));strcat(path,"/examples/projects/original-pluck.wav");assert(mkdtemp(root));
|
||||
snprintf(dest,sizeof(dest),"%s/copy.wav",root);
|
||||
assert(xtc_sample_load(path,&source)==XTC_OK && xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Audio",&t)==XTC_OK && xtc_project_import_audio(p,t,path,0,&id)==XTC_OK);
|
||||
for(op=0;op<12;++op){int succeeded=0;
|
||||
for(failure=0;failure<128;++failure){xtc_sample *output=NULL;xtc_project *copy=NULL;xtc_result r;size_t made=999,cuts[]={2400,4800};
|
||||
assert(xtc_project_clone(p,©)==XTC_OK);fail_after=failure;
|
||||
if(op<9)r=xtc_sample_process(source,100,1000,(xtc_sample_operation)op,op==XTC_SAMPLE_GAIN ? 2 : .5,&output);
|
||||
else if(op==9)r=xtc_sample_load(path,&output);
|
||||
else if(op==10)r=xtc_sample_write(source,100,1000,dest);
|
||||
else r=xtc_project_slice_source_frames(copy,id,48000,cuts,2,&made);
|
||||
fail_after=-1;
|
||||
if(r==XTC_OK)succeeded=1;
|
||||
else {assert(r==XTC_ERR_OUT_OF_MEMORY||r==XTC_ERR_IO);assert(output==NULL && made==999 && access(dest,F_OK)!=0);
|
||||
{size_t tracks,clips;uint64_t probe;assert(xtc_project_get_counts(copy,&tracks,&clips)==XTC_OK && clips==1);assert(xtc_project_duplicate_clip(copy,id,t,10000,&probe)==XTC_OK && probe==1);}}
|
||||
if(op==10&&succeeded)assert(unlink(dest)==0);
|
||||
{DIR *d=opendir(root);struct dirent *entry;assert(d);while((entry=readdir(d)))assert(!strcmp(entry->d_name,".")||!strcmp(entry->d_name,".."));assert(closedir(d)==0);}
|
||||
xtc_sample_destroy(output);xtc_project_destroy(copy);if(succeeded)break;
|
||||
}assert(succeeded);
|
||||
}
|
||||
xtc_sample_destroy(source);xtc_project_destroy(p);assert(rmdir(root)==0);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
sample_failures();
|
||||
composition_failures();
|
||||
modern_edit_failures();
|
||||
builtin_failures();
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
#include "xtc/xtc_sample.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
int main(void)
|
||||
{
|
||||
char root[]="/tmp/xtc-sample-test-XXXXXX",path[256];float values[]={0,.25f,-.5f,.75f,0,0,.5f,-.25f};
|
||||
|
|
@ -64,5 +66,33 @@ int main(void)
|
|||
assert(xtc_sample_zero_crossing(s,6,1,&crossing)==XTC_OK && crossing==5);
|
||||
assert(xtc_sample_zero_crossing(s,8,1,&crossing)==XTC_OK && crossing==7);
|
||||
assert(xtc_sample_zero_crossing(s,0,65537,&crossing)==XTC_ERR_INVALID_ARGUMENT && crossing==7);}
|
||||
{size_t positions[128]={999},count=999;assert(xtc_sample_transients(s,0,8,.4,1,positions,128,&count)==XTC_OK && count==2 && positions[0]==2 && positions[1]==6);
|
||||
count=999;positions[0]=999;assert(xtc_sample_transients(s,0,8,.4,1,positions,1,&count)==XTC_ERR_CAPACITY && count==999 && positions[0]==999);
|
||||
assert(xtc_sample_transients(s,0,8,.4,5,positions,128,&count)==XTC_OK && count==1 && positions[0]==2);}
|
||||
{xtc_project *p=NULL,*q=NULL;xtc_audio_plan *a=NULL,*b=NULL;uint64_t t,c;float before[128],after[128];size_t cuts[]={0,4,8},made=999,tracks=0,clips=0;char song[256];
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Audio",&t)==XTC_OK && xtc_project_import_audio(p,t,path,10,&c)==XTC_OK);
|
||||
assert(xtc_audio_plan_create(p,8000,&a)==XTC_OK && xtc_audio_plan_render(a,0,before,128)==XTC_OK);
|
||||
assert(xtc_project_slice_source_frames(p,c,8000,cuts,3,&made)==XTC_OK && made==1);
|
||||
assert(xtc_audio_plan_create(p,8000,&b)==XTC_OK && xtc_audio_plan_render(b,0,after,128)==XTC_OK && !memcmp(before,after,sizeof(before)));
|
||||
snprintf(song,sizeof(song),"%s/slices.xtc",root);assert(xtc_project_save(p,song)==XTC_OK && xtc_project_create(&q)==XTC_OK && xtc_project_load(q,song)==XTC_OK);
|
||||
assert(xtc_project_get_counts(q,&tracks,&clips)==XTC_OK && clips==2);assert(unlink(song)==0);
|
||||
xtc_audio_plan_destroy(a);xtc_audio_plan_destroy(b);xtc_project_destroy(p);xtc_project_destroy(q);}
|
||||
{xtc_project *p=NULL;uint64_t t,c,extra;size_t cuts[]={1000,2000},made=999,tracks,clips;
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Full",&t)==XTC_OK && xtc_project_add_clip(p,t,"Source",0,960,&c)==XTC_OK && xtc_project_set_clip_asset(p,c,path)==XTC_OK);
|
||||
for(size_t j=1;j<XTC_PROJECT_MAX_CLIPS-1;++j)assert(xtc_project_add_clip(p,t,"Fill",10000,960,&extra)==XTC_OK);
|
||||
assert(xtc_project_slice_source_frames(p,c,48000,cuts,2,&made)==XTC_ERR_CAPACITY && made==999);
|
||||
assert(xtc_project_get_counts(p,&tracks,&clips)==XTC_OK && clips==XTC_PROJECT_MAX_CLIPS-1);
|
||||
assert(xtc_project_add_clip(p,t,"Probe",10000,960,&extra)==XTC_OK && extra==XTC_PROJECT_MAX_CLIPS-1);xtc_project_destroy(p);}
|
||||
{char big[256];unsigned char header[44];uint32_t count=(XTC_SAMPLE_MAX_FRAMES+1U)*4U;FILE *input=fopen(path,"rb"),*output;xtc_sample *q=NULL;
|
||||
assert(input && fread(header,1,44,input)==44 && fclose(input)==0);snprintf(big,sizeof(big),"%s/large.wav",root);
|
||||
for(unsigned int j=0;j<4;++j){header[4+j]=(unsigned char)((count+36U)>>(j*8));header[40+j]=(unsigned char)(count>>(j*8));}
|
||||
output=fopen(big,"wb");assert(output && fwrite(header,1,44,output)==44 && fflush(output)==0 && ftruncate(fileno(output),(off_t)count+44)==0 && fclose(output)==0);
|
||||
assert(xtc_sample_load(big,&q)==XTC_ERR_CAPACITY && q==NULL);assert(unlink(big)==0);}
|
||||
{char edge[256];float large=FLT_MAX;xtc_sample *q=NULL,*result=NULL;snprintf(edge,sizeof(edge),"%s/edge.wav",root);
|
||||
assert(xtc_recorder_create(edge,8000,1,&rec)==XTC_OK);xtc_recorder_push(rec,&large,1);assert(xtc_recorder_finish(rec)==XTC_OK);xtc_recorder_destroy(rec);
|
||||
assert(xtc_sample_load(edge,&q)==XTC_OK && xtc_sample_process(q,0,1,XTC_SAMPLE_GAIN,2,&result)==XTC_ERR_CAPACITY && result==NULL);
|
||||
assert(xtc_sample_read(q,0,&v)==XTC_OK && v==FLT_MAX);xtc_sample_destroy(q);assert(unlink(edge)==0);
|
||||
large=.001f;assert(xtc_recorder_create(edge,8000,1,&rec)==XTC_OK);xtc_recorder_push(rec,&large,1);assert(xtc_recorder_finish(rec)==XTC_OK);xtc_recorder_destroy(rec);
|
||||
assert(xtc_sample_load(edge,&q)==XTC_OK && xtc_sample_process(q,0,1,XTC_SAMPLE_NORMALIZE,1,&result)==XTC_ERR_CAPACITY && result==NULL);xtc_sample_destroy(q);assert(unlink(edge)==0);}
|
||||
xtc_sample_destroy(s);assert(unlink(path)==0 && rmdir(root)==0);puts("Sample editor core tests passed.");return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* Optional documentation tool: real FLTK widgets and real backend, isolated data. */
|
||||
#include "../src/gui/main_window.hpp"
|
||||
#include "../src/gui/sample_editor.hpp"
|
||||
#include "../src/gui/sequencer_widget.hpp"
|
||||
#include "../src/gui/mixer_widget.hpp"
|
||||
#include "../src/gui/piano_roll.hpp"
|
||||
|
|
@ -29,9 +30,10 @@ void pump() {auto end=std::chrono::steady_clock::now()+std::chrono::milliseconds
|
|||
template<class T> T *first(Fl_Group &g) {for(int i=0;i<g.children();++i){auto *c=g.child(i);if(auto *t=dynamic_cast<T *>(c))return t;if(auto *n=dynamic_cast<Fl_Group *>(c))if(auto *t=first<T>(*n))return t;}return nullptr;}
|
||||
Fl_Widget *named(Fl_Group &g,const char *label) {for(int i=0;i<g.children();++i){auto *c=g.child(i);if(c->label()&&!strcmp(c->label(),label))return c;if(auto *n=dynamic_cast<Fl_Group *>(c))if(auto *w=named(*n,label))return w;}return nullptr;}
|
||||
void tab(const char *label) {auto *page=named(*main_window,label);if(!page)throw std::runtime_error(label);auto *tabs=dynamic_cast<Fl_Tabs *>(page->parent());if(!tabs)throw std::runtime_error("Tab parent");tabs->value(page);tabs->redraw();pump();}
|
||||
void capture(const char *name) {Fl::flush();pump();std::string command="import -window root '"+destination+"/"+name+".png'";if(system(command.c_str()))throw std::runtime_error("ImageMagick capture failed");std::printf("Captured %s\n",name);std::fflush(stdout);}
|
||||
void capture(const char *name) {if(std::getenv("XTC_CAPTURE_SAMPLE_ONLY")&&std::strncmp(name,"sample-",7))return;Fl::flush();pump();std::string command="import -window root '"+destination+"/"+name+".png'";if(system(command.c_str()))throw std::runtime_error("ImageMagick capture failed");std::printf("Captured %s\n",name);std::fflush(stdout);}
|
||||
struct Modal {const char *name;const char *page;};
|
||||
void modal_capture(void *data) {auto *job=static_cast<Modal *>(data);auto *w=Fl::modal();if(!w||w==main_window)throw std::runtime_error("Expected a modal dialog");if(job->page){auto *tabs=first<Fl_Tabs>(*w);auto *page=named(*w,job->page);if(!tabs||!page)throw std::runtime_error(job->page);tabs->value(page);tabs->redraw();}capture(job->name);auto *cancel=named(*w,"Cancel");if(!cancel)cancel=named(*w,"Close");if(!cancel)cancel=named(*w,"OK");if(cancel)cancel->do_callback();else w->do_callback();}
|
||||
void modal_capture(void *data) {auto *job=static_cast<Modal *>(data);auto *w=Fl::modal();if(!w||w==main_window)throw std::runtime_error("Expected a modal dialog");if(auto *editor=dynamic_cast<SampleEditor *>(w)){editor->select(2400,std::min(size_t(12000),editor->frames()));if(!strcmp(job->name,"sample-transients")){editor->select(0,editor->frames());check(editor->detect_attacks(.05,2400));}}
|
||||
if(job->page){auto *tabs=first<Fl_Tabs>(*w);auto *page=named(*w,job->page);if(!tabs||!page)throw std::runtime_error(job->page);tabs->value(page);tabs->redraw();}capture(job->name);auto *cancel=named(*w,"Cancel");if(!cancel)cancel=named(*w,"Close");if(!cancel)cancel=named(*w,"OK");if(cancel)cancel->do_callback();else w->do_callback();}
|
||||
void dialog(const char *name,std::function<void()> action,const char *page=nullptr) {Modal job{name,page};Fl::add_timeout(.3,modal_capture,&job);action();Fl::remove_timeout(modal_capture,&job);pump();}
|
||||
void menu(const char *path) {auto *bar=first<Fl_Menu_Bar>(*main_window);const auto *item=bar->find_item(path);if(!item)throw std::runtime_error(path);bar->picked(item);}
|
||||
void piano_view() {
|
||||
|
|
@ -59,6 +61,12 @@ int main(int argc,char **argv) {
|
|||
MainWindow window(false,"");main_window=&window;window.resize(0,0,1400,900);window.show();check(window.open_project(project.c_str()));pump();
|
||||
auto *s=first<SequencerWidget>(window);if(!s)throw std::runtime_error("Sequencer missing");
|
||||
auto reset=[&](){window.reset_layout();check(window.open_project(project.c_str()));window.resize(0,20,1400,900);tab("Sequencer");s->select(0,0);s->position(0);check(window.set_loop(3840,7680,true));window.fit_song();pump();};
|
||||
if(std::getenv("XTC_CAPTURE_SAMPLE_ONLY")) {
|
||||
reset();char source[4096];if(!realpath("examples/projects/original-pluck.wav",source))throw std::runtime_error("Missing sample WAV");
|
||||
check(window.create_track(false));check(window.import_audio(source));
|
||||
dialog("sample-editor",[&]{check(window.open_sample_editor());});dialog("sample-transients",[&]{check(window.open_sample_editor());});
|
||||
window.do_callback();std::puts("Sample-editor captures completed.");return 0;
|
||||
}
|
||||
reset();capture("overview");capture("sequencer-loop");capture("arrangement-markers");
|
||||
s->position(4800);check(window.loop_section());capture("section-navigation");
|
||||
dialog("marker-edit",[&]{s->marker_dialog();});
|
||||
|
|
@ -82,6 +90,7 @@ int main(int argc,char **argv) {
|
|||
window.detach_transport(true);pump();capture("transport-detached");window.detach_mixer(true);pump();capture("workspace-detached");capture("mixer-detached");reset();
|
||||
window.detach_editor(0,true);pump();capture("sequencer-detached");reset();tab("Modular Graph");window.detach_editor(1,true);pump();capture("graph-detached");reset();
|
||||
char wave[4096];if(!realpath("examples/projects/original-pluck.wav",wave))throw std::runtime_error("Missing WAV");check(window.create_track(false));check(window.import_audio(wave));check(s->set_zoom(192));check(s->audio_properties(.8f,240,480));pump();capture("sequencer-audio-waveform");
|
||||
dialog("sample-editor",[&]{check(window.open_sample_editor());});dialog("sample-transients",[&]{check(window.open_sample_editor());});
|
||||
dialog("audio-clip-properties",[&]{s->audio_properties_dialog();});dialog("audio-normalization",[&]{s->normalize_dialog();});check(s->normalize_audio(.25f));capture("audio-normalization-result");
|
||||
dialog("project-collection",[&]{menu("File/Collect project media...");});check(window.collect_project((root+"/portable").c_str()));
|
||||
dialog("loop-range",[&]{button("Loop range...");});dialog("midi-controller-mapping",[&]{menu("Audio/MIDI controller mapping");});dialog("about",[&]{menu("Help/About");});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue