From 7ad85b61444e8a6c7719653c3104fc4fa0e59bb7 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 02:08:13 +0100 Subject: [PATCH] feat(metering): show live stereo peaks beside track inserts (phase 96) --- docs/CREATIVE_WORKFLOWS.md | 9 +++++++ docs/ROADMAP.md | 8 +++--- src/gui/insert_meter.hpp | 45 ++++++++++++++++++++++++++++++++++ src/gui/main_window.cpp | 3 +++ src/gui/track_inserts.hpp | 10 ++++++-- tests/integration/test_gui.cpp | 6 +++++ 6 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 src/gui/insert_meter.hpp diff --git a/docs/CREATIVE_WORKFLOWS.md b/docs/CREATIVE_WORKFLOWS.md index 232954e..2b08928 100644 --- a/docs/CREATIVE_WORKFLOWS.md +++ b/docs/CREATIVE_WORKFLOWS.md @@ -52,3 +52,12 @@ one undoable edit. Enabling all intentionally enables every slot, including previously bypassed effects; Undo restores the previous mixed bypass state. Clear removes slots while preserving chain output gain. No plugin resources are released in the audio callback. Empty racks return a no-result error. + +## Meter while editing effects + +The insert workspace shows the selected track's stereo sample peaks after its +fader and before its bus, including the effects' output. Green bars release at +20 dB/s. A sticky red CLIP appears at unity or above; click the display to clear +it. Stopped, offline or missing-track readings show the -90 dBFS floor. Changing +tracks clears the indicator. These are sample peaks, not true-peak or loudness +measurements. The display shares the mixer's existing backend snapshot read. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1144c78..c45630c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2148,12 +2148,12 @@ Implemented: bounded C99 enable-all, bypass-all and clear transforms preserve ou Display selected-track stereo sample peaks and a resettable clip indicator from the existing single backend meter reader, with clear post-fader labeling and offline/stopped handling. ### Tasks and acceptance criteria -- [ ] Implement the workflow with bounded validation, safe preparation and documented limits. -- [ ] Pass meaningful core and/or GUI checks, including error and undo behavior. -- [ ] Document results and commit this phase separately. +- [x] Implement the workflow with bounded validation, safe preparation and documented limits. +- [x] Pass meaningful core and/or GUI checks, including error and undo behavior. +- [x] Document results and commit this phase separately. ### Implementation status -Planned. +Implemented: UI-only stereo peak display in the insert workspace shares the existing backend meter snapshot, with post-fader/pre-bus labels, 20 dB/s release and click-to-clear sticky clipping. Stopped/missing/offline readings show the floor and track changes reset history. GUI dB conversion, clipping, stopped/reset and selection checks passed; no extra backend reader was added. --- diff --git a/src/gui/insert_meter.hpp b/src/gui/insert_meter.hpp new file mode 100644 index 0000000..01d3c4b --- /dev/null +++ b/src/gui/insert_meter.hpp @@ -0,0 +1,45 @@ +#ifndef XTC_INSERT_METER_HPP +#define XTC_INSERT_METER_HPP +#include "xtc/xtc_backend.h" +#include "xtc/xtc_mixer.h" +#include "theme.hpp" +#include +#include +#include +/* UI-only display fed by the existing single backend snapshot reader. */ +class InsertMeter : public Fl_Button { + xtc_peak_meter left_{-90,0},right_{-90,0}; + uint64_t track_=UINT64_MAX; + public: + InsertMeter(int x,int y,int w,int h):Fl_Button(x,y,w,h) { + tooltip("Post-track-fader, pre-bus stereo sample peaks. Click to clear the sticky clip indicator."); + callback([](Fl_Widget *w,void *){static_cast(w)->clear_clips();}); + } + void clear_clips(){left_.clipped=right_.clipped=0;redraw();} + float left_db() const {return left_.db;} + float right_db() const {return right_.db;} + bool clipped() const {return left_.clipped || right_.clipped;} + void feed(const xtc_meter_snapshot &snapshot,uint64_t track,bool active,double seconds) { + if(track_!=track){track_=track;xtc_peak_meter_reset(&left_);xtc_peak_meter_reset(&right_);} + bool found=false;float l=0,r=0; + for(size_t i=0;i=0 ? l : 0,seconds); + (void)xtc_peak_meter_update(&right_,std::isfinite(r) && r>=0 ? r : 0,seconds); + } + redraw(); + } + void draw() override { + draw_box();char text[120];std::snprintf(text,sizeof(text),"Post-fader L %.1f R %.1f dBFS%s",static_cast(left_.db),static_cast(right_.db),clipped() ? " CLIP" : ""); + fl_font(FL_HELVETICA,11);fl_color(clipped() ? fl_rgb_color(245,132,142) : xtc_ui::muted());fl_draw(text,x()+8,y()+12); + int width=std::max(0,(w()-24)/2); + for(unsigned int side=0;side<2;++side) { + int left=x()+8+static_cast(side)*(width+8);float db=side ? right_.db : left_.db; + fl_color(xtc_ui::background());fl_rectf(left,y()+h()-7,width,4); + fl_color(clipped() ? fl_rgb_color(245,132,142) : xtc_ui::accent()); + fl_rectf(left,y()+h()-7,static_cast(static_cast(width)*std::min(1.f,std::max(0.f,(db+90)/90))),4); + } + } +}; +#endif diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 4161f5b..f78f287 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -1012,6 +1012,7 @@ void MainWindow::refresh_status() refresh_workspace(); update_history_controls(); if (mixer_ && !ready_) mixer_->meters(xtc_meter_snapshot{}); + if(track_inserts_ && !ready_)track_inserts_->meters(xtc_meter_snapshot{},false); bool can_play = false, can_record = false; if (!ready_) { set_label(indicator_, "Audio: disconnected"); @@ -1024,6 +1025,7 @@ void MainWindow::refresh_status() xtc_result result = xtc_backend_get_status(backend_, &status); if (result != XTC_OK) { report_error("Read engine status failed", result); + if(track_inserts_)track_inserts_->meters(xtc_meter_snapshot{},false); set_label(indicator_, "Audio: status unavailable"); set_label(graph_state_, "Graph: engine status unavailable"); set_label(status_, "Unable to read JACK status — use Reconnect"); @@ -1050,6 +1052,7 @@ void MainWindow::refresh_status() xtc_meter_snapshot meters{}; if (xtc_backend_read_meters(backend_, &meters) == XTC_OK) mixer_->meters(meters); else { meters.master_left=status.peak_left;meters.master_right=status.peak_right;mixer_->meters(meters); } + if(track_inserts_)track_inserts_->meters(meters,running_ && !fault,static_cast(settings_.refresh_ms)/1000); char text[256]; std::snprintf( text, sizeof(text), diff --git a/src/gui/track_inserts.hpp b/src/gui/track_inserts.hpp index c6e2c5a..29b3736 100644 --- a/src/gui/track_inserts.hpp +++ b/src/gui/track_inserts.hpp @@ -5,6 +5,7 @@ #include "plugin_library.hpp" #include "plugin_dialog.hpp" #include "sequencer_widget.hpp" +#include "insert_meter.hpp" #include #include #include @@ -18,13 +19,14 @@ class TrackInsertsPanel : public Fl_Group { MainWindow *owner_; Fl_Choice *tracks_; Fl_Menu_Button *actions_; + InsertMeter *meter_; Fl_Hold_Browser *slots_; Fl_Box *message_; Fl_Value_Input *wet_,*gain_; GraphWidget *graph_; Fl_Scroll *scroll_; xtc_graph *model_=nullptr; - uint64_t track_=UINT64_MAX; + uint64_t track_=UINT64_MAX,meter_track_=UINT64_MAX; xtc_track_chain cached_{}; xtc_track_chain clipboard_{}; bool clipboard_ready_=false; @@ -68,6 +70,7 @@ class TrackInsertsPanel : public Fl_Group { actions_->add("Copy chain",0,[](Fl_Widget *,void *c){auto *p=static_cast(c);p->result(p->copy_chain());},this); actions_->add("Paste chain (replace)",0,[](Fl_Widget *,void *c){auto *p=static_cast(c);p->result(p->paste_chain());},this,FL_MENU_INACTIVE); actions_->add("Duplicate effect",0,[](Fl_Widget *,void *c){auto *p=static_cast(c);p->result(p->duplicate_slot());},this); + meter_=new InsertMeter(x+650,y+52,w-666,26); message_=new Fl_Box(x+16,y+84,w-32,30,"Track input > inserts > output gain > track fader > bus/master");message_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE); slots_=new Fl_Hold_Browser(x+16,y+126,230,h-142);slots_->format_char(0);slots_->callback([](Fl_Widget *,void *c){static_cast(c)->update_mix();},this); scroll_=new Fl_Scroll(x+260,y+126,w-276,h-142); @@ -91,7 +94,9 @@ class TrackInsertsPanel : public Fl_Group { if(r==XTC_OK)r=owner_->set_track_chain(track_,chain); return r; } - void clear_session() {clipboard_={};clipboard_ready_=false;for(auto &slot:comparisons_)slot=Comparison{};update_actions();} + InsertMeter *meter() const {return meter_;} + void meters(const xtc_meter_snapshot &snapshot,bool active,double seconds=.02) {meter_->feed(snapshot,track_,active,seconds);} + void clear_session() {meter_->feed(xtc_meter_snapshot{},UINT64_MAX,false,.02);clipboard_={};clipboard_ready_=false;for(auto &slot:comparisons_)slot=Comparison{};update_actions();} bool clipboard_ready() const {return clipboard_ready_;} bool comparison_ready(unsigned int slot) const {return slot<2 && comparisons_[slot].ready && comparisons_[slot].track==track_;} xtc_result comparison(unsigned int slot,bool capture) { @@ -184,6 +189,7 @@ class TrackInsertsPanel : public Fl_Group { if(std::find(ids_.begin(),ids_.end(),track_)==ids_.end())track_=ids_.empty() ? UINT64_MAX : ids_[0]; for(size_t i=0;ivalue(static_cast(i)); update_actions(); + if(track_!=meter_track_){meter_track_=track_;meters(xtc_meter_snapshot{},false);} xtc_track_chain chain{};chain.output_gain=1; if(track_!=UINT64_MAX)xtc_project_get_chain(project,track_,&chain); if(!rebuild && !std::memcmp(&cached_,&chain,sizeof(chain)))return; diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index a62b6ad..eaf066b 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -256,6 +256,12 @@ int creative_workflow_tests() CHECK(panel->bulk(XTC_CHAIN_ENABLE_ALL)==XTC_OK && panel->bulk(XTC_CHAIN_CLEAR)==XTC_OK); CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==0 && chain.output_gain==.8f); CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==2); + xtc_meter_snapshot peaks{};peaks.count=1;peaks.tracks[0]={second_track,.5f,.25f};panel->meters(peaks,true); + CHECK(std::fabs(panel->meter()->left_db()+6.0206f)<.001f && std::fabs(panel->meter()->right_db()+12.0412f)<.001f); + peaks.tracks[0].left=1.2f;panel->meters(peaks,true);CHECK(panel->meter()->clipped()); + panel->meters(peaks,false);CHECK(panel->meter()->left_db()==-90 && panel->meter()->clipped()); + panel->meter()->do_callback();CHECK(!panel->meter()->clipped()); + panel->select_track(first_track);CHECK(panel->meter()->left_db()==-90 && !panel->meter()->clipped()); CHECK(window.new_project()==XTC_OK && !panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND); CHECK(!panel->comparison_ready(0) && !panel->comparison_ready(1)); window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;