feat: normalize selected audio clips to a bounded sample peak
This commit is contained in:
parent
ea0b4ca3da
commit
d676d74a11
10 changed files with 94 additions and 6 deletions
2
Makefile
2
Makefile
|
|
@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
|
|||
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
|
||||
LDFLAGS ?=
|
||||
|
||||
CORE_SOURCES := src/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/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)
|
||||
|
|
|
|||
|
|
@ -38,3 +38,7 @@ Edit/MIDI tools/Build chords accepts 1..8 unique nonzero semitone intervals, suc
|
|||
## Phase 66 — Offline Arpeggio Generation
|
||||
|
||||
Edit/MIDI tools/Generate arpeggio replaces source notes with a written pattern. Equal-onset notes on the same channel form a chord lasting until its longest note end. The pattern cycles sorted pitches upward or downward, with the selected step and gate. Each step inherits its chosen note velocity/channel. All source notes, including hidden trimmed notes, participate. This is offline generation with undo, not a live arpeggiator or tempo-synced MIDI effect.
|
||||
|
||||
## Phase 67 — Audio Peak Normalization
|
||||
|
||||
Edit/Audio tools/Normalize source peak accepts a target from -60 to 0 dBFS. The C API accepts a linear target above 0 and at most 1. Analysis uses the audible native-rate source slice and the same mono downmix as playback, before fades or mixer controls. It replaces clip gain without writing WAV data. Silence, missing/invalid media, MIDI selections or required gain above 4 reject the entire batch. This is sample-peak normalization, not true-peak limiting or LUFS loudness normalization. Analysis runs synchronously on the control thread.
|
||||
|
|
|
|||
|
|
@ -1632,12 +1632,12 @@ Implemented: bounded offline ascending/descending arpeggio generation with equal
|
|||
Analyze the audible source slice outside callbacks and adjust nondestructive clip gain to a requested sample-peak target; validate silence, missing media and gain limits.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [ ] Implement bounded C99 model/control APIs and accessible GUI commands.
|
||||
- [ ] Preserve transactional errors, history, compatibility and callback safety.
|
||||
- [ ] Verify the feature with automated model/GUI tests and document limitations.
|
||||
- [x] Implement bounded C99 model/control APIs and accessible GUI commands.
|
||||
- [x] Preserve transactional errors, history, compatibility and callback safety.
|
||||
- [x] Verify the feature with automated model/GUI tests and document limitations.
|
||||
|
||||
### Implementation status
|
||||
Planned.
|
||||
Implemented: native-rate audible-slice sample-peak analysis adjusts nondestructive clip gain atomically. GUI accepts dBFS targets. Core exact gain, missing-media rollback, silence/gain-limit checks and GUI normalization/undo tests pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ void xtc_waveform_destroy(xtc_waveform *waveform);
|
|||
xtc_result xtc_waveform_info(const xtc_waveform *waveform, unsigned int *rate, size_t *frames, size_t *bins);
|
||||
xtc_result xtc_waveform_peak(const xtc_waveform *waveform, size_t bin, float *minimum, float *maximum);
|
||||
|
||||
/* Control-thread sample-peak normalization of audible native-rate source slices
|
||||
* (same mono downmix as playback). target 0<peak<=1, replaces clip gain, ignores
|
||||
* fades/track mix. Gain >4, silence, missing/invalid media or MIDI reject the
|
||||
* entire unique nonempty selection. Reads media; no source writes. */
|
||||
xtc_result xtc_project_normalize_audio(xtc_project *project,const uint64_t *ids,size_t count,float target);
|
||||
/* Control-thread WAV import: PCM16/24/32 or float32, mono/stereo downmixed to
|
||||
* mono, 1..768000 Hz. Absolute canonical references, <=511 bytes.
|
||||
* Adds a timeline source -> output route if no timeline source exists.
|
||||
|
|
|
|||
29
src/audio/normalize.c
Normal file
29
src/audio/normalize.c
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#include "../project/project_internal.h"
|
||||
#include "wave_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
xtc_result xtc_project_normalize_audio(xtc_project *p,const uint64_t *ids,size_t count,float target)
|
||||
{
|
||||
xtc_project *copy=NULL;size_t i,j;xtc_result r;
|
||||
if(!p || !ids || !count || count>XTC_PROJECT_MAX_CLIPS || !isfinite(target) || target<=0 || target>1)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<count;++i)for(j=0;j<i;++j)if(ids[i]==ids[j])return XTC_ERR_INVALID_ARGUMENT;
|
||||
r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
for(i=0;r==XTC_OK && i<count;++i) {
|
||||
xtc_clip_info c;xtc_wave wave={0};uint64_t begin=0,end=0;float peak=0,gain;
|
||||
r=xtc_project_find_clip(copy,ids[i],&c);if(r!=XTC_OK)break;
|
||||
if(c.midi || !c.asset_path[0]){r=XTC_ERR_INVALID_STATE;break;}
|
||||
r=xtc_wave_load(c.asset_path,&wave);
|
||||
if(r==XTC_OK)r=xtc_project_tick_to_sample(copy,c.offset_tick,wave.rate,&begin);
|
||||
if(r==XTC_OK)r=xtc_project_tick_to_sample(copy,c.offset_tick+c.length_tick,wave.rate,&end);
|
||||
if(r==XTC_OK) {
|
||||
if(end>wave.frames)end=wave.frames;
|
||||
for(;begin<end;++begin){float value=fabsf(wave.samples[(size_t)begin]);if(value>peak)peak=value;}
|
||||
if(peak<=0)r=XTC_ERR_INVALID_STATE;
|
||||
else {gain=target/peak;if(!isfinite(gain) || gain>4)r=XTC_ERR_CAPACITY;else r=xtc_project_set_clip_audio(copy,c.id,gain,c.fade_in_tick,c.fade_out_tick);}
|
||||
}
|
||||
free(wave.samples);
|
||||
}
|
||||
if(r==XTC_OK){xtc_graph *old=p->graph;*p=*copy;copy->graph=old;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
@ -329,6 +329,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("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_);
|
||||
menu->add("Edit/MIDI tools/Generate arpeggio...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->arpeggio_dialog();},sequencer_);
|
||||
|
|
@ -1591,6 +1592,7 @@ xtc_result MainWindow::import_audio(const char *path)
|
|||
}
|
||||
project_.reset(candidate);
|
||||
sequencer_->set_project(candidate, false);
|
||||
sequencer_->select(track,clip);
|
||||
graph_widget_->set_graph(xtc_project_graph(candidate));
|
||||
mark_dirty();
|
||||
refresh_status();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include "sequencer_widget.hpp"
|
||||
#include "theme.hpp"
|
||||
|
|
@ -630,6 +631,19 @@ xtc_result SequencerWidget::arpeggiate(uint64_t step,unsigned int gate,bool desc
|
|||
if(r==XTC_OK)r=xtc_project_arpeggiate(project_,ids.data(),ids.size(),step,gate,descending);
|
||||
changed(r);return r;
|
||||
}
|
||||
xtc_result SequencerWidget::normalize_audio(float target)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_normalize_audio(project_,ids.data(),ids.size(),target);
|
||||
changed(r);return r;
|
||||
}
|
||||
void SequencerWidget::normalize_dialog()
|
||||
{
|
||||
const char *input=fl_input("Source sample peak target in dBFS (-60..0; excludes fades and mixer)","-1");if(!input)return;
|
||||
float db=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>db) || (values>>extra) || !std::isfinite(db) || db < -60 || db>0){fl_alert("Enter a finite sample peak target from -60 to 0 dBFS.");return;}
|
||||
normalize_audio(std::pow(10.0f,db/20.0f));
|
||||
}
|
||||
void SequencerWidget::arpeggio_dialog()
|
||||
{
|
||||
const char *input=fl_input("Step ticks (1..3840), gate percent (1..100), direction (0=up, 1=down)","240 80 0");if(!input)return;
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ class SequencerWidget : public Fl_Group {
|
|||
void chord_dialog();
|
||||
xtc_result arpeggiate(uint64_t step,unsigned int gate,bool descending);
|
||||
void arpeggio_dialog();
|
||||
xtc_result normalize_audio(float target);
|
||||
void normalize_dialog();
|
||||
void fades_dialog();
|
||||
xtc_result repeat_selection(unsigned int copies);
|
||||
void repeat_dialog();
|
||||
|
|
|
|||
|
|
@ -1392,6 +1392,10 @@ int main(int argc, char **argv)
|
|||
size_t notes=0;xtc_project_note_count(s->project(),¬es);CHECK(notes==3);
|
||||
CHECK(s->arpeggiate(80,50,false)==XTC_OK);xtc_project_note_count(s->project(),¬es);CHECK(notes==3);
|
||||
CHECK(window.undo()==XTC_OK);xtc_project_note_count(s->project(),¬es);CHECK(notes==3);
|
||||
char sample[4096];CHECK(realpath("examples/projects/original-pluck.wav",sample)!=nullptr);
|
||||
CHECK(window.import_audio(sample)==XTC_OK);CHECK(s->normalize_audio(.25f)==XTC_OK);
|
||||
xtc_clip_info normalized{};xtc_project_find_clip(s->project(),s->selected_clip(),&normalized);CHECK(normalized.gain>0 && normalized.gain<=4);
|
||||
CHECK(window.undo()==XTC_OK);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <math.h>
|
||||
|
||||
static void put32(unsigned char *p,uint32_t value){p[0]=(unsigned char)value;p[1]=(unsigned char)(value>>8);p[2]=(unsigned char)(value>>16);p[3]=(unsigned char)(value>>24);}
|
||||
static void test_wave(const char *path,float amplitude)
|
||||
{
|
||||
unsigned char h[44]={0},bytes[4];FILE *f=fopen(path,"wb");assert(f);
|
||||
memcpy(h,"RIFF",4);put32(h+4,52);memcpy(h+8,"WAVEfmt ",8);put32(h+16,16);h[20]=3;h[22]=1;put32(h+24,1000);put32(h+28,4000);h[32]=4;h[34]=32;memcpy(h+36,"data",4);put32(h+40,16);
|
||||
assert(fwrite(h,1,44,f)==44);for(size_t i=0;i<4;++i){uint32_t bits;float v=i%2 ? -amplitude*.5f : amplitude;memcpy(&bits,&v,4);put32(bytes,bits);assert(fwrite(bytes,1,4,f)==4);}assert(fclose(f)==0);
|
||||
}
|
||||
static void markers(void)
|
||||
{
|
||||
xtc_project *p=NULL,*q=NULL;xtc_marker m;size_t count;char path[]="/tmp/xtc-sections-XXXXXX";
|
||||
|
|
@ -163,4 +171,24 @@ static void arpeggio(void)
|
|||
assert(xtc_project_arpeggiate(p,&c,1,1,100,0)==XTC_ERR_CAPACITY);
|
||||
assert(xtc_project_note_count(p,&count)==XTC_OK && count==1);xtc_project_destroy(p);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();humanize();chords();arpeggio();puts("Next DAW workflow tests passed.");return 0;}
|
||||
static void normalize(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t t,ids[2];xtc_clip_info c;char dir[]="/tmp/xtc-normal-XXXXXX",path[512];
|
||||
assert(mkdtemp(dir));snprintf(path,sizeof(path),"%s/source.wav",dir);test_wave(path,.5f);
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
|
||||
assert(xtc_project_import_audio(p,t,path,0,&ids[0])==XTC_OK);
|
||||
assert(xtc_project_normalize_audio(p,ids,1,.25f)==XTC_OK);
|
||||
assert(xtc_project_find_clip(p,ids[0],&c)==XTC_OK && c.gain==.5f);
|
||||
assert(xtc_project_edit_clip(p,ids[0],t,0,2,2)==XTC_OK);
|
||||
assert(xtc_project_normalize_audio(p,ids,1,.25f)==XTC_OK);
|
||||
assert(xtc_project_find_clip(p,ids[0],&c)==XTC_OK && c.gain==1);
|
||||
assert(xtc_project_edit_clip(p,ids[0],t,0,8,0)==XTC_OK && xtc_project_set_clip_audio(p,ids[0],.5f,0,0)==XTC_OK);
|
||||
assert(xtc_project_add_clip(p,t,"Missing",100,960,&ids[1])==XTC_OK && xtc_project_set_clip_asset(p,ids[1],"/nonexistent/xtc.wav")==XTC_OK);
|
||||
assert(xtc_project_normalize_audio(p,ids,2,1)!=XTC_OK);
|
||||
assert(xtc_project_find_clip(p,ids[0],&c)==XTC_OK && c.gain==.5f);
|
||||
test_wave(path,.001f);assert(xtc_project_normalize_audio(p,ids,1,1)==XTC_ERR_CAPACITY);
|
||||
test_wave(path,0);assert(xtc_project_normalize_audio(p,ids,1,1)==XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_normalize_audio(p,ids,1,NAN)==XTC_ERR_INVALID_ARGUMENT);
|
||||
xtc_project_destroy(p);assert(unlink(path)==0 && rmdir(dir)==0);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();humanize();chords();arpeggio();normalize();puts("Next DAW workflow tests passed.");return 0;}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue