Skip to content

Commit d8c7e67

Browse files
committed
Keep a round every millisecond minimised or not; quiet the outputs before STOP waits on the driver; let an outage let go of the rig; drop presses made while stopped
- The output thread's rounds keep to a 1 ms grid on a high-resolution waitable timer. Measured on the exe: 1016 to 1034 wake-ups a second with the window normal, minimised and covered, where 1.3.2 made 684 to 689 (every wait Windows offers ends a timer tick late, and a wait timed from the end of a round added that tick to every round). And the process opts out of Windows' power throttling at startup, both kinds, so Windows 11 keeps its timer requests and its speed while the window is minimised or covered. The console does the same. - STOP posts to the outputs before it stops the tracker, and quitting does too: the tracker's stop waits on the driver, and the outputs went on firing the beats they predicted meanwhile. - An input outage tells the outputs, which let go of the rig as the input going quiet does: the lasers disarmed, every held note and clip released, held fires dropped, nothing predicted from the last beats, Link snapped again on the next lock. An unplugged interface sends no audio at all, so the tracker never heard a silence, and the lasers held their last clip through it. - ÷2, ×2, a tap, a pin or DOWNBEAT pressed on a control surface while stopped is dropped at START: START applied them to the run that had ended, and a tap moved the next run's BPM window. A setting is never refused by a queue full of presses — the oldest press gives way — so the latency and window sliders always reach the tracker. The console's --hold-bpm is posted after its tracker starts, where START no longer lets go of it. - RtMidi's errors go to a callback and are thrown after the call as before, instead of being written to stderr — takt4.log beside the executable, unbuffered — on the output thread every round while a pulled-out device failed. - Rule sets, patches and previews posted back to back are applied once, as the newest (a drag in the rule editor posts the whole set each step), a snapshot is taken only by a command that changes what it holds, and a flood of presses past 1024 waiting is refused and counted. - The input's supervision — the watchdog, ASIO reset and rate events — also runs from a Windows timer that a file dialog, the system menu or a message box goes on serving, where Slint's timers stop: a rate change met while IMPORT's dialog was open waited until it closed.
1 parent 3e78ab3 commit d8c7e67

24 files changed

Lines changed: 945 additions & 40 deletions

‎src/cli/main.cpp‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "core/output/output_runner.hpp"
3232
#include "core/output/transports.hpp"
3333
#include "core/rt/alloc_guard.hpp"
34+
#include "core/rt/thread_priority.hpp"
3435
#include "core/tracking/beat_decoder.hpp"
3536
#include "core/tracking/forward_filter.hpp"
3637
#include "core/tracking/particle_filter.hpp"
@@ -1202,9 +1203,6 @@ int runTrackDevice(const TrackArgs& args, const takt4::model::ModelWeights& weig
12021203
: takt4::audio::ChannelSelection::pair(args.beats.stream.pair->first,
12031204
args.beats.stream.pair->second);
12041205
takt4::engine::BeatEngine* const engine = &tracker.engine();
1205-
if (args.holdBpm > 0.0) {
1206-
(void)engine->post(takt4::engine::Command::holdTempo(args.holdBpm));
1207-
}
12081206

12091207
// **The declaration order from here down is the destruction order reversed, and all of
12101208
// it is load-bearing.** Everything below runs to completion in the ordinary path; what
@@ -1232,6 +1230,11 @@ int runTrackDevice(const TrackArgs& args, const takt4::model::ModelWeights& weig
12321230
// noticed it. `LiveTracker::start` installs it, and resets its filter, before the stream.
12331231
tracker.setHostTimeSource(&runner.hostTimeClock());
12341232
tracker.start(device, selection);
1233+
// The tempo hold once the run has started: posted before, it was applied to the run START
1234+
// ends (`BeatEngine::start` stops first) and let go of by the reset that begins the new one.
1235+
if (args.holdBpm > 0.0) {
1236+
(void)engine->post(takt4::engine::Command::holdTempo(args.holdBpm));
1237+
}
12351238
const takt4::audio::InputStream& stream = *tracker.stream();
12361239
FrameTracer tracer;
12371240
if (args.traceOut) {
@@ -1505,6 +1508,9 @@ int main(int argc, char** argv) {
15051508
#if defined(_WIN32)
15061509
SetConsoleOutputCP(CP_UTF8);
15071510
#endif
1511+
// A console behind other windows is throttled as a minimised one is — see
1512+
// `rt::keepFullSpeed`. `track` drives the outputs from here.
1513+
(void)takt4::rt::keepFullSpeed();
15081514
std::vector<std::string_view> args(argv + 1, argv + argc);
15091515
if (args.empty() || args[0] == "--help" || args[0] == "-h") {
15101516
printUsage(std::cout);

‎src/core/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ add_library(takt4_core STATIC
171171
output/output_target.hpp
172172
output/output_runner.cpp
173173
output/output_runner.hpp
174+
output/rtmidi_errors.hpp
174175
output/rule_sink.cpp
175176
output/rule_sink.hpp
176177
output/transports.cpp

‎src/core/control/midi_control.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "core/control/midi_control.hpp"
22

33
#include "core/output/midi_ports.hpp"
4+
#include "core/output/rtmidi_errors.hpp"
45
#include "core/sandbox.hpp"
56

67
#include <RtMidi.h>
@@ -50,6 +51,8 @@ bool isRelease(const MidiEvent& event) noexcept {
5051
} // namespace
5152

5253
struct MidiControl::Impl {
54+
/// Before `in`, which reports to it and so must go first.
55+
output::RtMidiErrors errors;
5356
RtMidiIn in;
5457
};
5558

@@ -77,11 +80,13 @@ void MidiControl::start() {
7780
error.getMessage() + ")");
7881
}
7982

83+
impl->errors.watch(impl->in);
8084
unsigned int index = 0;
8185
std::string name;
8286
try {
8387
index = findPort(impl->in, config_.port); // output::MidiPortMissing: not here
8488
name = impl->in.getPortName(index);
89+
impl->errors.raise();
8590
} catch (const RtMidiError& error) {
8691
throw std::runtime_error("MIDI control: " + error.getMessage());
8792
}
@@ -92,6 +97,7 @@ void MidiControl::start() {
9297
}
9398
try {
9499
impl->in.openPort(index, "takt4 control");
100+
impl->errors.raise();
95101
} catch (const RtMidiError& error) {
96102
// Listed and refused: a WinMM input is one program's at a time, and a DAW that opened
97103
// every input it saw is the usual holder. Not "no such port" — the operator can see it.
@@ -140,6 +146,7 @@ void MidiControl::stop() noexcept {
140146
try {
141147
impl_->in.cancelCallback();
142148
impl_->in.closePort();
149+
impl_->errors.raise();
143150
} catch (const RtMidiError&) {
144151
// Closing a port that is already gone — an unplugged controller — is not
145152
// something a stop path can usefully report.

‎src/core/engine/beat_engine.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ void BeatEngine::setHostTimeSource(audio::HostTimeSource* source) noexcept {
8484
}
8585

8686
void BeatEngine::start() {
87+
// **The presses made while stopped go first**: a ÷2, ×2, tap, pin or DOWNBEAT from a control
88+
// surface between sets — the window greys its own. `stop` below takes what is queued and
89+
// applies it to the run that ended, and the reset after it undoes most of a press — but not a
90+
// tap, which moves the BPM window, a setting: a tap on a pad between sets moved the next
91+
// set's window. The settings posted meanwhile stay, for the run about to start. See
92+
// `ControlQueue::dropPresses`.
93+
controls_.dropPresses();
8794
stop();
8895

8996
EngineFrame frame;

‎src/core/engine/control.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ bool ControlQueue::post(const Command& command) {
2424
});
2525
pending_.erase(stale, pending_.end());
2626
}
27+
if (pending_.size() >= kCapacity && isSetting(command.kind)) {
28+
const auto press = std::find_if(pending_.begin(), pending_.end(),
29+
[](const Command& c) { return !isSetting(c.kind); });
30+
if (press != pending_.end()) {
31+
pending_.erase(press);
32+
++dropped_;
33+
}
34+
}
2735
if (pending_.size() >= kCapacity) {
2836
++dropped_;
2937
return false;
@@ -32,6 +40,11 @@ bool ControlQueue::post(const Command& command) {
3240
return true;
3341
}
3442

43+
void ControlQueue::dropPresses() {
44+
const std::lock_guard<std::mutex> lock(mutex_);
45+
std::erase_if(pending_, [](const Command& c) { return !isSetting(c.kind); });
46+
}
47+
3548
void ControlQueue::drain(std::vector<Command>& out) {
3649
out.clear();
3750
const std::lock_guard<std::mutex> lock(mutex_);

‎src/core/engine/control.hpp‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@ struct Command {
7373
static Command holdTempo(double bpm) noexcept { return Command{Kind::HoldTempo, {}, bpm, false}; }
7474
};
7575

76+
/// **A setting, not a press**: what the operator has set up rather than something done to the
77+
/// tracker at a moment — the settings (`SetTempoOptions`) and the tempo hold named directly
78+
/// (`HoldTempo`, which the console sets before it starts). The rest — ÷2, ×2, DOWNBEAT, a tap, a
79+
/// pin — act on what the tracker is hearing at the press, and mean nothing to a tracker that is
80+
/// stopped.
81+
constexpr bool isSetting(Command::Kind kind) noexcept {
82+
return kind == Command::Kind::SetTempoOptions || kind == Command::Kind::HoldTempo;
83+
}
84+
7685
/// The road into the inference thread: many writers, one reader, bounded, no allocation
7786
/// once it is warm.
7887
///
@@ -96,8 +105,18 @@ class ControlQueue {
96105
/// Any thread but the audio one. False when the queue is full, and then `dropped()`
97106
/// counts it. A SetTempoOptions supersedes any already waiting rather than queueing
98107
/// behind it, so a slider dragged while nothing is draining cannot fill this.
108+
///
109+
/// **A setting is never refused** (`isSetting`): a queue full of presses — 64 of them posted
110+
/// while nothing drained, a stopped tracker and a control surface — gives up the oldest press
111+
/// for it, counted as dropped. It used to refuse the setting, so the latency and BPM window
112+
/// sliders moved on screen and reached nothing.
99113
bool post(const Command& command);
100114

115+
/// Drops every press waiting (`isSetting`), keeping the settings in their order. What START
116+
/// does before it applies what was posted while the tracker was stopped: a ÷2, a pin or a
117+
/// DOWNBEAT pressed on a control surface between sets would otherwise land on the next run.
118+
void dropPresses();
119+
101120
/// The consumer. Takes everything waiting, in the order it was posted, and leaves the
102121
/// queue empty. `out` is cleared first. Keep the same vector across calls, **reserved to
103122
/// `kCapacity` before the first**, and this allocates nothing: the queue swaps it in and

‎src/core/output/midi_clock.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "core/output/midi_clock.hpp"
22

3+
#include "core/output/rtmidi_errors.hpp"
4+
35
#include "core/output/midi_ports.hpp"
46
#include "core/sandbox.hpp"
57

@@ -57,6 +59,7 @@ class RtMidiPort final : public MidiPort {
5759
RtMidiPort() {
5860
try {
5961
out_ = std::make_unique<RtMidiOut>();
62+
errors_.watch(*out_);
6063
} catch (const RtMidiError& error) {
6164
// A machine can have no usable MIDI API at all — a headless Linux box without an
6265
// ALSA sequencer is the ordinary case, and CI runs on one — and RtMidi reports
@@ -81,6 +84,7 @@ class RtMidiPort final : public MidiPort {
8184
try {
8285
index = findPort(*out_, spec); // MidiPortMissing: not on this machine
8386
name = out_->getPortName(index);
87+
errors_.raise();
8488
} catch (const RtMidiError& error) {
8589
throw std::runtime_error("MIDI output: " + error.getMessage());
8690
}
@@ -92,6 +96,7 @@ class RtMidiPort final : public MidiPort {
9296
}
9397
try {
9498
out_->openPort(index, "takt4");
99+
errors_.raise();
95100
} catch (const RtMidiError& error) {
96101
// Listed, and refused: on Windows a MIDI port is one program's at a time, so this is
97102
// another program holding it far more often than a broken driver — and saying "no
@@ -108,6 +113,7 @@ class RtMidiPort final : public MidiPort {
108113
}
109114
try {
110115
out_->closePort();
116+
errors_.raise();
111117
} catch (...) {
112118
// A port that is already gone cannot be closed any more gone.
113119
}
@@ -123,9 +129,12 @@ class RtMidiPort final : public MidiPort {
123129
throw std::runtime_error("MIDI output: the port is not open");
124130
}
125131
out_->sendMessage(message.data(), message.size());
132+
errors_.raise();
126133
}
127134

128135
private:
136+
/// Before `out_`, which reports to it and so must go first.
137+
RtMidiErrors errors_;
129138
std::unique_ptr<RtMidiOut> out_;
130139
/// RtMidi's reason, when it could not start at all and `out_` is empty.
131140
std::string unusable_;

‎src/core/output/midi_ports.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "core/output/midi_ports.hpp"
22

3+
#include "core/output/rtmidi_errors.hpp"
4+
35
#include <RtMidi.h>
46

57
#include <algorithm>
@@ -27,12 +29,15 @@ template <typename Midi>
2729
std::vector<std::string> listPorts() {
2830
std::vector<std::string> ports;
2931
try {
32+
RtMidiErrors errors;
3033
Midi midi;
34+
errors.watch(midi);
3135
const unsigned int count = midi.getPortCount();
3236
ports.reserve(count);
3337
for (unsigned int i = 0; i < count; ++i) {
3438
ports.push_back(midi.getPortName(i));
3539
}
40+
errors.raise();
3641
} catch (const RtMidiError&) {
3742
// No usable MIDI API on this machine; see the header.
3843
}

0 commit comments

Comments
 (0)