AudioPlayHead: how to handle partial PositionInfo

I’m struggling to understand how to safely make use of the AudioPlayHead while also trying to figure out how to properly use the AudioProcessorValueTreeState class. Ultimately, I want to be able run in a DAW or as a standalone with controls in the plugin for manipulating the transport (play, stop, rewind, current position etc).

For now I’m trying to craft a simple plugin to help me understand these pieces, but I’m running into issues that I don’t really understand.

The goals of this plugin are:

  1. Track the current transport position, when playing
  2. Control the playing state (whether in a host or Standalone)
  3. Reposition the transport (by rewinding)

Currently, I’m working on #1.

The documentation for AudioPlayHead states:

As a plugin developer, you should code defensively so that the plugin behaves sensibly even when the host fails to provide timing information.

I’ve tried to do this, but when running in standalone mode, I’m seeing the sample time increase, but calls to check the position info for the ‘playing’ state are returning false.

My primary question is: How can I handle the case that the transport is moving, the position info is tracking that movement, but the position info does not indicate that the transport is in the “playing” state. There is a DBG line in TransportState::trackChanges that never gets hit, meanwhile there is a DBG line in TransportState::updatePosition that reports the movement of the transport (showing that the transport is moving, while not “playing”).

Secondarily, I’d like to know if I’m using AudioProcessorValueTreeState in something close to the right way. I’ve tried a handful of tutorials, but none seem to cover how to directly manipulate the value tree.

Here is the basic class I’m trying to construct (apologies for length…):

TransportState.h

#pragma once
#include <JuceHeader.h>

/// <summary>
/// Manages the state of the transport
/// </summary>
class TransportState {
public:
    juce::AudioProcessorValueTreeState::ParameterLayout parameters;
    juce::AudioProcessorValueTreeState state;

    TransportState(juce::AudioProcessor& processor, juce::UndoManager* undoManager, const juce::Identifier& valueTreeType);
    ~TransportState();

    // Id's are symbolic names, Names are human-friendly names for GUI
    static const juce::String playing_id, playing_name;
    static const juce::String ppq_id, ppq_name;
    static const juce::String sample_position_id, sample_position_name;
    static const juce::String sample_rate_id, sample_rate_name;

    void trackChanges(juce::AudioPlayHead* playHead);

    void updatePosition(juce::AudioPlayHead* playHead, juce::AudioBuffer<float>& buffer);

private:
    // for calculating ppq (in the absence of playhead position info
    float samplesPerQuarterNote = 480000.f;
    float secondsPerBeat = 1.f;
    float secondsPerTick = secondsPerBeat / tpq; 
    float tpq = 32; // 32 ticks per beat (1/128th note tick at 1/4 note beats)
    
    juce::AudioProcessorValueTreeState::ParameterLayout createParameters();

    void updatePpq(juce::AudioPlayHead* playHead, int bufferSize);

    void updateSamplePosition(juce::AudioPlayHead* playHead, int bufferSize);

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TransportState)
};

TransportState.cpp

#include "TransportState.h"

const juce::String TransportState::playing_id = "PLAYING";
const juce::String TransportState::playing_name = "Playing";
const juce::String TransportState::ppq_id = "PPQ";
const juce::String TransportState::ppq_name = "Play Position in Quarter Notes";
const juce::String TransportState::sample_position_id = "SAMPLE_POSITION";
const juce::String TransportState::sample_position_name = "Sample Position";
const juce::String TransportState::sample_rate_id = "SAMPLE_RATE";
const juce::String TransportState::sample_rate_name = "Sample Rate";

TransportState::TransportState(juce::AudioProcessor& processor, juce::UndoManager* undoManager, const juce::Identifier& valueTreeType)
	: state(processor, undoManager, valueTreeType, createParameters()) {}

TransportState::~TransportState() {}

juce::AudioProcessorValueTreeState::ParameterLayout TransportState::createParameters() {
	std::vector<std::unique_ptr<juce::RangedAudioParameter>> parameters;

	parameters.push_back(std::make_unique<juce::AudioParameterBool>(playing_id, playing_name, false));
	parameters.push_back(std::make_unique<juce::AudioParameterFloat>(ppq_id, ppq_name, 0.0f, std::numeric_limits<float>::max(), 0.0f));
	parameters.push_back(std::make_unique<juce::AudioParameterInt>(sample_position_id, sample_position_name, 0, std::numeric_limits<int>::max(), 0));
	parameters.push_back(std::make_unique<juce::AudioParameterFloat>(sample_rate_id, sample_rate_name, 0.f, std::numeric_limits<float>::max(), 48000.f));
	parameters.push_back(std::make_unique<juce::AudioParameterInt>(tempo_id, tempo_name, 10, 360, 60));


	return { parameters.begin(), parameters.end() }; // I don't understand what this does...
}

