From 713a5dce7e6e23cde11b2a172db9fea1dbf5181c Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Thu, 9 Jul 2026 14:39:57 -0400 Subject: [PATCH] midi set list: also send time signature info on program change --- include/element/audioengine.hpp | 2 +- include/element/transport.hpp | 2 +- src/engine/audioengine.cpp | 4 +- src/engine/transport.cpp | 13 ++--- src/nodes/midisetlist.cpp | 33 ++++++++++- src/nodes/midisetlist.hpp | 12 +++- src/nodes/midisetlisteditor.cpp | 97 ++++++++++++++++++++++++++++++++- src/ui/content.cpp | 2 + src/ui/tempoandmeterbar.hpp | 19 +++++++ 9 files changed, 167 insertions(+), 17 deletions(-) diff --git a/include/element/audioengine.hpp b/include/element/audioengine.hpp index 97dac35fc..35d5f966c 100644 --- a/include/element/audioengine.hpp +++ b/include/element/audioengine.hpp @@ -59,7 +59,7 @@ class AudioEngine final : public juce::ReferenceCountedObject { void setPlaying (const bool shouldBePlaying); void setRecording (const bool shouldBeRecording); void seekToAudioFrame (const int64_t frame); - void setMeter (int beatsPerBar, int beatDivisor); + void setMeter (int beatsPerBar, int beatType); void togglePlayPause(); diff --git a/include/element/transport.hpp b/include/element/transport.hpp index 38f122c78..b5141c74f 100644 --- a/include/element/transport.hpp +++ b/include/element/transport.hpp @@ -120,7 +120,7 @@ class Transport : public Shuttle { /** Requests a time signature change (thread-safe). @param beatsPerBar Numerator of time signature - @param beatType Denominator of time signature + @param beatType Beat type enum */ void requestMeter (int beatsPerBar, int beatType); diff --git a/src/engine/audioengine.cpp b/src/engine/audioengine.cpp index c268ca47a..00e1ced9d 100644 --- a/src/engine/audioengine.cpp +++ b/src/engine/audioengine.cpp @@ -925,10 +925,10 @@ Transport::MonitorPtr AudioEngine::getTransportMonitor() const return (priv != nullptr) ? priv->transport.getMonitor() : nullptr; } -void AudioEngine::setMeter (int beatsPerBar, int beatDivisor) +void AudioEngine::setMeter (int beatsPerBar, int beatType) { auto& transport (priv->transport); - transport.requestMeter (beatsPerBar, beatDivisor); + transport.requestMeter (beatsPerBar, beatType); } void AudioEngine::togglePlayPause() diff --git a/src/engine/transport.cpp b/src/engine/transport.cpp index 770c636f1..50e012f71 100644 --- a/src/engine/transport.cpp +++ b/src/engine/transport.cpp @@ -138,19 +138,18 @@ void Transport::postProcess (int nframes) } } -void Transport::requestMeter (int beatsPerBar, int beatDivisor) +void Transport::requestMeter (int beatsPerBar, int beatType) { - // std::clog << "request meter: " << beatsPerBar << "/" << (1 << beatDivisor) << std::endl; if (beatsPerBar < 1) beatsPerBar = 1; if (beatsPerBar > 99) beatsPerBar = 99; - if (beatDivisor < 0) - beatDivisor = 0; - if (beatDivisor > BeatType::SixteenthNote) - beatDivisor = BeatType::SixteenthNote; + if (beatType < 0) + beatType = 0; + if (beatType > BeatType::SixteenthNote) + beatType = BeatType::SixteenthNote; nextBeatsPerBar.set (beatsPerBar); - nextBeatDivisor.set (beatDivisor); + nextBeatDivisor.set (beatType); } void Transport::requestAudioFrame (const int64_t frame) diff --git a/src/nodes/midisetlist.cpp b/src/nodes/midisetlist.cpp index b2d8fd3c4..5a43c4440 100644 --- a/src/nodes/midisetlist.cpp +++ b/src/nodes/midisetlist.cpp @@ -2,9 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include +#include #include "nodes/midisetlist.hpp" #include "engine/trace.hpp" +#include "tempo.hpp" using namespace juce; @@ -111,6 +113,16 @@ void MidiSetListProcessor::maybeSendTempoAndPosition (int program) { if (entry->tempo >= 20.0) s->getValueTree().setProperty (tags::tempo, entry->tempo, nullptr); + + if (entry->tsNum > 0 && entry->tsDen > 0) + { + const int div = BeatType::fromDivisor (entry->tsDen); // raw denom -> BeatType enum + s->getValueTree().setProperty (tags::beatsPerBar, entry->tsNum, nullptr); + s->getValueTree().setProperty (tags::beatDivisor, div, nullptr); + if (auto e = _context.audio()) + e->setMeter (entry->tsNum, div); + } + if (auto e = _context.audio()) e->seekToAudioFrame (0); } @@ -148,6 +160,8 @@ void MidiSetListProcessor::addProgramEntry (const String& name, int programIn, i entry->in = programIn; entry->out = programOut; entry->tempo = 0.0; + entry->tsNum = 0; + entry->tsDen = 0; sendChangeMessage(); ScopedLock sl (lock); @@ -158,15 +172,32 @@ void MidiSetListProcessor::editProgramEntry (int index, const String& name, int inProgram, int outProgram, - double tempo) + double tempo, + int tsNum, + int tsDen) { tempo = (tempo <= 0) ? 0.0 : std::max (20.0, std::min (999.0, tempo)); + + // A valid signature needs both a numerator and a power-of-two divisor; otherwise unset (0/0). + if (tsNum <= 0 || tsDen <= 0) + { + tsNum = tsDen = 0; + } + else + { + tsNum = jlimit (1, 99, tsNum); + // Snap the denominator to the nearest supported divisor via the BeatType enum. + tsDen = (int) BeatType ((BeatType::ID) BeatType::fromDivisor (tsDen)).divisor(); + } + if (auto* entry = entries[index]) { entry->name = name.isNotEmpty() ? name : entry->name; entry->in = inProgram; entry->out = outProgram; entry->tempo = tempo; + entry->tsNum = tsNum; + entry->tsDen = tsDen; ScopedLock sl (lock); programMap[entry->in] = entry->out; sendChangeMessage(); diff --git a/src/nodes/midisetlist.hpp b/src/nodes/midisetlist.hpp index 4d336f6ae..679c03ada 100644 --- a/src/nodes/midisetlist.hpp +++ b/src/nodes/midisetlist.hpp @@ -24,6 +24,8 @@ class MidiSetListProcessor : public MidiFilterNode, int in; int out; double tempo { 0.0 }; + int tsNum { 0 }; + int tsDen { 0 }; }; MidiSetListProcessor (Context&); @@ -59,7 +61,9 @@ class MidiSetListProcessor : public MidiFilterNode, const juce::String& name, int inProgram, int outProgram, - double tempo); + double tempo, + int tsNum = 0, + int tsDen = 0); ProgramEntry getProgramEntry (int index) const; inline int getWidth() const { return width; } @@ -103,6 +107,8 @@ class MidiSetListProcessor : public MidiFilterNode, entry->in = (int) e["in"]; entry->out = (int) e["out"]; entry->tempo = (double) e["tempo"]; + entry->tsNum = (int) e["tsNum"]; + entry->tsDen = (int) e["tsDen"]; } { @@ -127,7 +133,9 @@ class MidiSetListProcessor : public MidiFilterNode, e.setProperty ("name", entry->name, nullptr) .setProperty ("in", entry->in, nullptr) .setProperty ("out", entry->out, nullptr) - .setProperty ("tempo", entry->tempo, nullptr); + .setProperty ("tempo", entry->tempo, nullptr) + .setProperty ("tsNum", entry->tsNum, nullptr) + .setProperty ("tsDen", entry->tsDen, nullptr); tree.appendChild (e, nullptr); } diff --git a/src/nodes/midisetlisteditor.cpp b/src/nodes/midisetlisteditor.cpp index f6128fb45..bfd96daac 100644 --- a/src/nodes/midisetlisteditor.cpp +++ b/src/nodes/midisetlisteditor.cpp @@ -5,7 +5,9 @@ #include "nodes/midisetlist.hpp" #include "nodes/midisetlisteditor.hpp" +#include "ui/buttons.hpp" #include "ui/viewhelpers.hpp" +#include "tempo.hpp" namespace element { @@ -168,6 +170,69 @@ class MidiSetListProgramTempoLabel : public Label int row = -1; }; +class MidiSetListProgramSignatureWidget : public TimeSignatureSetting +{ +public: + MidiSetListProgramSignatureWidget (MidiSetListEditor& e) + : editor (e) {} + + ~MidiSetListProgramSignatureWidget() {} + + void setRow (int r) { row = r; } + + void setSelected (bool s) + { + if (selected != s) + { + selected = s; + repaint(); + } + } + + /** Populate the widget from a program entry, defaulting to 4/4 when unset. */ + void setSignature (int tsNum, int tsDen) + { + const int bpb = tsNum > 0 ? tsNum : 4; + const int div = tsDen > 0 ? BeatType::fromDivisor (tsDen) + : (int) BeatType::QuarterNote; + updateMeter (bpb, div, false); + } + + /** Render like the surrounding table cells (transparent, light text over the + row background) instead of the base class's boxed look. */ + void paint (Graphics& g) override + { + String text; + text << getBeatsPerBar() << " / " + << (int) BeatType ((BeatType::ID) getBeatDivisor()).divisor(); + g.setFont (Font (FontOptions (editor.getFontSize()))); + ViewHelpers::drawBasicTextRow (text, g, getWidth(), getHeight(), selected, 0, Justification::centred); + } + + void mouseDown (const MouseEvent& ev) override + { + editor.selectRow (row); + TimeSignatureSetting::mouseDown (ev); + } + +protected: + void meterChanged() override + { + if (row < 0) + return; + + auto program = editor.getProgram (row); + program.tsNum = getBeatsPerBar(); + program.tsDen = (int) BeatType ((BeatType::ID) getBeatDivisor()).divisor(); + editor.setProgram (row, program); + } + +private: + MidiSetListEditor& editor; + int row = -1; + bool selected = false; +}; + class MSLE::TableModel : public TableListBoxModel { public: @@ -177,6 +242,7 @@ class MSLE::TableModel : public TableListBoxModel InProgram = 1, Name, Tempo, + Signature, OutProgram }; @@ -210,7 +276,12 @@ class MSLE::TableModel : public TableListBoxModel text = String (1 + program.in); break; case TableModel::Tempo: - text = String (120.00, 2) + " bpm"; + text = program.tempo >= 20.0 ? String (program.tempo, 2) + " bpm" : "N/A"; + break; + case TableModel::Signature: + text = (program.tsNum > 0 && program.tsDen > 0) + ? String (program.tsNum) + "/" + String (program.tsDen) + : "N/A"; break; case TableModel::OutProgram: text = String (1 + program.out); @@ -266,6 +337,17 @@ class MSLE::TableModel : public TableListBoxModel label = t; break; } + + case TableModel::Signature: { + auto* sig = existing == nullptr + ? new MidiSetListProgramSignatureWidget (editor) + : dynamic_cast (existing); + sig->setSignature (program.tsNum, program.tsDen); + sig->setRow (rowNumber); + sig->setSelected (isRowSelected); + sig->repaint(); + return sig; + } } if (label == nullptr) @@ -304,6 +386,7 @@ MidiSetListEditor::MidiSetListEditor (const Node& node) header.addColumn ("IN", TableModel::InProgram, 50, 50, -1, flags, -1); header.addColumn ("NAME", TableModel::Name, 100, 100, -1, flags, -1); header.addColumn ("TEMPO", TableModel::Tempo, 70, 70, -1, flags, -1); + header.addColumn ("SIG", TableModel::Signature, 60, 60, -1, flags, -1); header.addColumn ("OUT", TableModel::OutProgram, 50, 50, -1, flags, -1); model.reset (new TableModel (*this)); table.setModel (model.get()); @@ -453,7 +536,9 @@ void MidiSetListEditor::setProgram (int index, MidiSetListProcessor::ProgramEntr entry.name, entry.in, entry.out, - entry.tempo); + entry.tempo, + entry.tsNum, + entry.tsDen); table.updateContent(); } } @@ -551,10 +636,16 @@ void MidiSetListEditor::updateTableHeaderSizes() const auto tempoSize = (int) glyphs.getBoundingBox (0, -1, true).getWidth() + 4; header.setColumnWidth (TableModel::Tempo, tempoSize); + GlyphArrangement sigGlyphs; + sigGlyphs.addLineOfText (Font (FontOptions (getFontSize())), "99 / 16", 0, 0); + const auto sigSize = (int) sigGlyphs.getBoundingBox (0, -1, true).getWidth() + 4; + header.setColumnWidth (TableModel::Signature, sigSize); + // clang-format on const auto fixedTotalSize = header.getColumnWidth (TableModel::InProgram) + header.getColumnWidth (TableModel::OutProgram) - + header.getColumnWidth (TableModel::Tempo); + + header.getColumnWidth (TableModel::Tempo) + + header.getColumnWidth (TableModel::Signature); header.setColumnWidth (TableModel::Name, table.getWidth() - fixedTotalSize); // clang-format off diff --git a/src/ui/content.cpp b/src/ui/content.cpp index 4cd591011..89b3521a4 100644 --- a/src/ui/content.cpp +++ b/src/ui/content.cpp @@ -120,6 +120,8 @@ class Content::Toolbar : public Component, tempoBar.setUseExtButton (showExt); tempoBar.getTempoValue().referTo (session->getPropertyAsValue (tags::tempo)); tempoBar.getExternalSyncValue().referTo (session->getPropertyAsValue (tags::externalSync)); + tempoBar.getBeatsPerBarValue().referTo (session->getPropertyAsValue (tags::beatsPerBar)); + tempoBar.getBeatDivisorValue().referTo (session->getPropertyAsValue (tags::beatDivisor)); tempoBar.stabilizeWithSession (false); } diff --git a/src/ui/tempoandmeterbar.hpp b/src/ui/tempoandmeterbar.hpp index 9887b5a99..b4c602ba4 100644 --- a/src/ui/tempoandmeterbar.hpp +++ b/src/ui/tempoandmeterbar.hpp @@ -25,6 +25,8 @@ class TempoAndMeterBar : public Component, tempoLabel.tempoValue.addListener (this); extButton.getToggleStateValue().addListener (this); + beatsPerBarValue.addListener (this); + beatDivisorValue.addListener (this); meter = std::make_unique (*this); addAndMakeVisible (meter.get()); @@ -34,10 +36,17 @@ class TempoAndMeterBar : public Component, ~TempoAndMeterBar() { + beatDivisorValue.removeListener (this); + beatsPerBarValue.removeListener (this); extButton.getToggleStateValue().removeListener (this); tempoLabel.tempoValue.removeListener (this); } + /** The meter numerator/divisor values, bound to the session so the widget + tracks external changes (e.g. from the MIDI Set List node). */ + Value& getBeatsPerBarValue() { return beatsPerBarValue; } + Value& getBeatDivisorValue() { return beatDivisorValue; } + void resized() override { auto r (getLocalBounds()); @@ -73,6 +82,14 @@ class TempoAndMeterBar : public Component, { stabilize(); + if (v.refersToSameSourceAs (beatsPerBarValue) || v.refersToSameSourceAs (beatDivisorValue)) + { + // notify=false: display-only sync, so we don't re-broadcast to engine/session. + meter->updateMeter ((int) beatsPerBarValue.getValue(), + (int) beatDivisorValue.getValue(), + false); + } + if (extButton.isVisible() && v.refersToSameSourceAs (extButton.getToggleStateValue())) { if (extButton.getToggleState()) @@ -160,6 +177,8 @@ class TempoAndMeterBar : public Component, Transport::MonitorPtr monitor; AudioEnginePtr engine; SessionPtr session; + Value beatsPerBarValue; + Value beatDivisorValue; void stabilize() {