feat(metering): show live stereo peaks beside track inserts (phase 96)
This commit is contained in:
parent
924821738d
commit
7ad85b6144
6 changed files with 75 additions and 6 deletions
|
|
@ -52,3 +52,12 @@ one undoable edit. Enabling all intentionally enables every slot, including
|
||||||
previously bypassed effects; Undo restores the previous mixed bypass state.
|
previously bypassed effects; Undo restores the previous mixed bypass state.
|
||||||
Clear removes slots while preserving chain output gain. No plugin resources
|
Clear removes slots while preserving chain output gain. No plugin resources
|
||||||
are released in the audio callback. Empty racks return a no-result error.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
### Tasks and acceptance criteria
|
||||||
- [ ] Implement the workflow with bounded validation, safe preparation and documented limits.
|
- [x] Implement the workflow with bounded validation, safe preparation and documented limits.
|
||||||
- [ ] Pass meaningful core and/or GUI checks, including error and undo behavior.
|
- [x] Pass meaningful core and/or GUI checks, including error and undo behavior.
|
||||||
- [ ] Document results and commit this phase separately.
|
- [x] Document results and commit this phase separately.
|
||||||
|
|
||||||
### Implementation status
|
### 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
45
src/gui/insert_meter.hpp
Normal 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
|
||||||
|
|
@ -1012,6 +1012,7 @@ void MainWindow::refresh_status()
|
||||||
refresh_workspace();
|
refresh_workspace();
|
||||||
update_history_controls();
|
update_history_controls();
|
||||||
if (mixer_ && !ready_) mixer_->meters(xtc_meter_snapshot{});
|
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;
|
bool can_play = false, can_record = false;
|
||||||
if (!ready_) {
|
if (!ready_) {
|
||||||
set_label(indicator_, "Audio: disconnected");
|
set_label(indicator_, "Audio: disconnected");
|
||||||
|
|
@ -1024,6 +1025,7 @@ void MainWindow::refresh_status()
|
||||||
xtc_result result = xtc_backend_get_status(backend_, &status);
|
xtc_result result = xtc_backend_get_status(backend_, &status);
|
||||||
if (result != XTC_OK) {
|
if (result != XTC_OK) {
|
||||||
report_error("Read engine status failed", result);
|
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(indicator_, "Audio: status unavailable");
|
||||||
set_label(graph_state_, "Graph: engine status unavailable");
|
set_label(graph_state_, "Graph: engine status unavailable");
|
||||||
set_label(status_, "Unable to read JACK status — use Reconnect");
|
set_label(status_, "Unable to read JACK status — use Reconnect");
|
||||||
|
|
@ -1050,6 +1052,7 @@ void MainWindow::refresh_status()
|
||||||
xtc_meter_snapshot meters{};
|
xtc_meter_snapshot meters{};
|
||||||
if (xtc_backend_read_meters(backend_, &meters) == XTC_OK) mixer_->meters(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); }
|
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];
|
char text[256];
|
||||||
std::snprintf(
|
std::snprintf(
|
||||||
text, sizeof(text),
|
text, sizeof(text),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "plugin_library.hpp"
|
#include "plugin_library.hpp"
|
||||||
#include "plugin_dialog.hpp"
|
#include "plugin_dialog.hpp"
|
||||||
#include "sequencer_widget.hpp"
|
#include "sequencer_widget.hpp"
|
||||||
|
#include "insert_meter.hpp"
|
||||||
#include <FL/Fl_Choice.H>
|
#include <FL/Fl_Choice.H>
|
||||||
#include <FL/Fl_Menu_Button.H>
|
#include <FL/Fl_Menu_Button.H>
|
||||||
#include <FL/Fl_Hold_Browser.H>
|
#include <FL/Fl_Hold_Browser.H>
|
||||||
|
|
@ -18,13 +19,14 @@ class TrackInsertsPanel : public Fl_Group {
|
||||||
MainWindow *owner_;
|
MainWindow *owner_;
|
||||||
Fl_Choice *tracks_;
|
Fl_Choice *tracks_;
|
||||||
Fl_Menu_Button *actions_;
|
Fl_Menu_Button *actions_;
|
||||||
|
InsertMeter *meter_;
|
||||||
Fl_Hold_Browser *slots_;
|
Fl_Hold_Browser *slots_;
|
||||||
Fl_Box *message_;
|
Fl_Box *message_;
|
||||||
Fl_Value_Input *wet_,*gain_;
|
Fl_Value_Input *wet_,*gain_;
|
||||||
GraphWidget *graph_;
|
GraphWidget *graph_;
|
||||||
Fl_Scroll *scroll_;
|
Fl_Scroll *scroll_;
|
||||||
xtc_graph *model_=nullptr;
|
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 cached_{};
|
||||||
xtc_track_chain clipboard_{};
|
xtc_track_chain clipboard_{};
|
||||||
bool clipboard_ready_=false;
|
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("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("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);
|
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);
|
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);
|
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);
|
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);
|
if(r==XTC_OK)r=owner_->set_track_chain(track_,chain);
|
||||||
return r;
|
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 clipboard_ready() const {return clipboard_ready_;}
|
||||||
bool comparison_ready(unsigned int slot) const {return slot<2 && comparisons_[slot].ready && comparisons_[slot].track==track_;}
|
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) {
|
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];
|
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));
|
for(size_t i=0;i<ids_.size();++i)if(ids_[i]==track_)tracks_->value(static_cast<int>(i));
|
||||||
update_actions();
|
update_actions();
|
||||||
|
if(track_!=meter_track_){meter_track_=track_;meters(xtc_meter_snapshot{},false);}
|
||||||
xtc_track_chain chain{};chain.output_gain=1;
|
xtc_track_chain chain{};chain.output_gain=1;
|
||||||
if(track_!=UINT64_MAX)xtc_project_get_chain(project,track_,&chain);
|
if(track_!=UINT64_MAX)xtc_project_get_chain(project,track_,&chain);
|
||||||
if(!rebuild && !std::memcmp(&cached_,&chain,sizeof(chain)))return;
|
if(!rebuild && !std::memcmp(&cached_,&chain,sizeof(chain)))return;
|
||||||
|
|
|
||||||
|
|
@ -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(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(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);
|
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(window.new_project()==XTC_OK && !panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND);
|
||||||
CHECK(!panel->comparison_ready(0) && !panel->comparison_ready(1));
|
CHECK(!panel->comparison_ready(0) && !panel->comparison_ready(1));
|
||||||
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;
|
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue