Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion include/element/transport.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,146 @@

namespace element {

/** Audio transport with thread-safe state management.

Extends Shuttle to provide a complete transport implementation with
lock-free communication between the audio thread and other threads.

State changes (play, record, tempo, seek) are requested from any thread
and applied during preProcess() on the audio thread. The Monitor class
provides read-only access to current transport state for UI updates.

@see Shuttle, Monitor
*/
class Transport : public Shuttle {
public:
/** Thread-safe, read-only view of transport state.

Monitor provides atomic access to transport parameters for UI display
and other non-audio threads. Values are updated by the Transport
during audio processing.

Reference counted for safe sharing between threads.
*/
class Monitor : public juce::ReferenceCountedObject {
public:
Monitor();

/** Number of beats per bar (time signature numerator). */
juce::Atomic<int> beatsPerBar;

/** Beat type/unit (time signature denominator, e.g. 4 = quarter note). */
juce::Atomic<int> beatType;

/** Beat divisor for subdivision calculations. */
juce::Atomic<int> beatDivisor;

/** Current sample rate in Hz. */
juce::Atomic<double> sampleRate;

/** Current tempo in beats per minute. */
juce::Atomic<float> tempo;

/** True if transport is currently playing. */
juce::Atomic<bool> playing;

/** True if transport is currently recording. */
juce::Atomic<bool> recording;

/** Current playhead position in audio frames (samples). */
juce::Atomic<int64_t> positionFrames;

/** Returns the ratio of beat type to beat divisor. */
double beatRatio() const noexcept;

/** Returns the current position in seconds. */
double getPositionSeconds() const;

/** Returns the current position in beats. */
float getPositionBeats() const;

/** Calculates bars, beats, and sub-beats from current position.
@param bars Output: bar number (0-based)
@param beats Output: beat within bar (0-based)
@param subBeats Output: sub-beat within beat (0-based)
@param subDivisions Number of sub-divisions per beat (default: 4)
*/
void getBarsAndBeats (int& bars, int& beats, int& subBeats, int subDivisions = 4);
};

typedef juce::ReferenceCountedObjectPtr<Monitor> MonitorPtr;
using MonitorPtr = juce::ReferenceCountedObjectPtr<Monitor>;

Transport();
~Transport();

/** Returns beats per bar from the current time signature. */
int getBeatsPerBar() const { return getTimeScale().beatsPerBar(); }

/** Returns beat type (denominator) from the current time signature. */
int getBeatType() const { return getTimeScale().beatType(); }

/** Requests a play state change (thread-safe).
@param p True to play, false to stop
*/
inline void requestPlayState (bool p)
{
while (! playState.set (p)) {
}
}

/** Toggles between play and pause (thread-safe). */
inline void requestPlayPause() { requestPlayState (! playState.get()); }

/** Requests a record state change (thread-safe).
@param r True to record, false to stop recording
*/
inline void requestRecordState (bool r)
{
while (! recordState.set (r)) {
}
}

/** Requests a tempo change (thread-safe).
@param bpm New tempo in beats per minute
*/
inline void requestTempo (const double bpm)
{
while (! nextTempo.set (bpm)) {
}
}

/** Requests a time signature change (thread-safe).
@param beatsPerBar Numerator of time signature
@param beatType Denominator of time signature
*/
void requestMeter (int beatsPerBar, int beatType);

/** Requests a seek to a specific audio frame (thread-safe).
@param frame Target position in samples
*/
void requestAudioFrame (const int64_t frame);

/** Called at the start of each audio block to apply pending state changes.
@param nframes Number of samples in the upcoming block
*/
void preProcess (int nframes);

/** Called at the end of each audio block to update the monitor.
@param nframes Number of samples that were processed
*/
void postProcess (int nframes);

/** Returns the monitor for reading transport state from other threads. */
inline MonitorPtr getMonitor() const { return monitor; }

/** Applies any pending tempo change immediately.

@warning Must only be called from the audio thread. Calling from other
threads may cause race conditions. Safe to call when the audio engine
is not running.
*/
void applyTempo() noexcept;

private:
AtomicValue<bool> playState, recordState;
AtomicValue<double> nextTempo;
Expand Down
20 changes: 15 additions & 5 deletions src/engine/audioengine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ class AudioEngine::Private : public AudioIODeviceCallback,
public:
Private (AudioEngine& e)
: engine (e),
sampleRate (0),
blockSize (0),
sampleRate (44100.0),
blockSize (512),
isPrepared (false),
numInputChans (0),
numOutputChans (0),
Expand All @@ -320,6 +320,10 @@ class AudioEngine::Private : public AudioIODeviceCallback,
midiClock.addListener (this);
graphs.onActiveGraphChanged = std::bind (&AudioEngine::Private::onCurrentGraphChanged, this);
midiIOMonitor = new MidiIOMonitor();

messageCollector.reset (sampleRate);
midiClock.reset (sampleRate, blockSize);

startTimerHz (90);
}

Expand Down Expand Up @@ -573,6 +577,7 @@ class AudioEngine::Private : public AudioIODeviceCallback,

prepareToPlay (sampleRate, blockSize);
isPrepared = true;
audioStarted.store (true, std::memory_order_release);
}

void audioDeviceStopped() override
Expand All @@ -587,8 +592,7 @@ class AudioEngine::Private : public AudioIODeviceCallback,
if (isPrepared)
releaseResources();
isPrepared = false;
sampleRate = 0.0;
blockSize = 0;
audioStarted.store (false, std::memory_order_release);
tempBuffer.setSize (1, 1);
graphs.releaseBuffers();
}
Expand All @@ -597,7 +601,8 @@ class AudioEngine::Private : public AudioIODeviceCallback,
{
if (! message.isActiveSense() && ! message.isMidiClock())
midiIOMonitor->received();
messageCollector.addMessageToQueue (message);
if (audioStarted.load (std::memory_order_acquire))
messageCollector.addMessageToQueue (message);
const bool clockWanted = processMidiClock.get() > 0 && sessionWantsExternalClock.get() > 0;
const bool doStartStop = startStopCont.get() != 0;

Expand Down Expand Up @@ -703,7 +708,11 @@ class AudioEngine::Private : public AudioIODeviceCallback,
void midiClockTempoChanged (const float bpm) override
{
if (sessionWantsExternalClock.get() > 0 && processMidiClock.get() > 0)
{
transport.requestTempo (bpm);
if (! audioStarted.load (std::memory_order_acquire))
transport.applyTempo();
}
}

void midiClockSignalAcquired() override {}
Expand Down Expand Up @@ -761,6 +770,7 @@ class AudioEngine::Private : public AudioIODeviceCallback,
MidiIOMonitorPtr midiIOMonitor;

Atomic<double> midiOutLatency { 0.0 };
std::atomic<bool> audioStarted { false };

ReferenceCountedArray<AudioEngine::LevelMeter> inMeters, outMeters;

Expand Down
7 changes: 6 additions & 1 deletion src/engine/transport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,19 @@ void Transport::preProcess (int nframes)
}
}

void Transport::postProcess (int nframes)
void Transport::applyTempo() noexcept
{
if (getTempo() != nextTempo.get())
{
setTempo (nextTempo.get());
nextTempo.set (getTempo());
monitor->tempo.set (nextTempo.get());
}
}

void Transport::postProcess (int nframes)
{
applyTempo();

monitor->playing.set (playing);
monitor->recording.set (recording);
Expand Down
Loading