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:
- Track the current transport position, when playing
- Control the playing state (whether in a host or Standalone)
- 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);
}
