feat(metering): show live stereo peaks beside track inserts (phase 96)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:08:13 +01:00
commit 7ad85b6144
6 changed files with 75 additions and 6 deletions

View file

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

View file

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

45
src/gui/insert_meter.hpp Normal file
View file

@ -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 <cstdio>
#include <cmath>
#include <algorithm>
/* 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<InsertMeter *>(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<snapshot.count && i<XTC_PROJECT_MAX_TRACKS;++i)if(snapshot.tracks[i].id==track){l=snapshot.tracks[i].left;r=snapshot.tracks[i].right;found=true;break;}
if(!active || !found){left_.db=right_.db=-90;}
else {
(void)xtc_peak_meter_update(&left_,std::isfinite(l) && l>=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<double>(left_.db),static_cast<double>(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<int>(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<int>(static_cast<float>(width)*std::min(1.f,std::max(0.f,(db+90)/90))),4);
}
}
};
#endif

View file

@ -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<double>(settings_.refresh_ms)/1000);
char text[256];
std::snprintf(
text, sizeof(text),

View file

@ -5,6 +5,7 @@
#include "plugin_library.hpp"
#include "plugin_dialog.hpp"
#include "sequencer_widget.hpp"
#include "insert_meter.hpp"
#include <FL/Fl_Choice.H>
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl_Hold_Browser.H>
@ -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<TrackInsertsPanel *>(c);p->result(p->copy_chain());},this);
actions_->add("Paste chain (replace)",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->paste_chain());},this,FL_MENU_INACTIVE);
actions_->add("Duplicate effect",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(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<TrackInsertsPanel *>(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;i<ids_.size();++i)if(ids_[i]==track_)tracks_->value(static_cast<int>(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;

View file

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