/// <summary>
/// Look for changes to the transport state to see if something of interest has changed
/// </summary>
/// <param name="playHead"></param>
void TransportState::trackChanges(juce::AudioPlayHead* playHead) {
	if (playHead != nullptr) {
		// read from the playHead
		juce::Optional<juce::AudioPlayHead::PositionInfo> pos = playHead->getPosition();
		if (pos.hasValue()) {
			// check if the play state has changed
			if (state.getParameter(TransportState::playing_id)->getValue() != pos->getIsPlaying()) {
				// update the play state in the value tree
				state.getParameter(TransportState::playing_id)->setValue(pos->getIsPlaying()); // will this trigger change listeners?
			}
	
			if (pos->getIsPlaying()) {
				DBG("playing"); // This is never reached when running the plugin as a standalone
			}
			
			// check if the tempo or time signature has changed
			if (pos->getBpm().hasValue()) {
				int newBpm = *pos->getBpm();
				if (state.getParameter(TransportState::tempo_id)->getValue() != newBpm) {
					state.getParameter(TransportState::tempo_id)->setValue(newBpm); // trigger change listeners?

					// update
					secondsPerBeat = 60.f / newBpm;
					secondsPerTick = secondsPerBeat / tpq;
					samplesPerQuarterNote = secondsPerTick * state.getParameter(TransportState::sample_rate_id)->getValue();
				};
			}
		}
	}
}

/// <summary>
/// after processing a buffer, update the play position
/// </summary>
/// <param name="buffer"></param>
void TransportState::updatePosition(juce::AudioPlayHead* playHead, juce::AudioBuffer<float>& buffer) {
	if (playHead != nullptr) {
		juce::Optional<juce::AudioPlayHead::PositionInfo> pos = playHead->getPosition();
		if (pos.hasValue()) {
			if (pos->getTimeInSamples().hasValue()) {
				DBG(*pos->getTimeInSamples());
				// the play head was able to tell us the position, so set it explicitly
				state.getParameter(TransportState::sample_position_id)->setValue(*pos->getTimeInSamples());
				if (pos->getPpqPosition().hasValue()) {
					state.getParameter(TransportState::ppq_id)->setValue(*pos->getPpqPosition());
				} else {
					updatePpq(playHead, buffer.getNumSamples());
				}
				return;
			}
		}
	}

	// if we get here, there is no play head or it was unable to tell us the position
	// calculate the position manually
	updateSamplePosition(playHead, buffer.getNumSamples());
	updatePpq(playHead, buffer.getNumSamples());
}

void TransportState::updatePpq(juce::AudioPlayHead* playHead, int bufferSize) {
	// update the ppq position
	state.getParameter(TransportState::ppq_id)->setValue(state.getParameter(TransportState::sample_position_id)->getValue() / samplesPerQuarterNote);
}

void TransportState::updateSamplePosition(juce::AudioPlayHead* playHead, int bufferSize) {
	// get the last position and add the number of samples in the buffer
	state.getParameter(TransportState::sample_position_id)->setValue(state.getParameter(TransportState::sample_position_id)->getValue() + bufferSize);
}

Plugin formats generally don’t provide any interface to manipulate the host’s playhead, and JUCE also doesn’t provide any way of doing this except for in Inter-App Audio on iOS, which has been deprecated by Apple in favour of AUv3.

The playhead provided by JUCE’s standalone wrapper via the AudioProcessorPlayer is very basic. It doesn’t have a notion of tempo, playing/recording state, and so on. To set custom position info in standalone apps, developers may derive from AudioPlayHead and override getPosition. Then, before accessing the playhead info, they can decide whether to use the custom playhead or the host-provided playhead, e.g.

auto *external_playhead = getPlayHead();
// internal_playhead is a data member of the AudioProcessor
auto *playhead_to_use = (is_standalone || external_playhead != nullptr) ? external_playhead : &internal_playhead;
const auto info = playhead_to_use->getPosition();

Thank you @reuk. My apologies, I’m still having a lot of difficulty wrapping my head around this.

I think, ideally, what I really want is to have UI elements for tempo, time signature, play, record, position etc. that bind to the host’s values (when they exist) and let the UI supply values that aren’t provided by the host.

I couldn’t get your code snippet to compile. I’m not sure if it was intended to. Putting it in the process block, it was not aware of is_standalone or internal_playhead.

I don’t know if I’m just going about things wrong, or if this stuff is really as dense as it seems to be (to me).

Can I derive my own playhead and initialize it in prepareToPlay and then use the PlayHead associated with the processor inside it, to get the host info?

The snippet was just pseudo code, but can easily be written:
Instead of is_standalone use the static method isStandaloneApp()

juce::JUCEApplicationBase::isStandaloneApp()

And internal_playhead is a class you have to write yourself derived from juce::AudioPlayHead.

getPosition() is pure virtual, so you have to implement that.

It is not provided by juce, because as soon as you start thinking of tempo you will find it to be a deep rabbit hole. Tempo changes, changes of time signature, tempo automation like ritardando etc. make it a tough one.

Only by limiting it to static tempo makes it easy, but in reality that won’t get you far.

Thank you for clarifying @daniel.

I think I’m finally getting a sense of how to put a custom playhead together. My last attempt involved inspecting the playhead constructed by the plugin within my custom playhead. I’ve run into some issues when running in standalone mode though.

TimeNs and SampleCount are automatically advancing as soon as the standalone loads. As @reuk mentioned, the AudioProcessorPlayer has no concept of ‘playing’, which is presumably why.

Now my problem is that I can’t figure out how to tell the difference between a playhead that is “Playing” aware and one that is not. since PositionInfo::getIsPlaying returns a raw bool, instead of an Optional<T> like most of the other getters in PositionInfo.