Merge pull request #553 from jpcima/beat-lfo

Add lfoN_beats
This commit is contained in:
JP Cimalando 2020-12-12 14:14:03 +01:00 committed by GitHub
commit d13dbe5bd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 889 additions and 94 deletions

View file

@ -48,6 +48,7 @@ SFIZZ_CXX_FLAGS = $(SFIZZ_C_FLAGS)
SFIZZ_SOURCES = \
src/sfizz/ADSREnvelope.cpp \
src/sfizz/AudioReader.cpp \
src/sfizz/BeatClock.cpp \
src/sfizz/Curve.cpp \
src/sfizz/effects/Apan.cpp \
src/sfizz/Effects.cpp \
@ -90,6 +91,7 @@ SFIZZ_SOURCES = \
src/sfizz/LFO.cpp \
src/sfizz/LFODescription.cpp \
src/sfizz/Messaging.cpp \
src/sfizz/Metronome.cpp \
src/sfizz/MidiState.cpp \
src/sfizz/OpcodeCleanup.cpp \
src/sfizz/Opcode.cpp \

View file

@ -108,25 +108,30 @@ int main(int argc, char* argv[])
return 1;
}
sfz::BufferPool bufferPool;
size_t numLfos = desc.size();
std::vector<sfz::LFO> lfos(numLfos);
std::vector<std::unique_ptr<sfz::LFO>> lfos(numLfos);
for (size_t l = 0; l < numLfos; ++l) {
lfos[l].setSampleRate(sampleRate);
lfos[l].configure(&desc[l]);
const NumericId<sfz::LFO> id { static_cast<int>(l) };
sfz::LFO* lfo = new sfz::LFO(id, bufferPool);
lfos[l].reset(lfo);
lfo->setSampleRate(sampleRate);
lfo->configure(&desc[l]);
}
size_t numFrames = (size_t)std::ceil(sampleRate * duration);
std::vector<float> outputMemory(numLfos * numFrames);
for (size_t l = 0; l < numLfos; ++l) {
lfos[l].start(0);
lfos[l]->start(0);
}
std::vector<absl::Span<float>> lfoOutputs(numLfos);
for (size_t l = 0; l < numLfos; ++l) {
lfoOutputs[l] = absl::MakeSpan(&outputMemory[l * numFrames], numFrames);
lfos[l].process(lfoOutputs[l]);
lfos[l]->process(lfoOutputs[l]);
}
if (saveFlac) {

View file

@ -175,11 +175,11 @@ typedef struct
// Timing data
int bar;
float bar_beat;
double bar_beat;
int beats_per_bar;
int beat_unit;
float bpm_tempo;
float speed;
double bpm_tempo;
double speed;
// Paths
char bundle_path[MAX_BUNDLE_PATH_SIZE];

View file

@ -21,6 +21,7 @@ set (SFIZZ_HEADERS
sfizz/AudioBuffer.h
sfizz/AudioReader.h
sfizz/AudioSpan.h
sfizz/BeatClock.h
sfizz/Buffer.h
sfizz/BufferPool.h
sfizz/CCMap.h
@ -79,6 +80,7 @@ set (SFIZZ_HEADERS
sfizz/LFO.h
sfizz/LFODescription.h
sfizz/MathHelpers.h
sfizz/Metronome.h
sfizz/MidiState.h
sfizz/ModifierHelpers.h
sfizz/OnePoleFilter.h
@ -149,6 +151,8 @@ set (SFIZZ_SOURCES
sfizz/PowerFollower.cpp
sfizz/FlexEGDescription.cpp
sfizz/FlexEnvelope.cpp
sfizz/BeatClock.cpp
sfizz/Metronome.cpp
sfizz/SynthMessaging.cpp
sfizz/modulations/ModId.cpp
sfizz/modulations/ModKey.cpp

View file

@ -392,7 +392,7 @@ SFIZZ_EXPORTED_API void sfizz_send_time_signature(sfizz_synth_t* synth, int dela
* @param bar The current bar.
* @param bar_beat The fractional position of the current beat within the bar.
*/
SFIZZ_EXPORTED_API void sfizz_send_time_position(sfizz_synth_t* synth, int delay, int bar, float bar_beat);
SFIZZ_EXPORTED_API void sfizz_send_time_position(sfizz_synth_t* synth, int delay, int bar, double bar_beat);
/**
* @brief Send the playback state.

View file

@ -359,7 +359,7 @@ public:
* @param bar The current bar.
* @param barBeat The fractional position of the current beat within the bar.
*/
void timePosition(int delay, int bar, float barBeat);
void timePosition(int delay, int bar, double barBeat);
/**
* @brief Send the playback state.

253
src/sfizz/BeatClock.cpp Normal file
View file

@ -0,0 +1,253 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "BeatClock.h"
#include "SIMDHelpers.h"
#include "Config.h"
#include "Debug.h"
#include <iostream>
#include <cmath>
namespace sfz {
bool TimeSignature::operator==(const TimeSignature& other) const
{
return beatsPerBar == other.beatsPerBar && beatUnit == other.beatUnit;
}
bool TimeSignature::operator!=(const TimeSignature& other) const
{
return !operator==(other);
}
///
BBT BBT::toSignature(TimeSignature oldSig, TimeSignature newSig) const
{
double beatsInOldSig = toBeats(oldSig);
double beatsInNewSig = beatsInOldSig * newSig.beatUnit / oldSig.beatUnit;
return BBT::fromBeats(newSig, beatsInNewSig);
}
double BBT::toBeats(TimeSignature sig) const
{
return beat + bar * sig.beatsPerBar;
}
BBT BBT::fromBeats(TimeSignature sig, double beats)
{
int newBar = static_cast<int>(beats / sig.beatsPerBar);
double newBeat = beats - newBar * sig.beatsPerBar;
return BBT(newBar, newBeat);
}
double BBT::toBars(TimeSignature sig) const
{
return bar + beat / sig.beatsPerBar;
}
///
constexpr int BeatClock::resolution;
auto BeatClock::quantize(double beats) -> qbeats_t
{
double d = beats * (1 << resolution);
d = std::copysign(0.5 + std::fabs(d), d);
return static_cast<qbeats_t>(d);
}
template <class T>
T BeatClock::dequantize(qbeats_t qbeats)
{
return qbeats / static_cast<T>(1 << resolution);
}
///
void BeatClock::clear()
{
beatsPerSecond_ = 2.0;
timeSig_ = { 4, 4 };
isPlaying_ = false;
lastHostPos_ = { 0, 0 };
lastClientPos_ = { 0, 0 };
}
void BeatClock::beginCycle(unsigned numFrames)
{
currentCycleFrames_ = numFrames;
currentCycleFill_ = 0;
currentCycleStartPos_ = lastClientPos_;
}
void BeatClock::endCycle()
{
fillBufferUpTo(currentCycleFrames_);
}
void BeatClock::setSampleRate(double sampleRate)
{
samplePeriod_ = 1.0 / sampleRate;
}
void BeatClock::setSamplesPerBlock(unsigned samplesPerBlock)
{
runningBeatNumber_.resize(samplesPerBlock);
runningBeatPosition_.resize(samplesPerBlock);
runningBeatsPerBar_.resize(samplesPerBlock);
}
void BeatClock::setTempo(unsigned delay, double secondsPerBeat)
{
fillBufferUpTo(delay);
beatsPerSecond_ = 1.0 / secondsPerBeat;
}
void BeatClock::setTimeSignature(unsigned delay, TimeSignature newSig)
{
fillBufferUpTo(delay);
if (!newSig.valid()) {
CHECKFALSE;
return;
}
TimeSignature oldSig = timeSig_;
if (oldSig == newSig)
return;
timeSig_ = newSig;
// convert time to new signature
lastHostPos_ = lastHostPos_.toSignature(oldSig, newSig);
lastClientPos_ = lastClientPos_.toSignature(oldSig, newSig);
}
void BeatClock::setTimePosition(unsigned delay, BBT newPos)
{
fillBufferUpTo(delay);
lastHostPos_ = newPos;
// apply host position in the next frame
mustApplyHostPos_ = true;
}
void BeatClock::setPlaying(unsigned delay, bool playing)
{
fillBufferUpTo(delay);
isPlaying_ = playing;
}
absl::Span<const int> BeatClock::getRunningBeatNumber()
{
fillBufferUpTo(currentCycleFrames_);
return absl::MakeConstSpan(runningBeatNumber_.data(), currentCycleFrames_);
}
absl::Span<const float> BeatClock::getRunningBeatPosition()
{
fillBufferUpTo(currentCycleFrames_);
return absl::MakeConstSpan(runningBeatPosition_.data(), currentCycleFrames_);
}
absl::Span<const int> BeatClock::getRunningBeatsPerBar()
{
fillBufferUpTo(currentCycleFrames_);
return absl::MakeConstSpan(runningBeatsPerBar_.data(), currentCycleFrames_);
}
void BeatClock::fillBufferUpTo(unsigned delay)
{
int *beatNumberData = runningBeatNumber_.data();
float *beatNumberPosition = runningBeatPosition_.data();
int *beatsPerBarData = runningBeatsPerBar_.data();
unsigned fillIdx = currentCycleFill_;
const TimeSignature sig = timeSig_;
for (unsigned i = fillIdx; i < delay; ++i)
beatsPerBarData[i] = sig.beatsPerBar;
if (!isPlaying_) {
if (fillIdx < delay) {
fill(absl::MakeSpan(&beatNumberData[fillIdx], delay - fillIdx), 0);
fill(absl::MakeSpan(&beatNumberPosition[fillIdx], delay - fillIdx), 0.0f);
}
currentCycleFill_ = fillIdx;
return;
}
BBT clientPos = lastClientPos_;
const double beatsPerFrame = beatsPerSecond_ * samplePeriod_;
const BBT hostPos = lastHostPos_;
bool mustApplyHostPos = mustApplyHostPos_;
for (; fillIdx < delay; ++fillIdx) {
clientPos = BBT::fromBeats(sig, clientPos.toBeats(sig) + beatsPerFrame);
clientPos = mustApplyHostPos ? hostPos : clientPos;
mustApplyHostPos = false;
// quantization to nearest for prevention of rounding errors
double beats = clientPos.toBeats(sig);
beatNumberData[fillIdx] = dequantize<int>(quantize(beats));
beatNumberPosition[fillIdx] = static_cast<float>(beats);
}
currentCycleFill_ = fillIdx;
lastClientPos_ = clientPos;
mustApplyHostPos_ = mustApplyHostPos;
}
void BeatClock::calculatePhase(float beatPeriod, float* phaseOut)
{
const unsigned numFrames = currentCycleFrames_;
if (beatPeriod <= 0.0f) {
fill(absl::MakeSpan(phaseOut, numFrames), 0.0f);
return;
}
const float invBeatPeriod = 1.0f / beatPeriod;
const float* beatPositionData = getRunningBeatPosition().data();
for (unsigned i = 0; i < numFrames; ++i) {
float beatPosition = std::max(0.0f, beatPositionData[i]);
float phase = beatPosition * invBeatPeriod;
phase -= static_cast<int>(phase);
phaseOut[i] = phase;
}
}
void BeatClock::calculatePhaseModulated(const float* beatPeriodData, float* phaseOut)
{
const unsigned numFrames = currentCycleFrames_;
const float* beatPositionData = getRunningBeatPosition().data();
for (unsigned i = 0; i < numFrames; ++i) {
float beatPeriod = beatPeriodData[i];
float beatPosition = std::max(0.0f, beatPositionData[i]);
float phase = beatPosition / beatPeriod;
phase -= static_cast<int>(phase);
phaseOut[i] = (beatPeriod > 0.0f) ? phase : 0.0f;
}
}
} // namespace sfz
std::ostream& operator<<(std::ostream& os, const sfz::BBT& pos)
{
return os << pos.bar << ':' << std::fixed << pos.beat;
}
std::ostream& operator<<(std::ostream& os, const sfz::TimeSignature& sig)
{
return os << sig.beatsPerBar << '/' << sig.beatUnit;
}

186
src/sfizz/BeatClock.h Normal file
View file

@ -0,0 +1,186 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "Buffer.h"
#include <absl/types/span.h>
#include <vector>
#include <iosfwd>
namespace sfz {
/**
* @brief Musical time signature
*/
struct TimeSignature {
TimeSignature() {}
TimeSignature(int beatsPerBar, int beatUnit) : beatsPerBar(beatsPerBar), beatUnit(beatUnit) {}
/**
* @brief Check the signature validity.
* Valid signatures have a strictly positive numerator and denominator.
*/
bool valid() const { return beatsPerBar > 0 && beatUnit > 0; }
bool operator==(const TimeSignature& other) const;
bool operator!=(const TimeSignature& other) const;
/**
* @brief Time signature numerator, indicating the number of beats in a bar
*/
int beatsPerBar = 0;
/**
* @brief Time signature denominator, indicating the type of note (4=quarter)
*/
int beatUnit = 0;
};
/**
* @brief Musical time in BBT form
*/
struct BBT {
BBT() {}
BBT(int bar, double beat) : bar(bar), beat(beat) {}
/**
* @brief Convert the time to a different signature.
*/
BBT toSignature(TimeSignature oldSig, TimeSignature newSig) const;
/**
* @brief Convert the time to a fractional quantity in beats.
*/
double toBeats(TimeSignature sig) const;
/**
* @brief Convert the time to a fractional quantity in bars.
*/
double toBars(TimeSignature sig) const;
/**
* @brief Convert the fractional quantity in beats to musical time.
*/
static BBT fromBeats(TimeSignature sig, double beats);
/**
* @brief Bar number
*/
int bar = 0;
/**
* @brief Beat and tick, stored in the integral and fractional parts
*/
double beat = 0;
};
class BeatClock {
public:
/**
* @brief Set the sample rate.
*/
void setSampleRate(double sampleRate);
/**
* @brief Set the block size.
*/
void setSamplesPerBlock(unsigned samplesPerBlock);
/**
* @brief Reinitialize the current state.
*/
void clear();
/**
* @brief Start a new cycle of clock processing.
*/
void beginCycle(unsigned numFrames);
/**
* @brief End the current cycle of clock processing.
*/
void endCycle();
/**
* @brief Set the tempo.
*/
void setTempo(unsigned delay, double secondsPerBeat);
/**
* @brief Set the time signature.
*/
void setTimeSignature(unsigned delay, TimeSignature newSig);
/**
* @brief Set the time position.
*/
void setTimePosition(unsigned delay, BBT newPos);
/**
* @brief Set whether the clock is ticking or stopped.
*/
void setPlaying(unsigned delay, bool playing);
/**
* Check whether the clock is currently ticking.
*/
bool isPlaying() const noexcept { return isPlaying_; }
/**
* @brief Get the beat number for each frame of the current cycle.
*
* This signal is quantized to a fixed resolution, such that it never
* suffers 1-off errors due to imprecision in the host time position.
*/
absl::Span<const int> getRunningBeatNumber();
/**
* @brief Get the beat position for each frame of the current cycle.
*
* This is a fractional equivalent of the beat number, however the beat
* boundaries can be traversed erratically due to approximation errors.
* If you need to perform work on exact beat transitions, prefer
* `getRunningBeatNumber` instead.
*/
absl::Span<const float> getRunningBeatPosition();
/**
* @brief Get the time signature numerator for each frame of the current cycle.
*/
absl::Span<const int> getRunningBeatsPerBar();
/**
* @brief Create a normalized phase signal for LFO which completes a
* period every N-th beat.
*/
void calculatePhase(float beatPeriod, float* phaseOut);
/**
* @brief Create a normalized phase signal for LFO which completes a
* period every N-th beat, where N can vary over time.
*/
void calculatePhaseModulated(const float* beatPeriodData, float* phaseOut);
private:
void fillBufferUpTo(unsigned delay);
private:
double samplePeriod_ { 1.0 / config::defaultSampleRate };
// quantization
typedef int64_t qbeats_t;
static constexpr int resolution = 16; // bits
static qbeats_t quantize(int beats) { return beats * (1 << resolution); }
static qbeats_t quantize(double beats);
template <class T> static T dequantize(qbeats_t qbeats);
// status of current cycle
unsigned currentCycleFrames_ = 0;
unsigned currentCycleFill_ = 0;
BBT currentCycleStartPos_;
// musical time information from host
double beatsPerSecond_ = 2.0;
TimeSignature timeSig_ { 4, 4 };
bool isPlaying_ = false;
// last time position received from host
BBT lastHostPos_;
bool mustApplyHostPos_ = false;
// plugin-side counter
BBT lastClientPos_;
Buffer<int> runningBeatNumber_ { config::defaultSamplesPerBlock };
Buffer<float> runningBeatPosition_ { config::defaultSamplesPerBlock };
Buffer<int> runningBeatsPerBar_ { config::defaultSamplesPerBlock };
};
} // namespace sfz
std::ostream& operator<<(std::ostream& os, const sfz::BBT& pos);
std::ostream& operator<<(std::ostream& os, const sfz::TimeSignature& sig);

View file

@ -9,6 +9,7 @@
#include "Debug.h"
#include "Buffer.h"
#include "AudioBuffer.h"
#include "AudioSpan.h"
#include <array>
#include <memory>
#include <functional>

View file

@ -222,6 +222,9 @@ namespace Default
constexpr int numLFOSubs { 2 };
constexpr int numLFOSteps { 8 };
constexpr Range<float> lfoFreqRange { 0.0, 100.0 };
constexpr Range<float> lfoFreqModRange { -100.0, 100.0 };
constexpr Range<float> lfoBeatsRange { 0.0, 1000.0 };
constexpr Range<float> lfoBeatsModRange { -1000.0, 1000.0 };
constexpr Range<float> lfoPhaseRange { 0.0, 1.0 };
constexpr Range<float> lfoDelayRange { 0.0, 30.0 };
constexpr Range<float> lfoFadeRange { 0.0, 30.0 };

View file

@ -6,9 +6,14 @@
#include "LFO.h"
#include "LFODescription.h"
#include "BeatClock.h"
#include "BufferPool.h"
#include "MathHelpers.h"
#include "SIMDHelpers.h"
#include "Config.h"
#include "modulations/ModMatrix.h"
#include "modulations/ModKey.h"
#include "modulations/ModId.h"
#include <array>
#include <algorithm>
#include <cmath>
@ -16,6 +21,20 @@
namespace sfz {
struct LFO::Impl {
explicit Impl(NumericId<LFO> id, BufferPool& bufferPool, BeatClock* beatClock, ModMatrix* modMatrix)
: id_(id),
bufferPool_(bufferPool),
beatClock_(beatClock),
modMatrix_(modMatrix),
sampleRate_(config::defaultSampleRate),
desc_(&LFODescription::getDefault())
{
}
NumericId<LFO> id_;
BufferPool& bufferPool_;
BeatClock* beatClock_ = nullptr;
ModMatrix* modMatrix_ = nullptr;
float sampleRate_ = 0;
// control
@ -26,19 +45,23 @@ struct LFO::Impl {
float fadePosition_ = 0;
std::array<float, config::maxLFOSubs> subPhases_ {{}};
std::array<float, config::maxLFOSubs> sampleHoldMem_ {{}};
std::array<int, config::maxLFOSubs> sampleHoldState_ {{}};
};
LFO::LFO()
: impl_(new Impl)
LFO::LFO(NumericId<LFO> id, BufferPool& bufferPool, BeatClock* beatClock, ModMatrix* modMatrix)
: impl_(new Impl(id, bufferPool, beatClock, modMatrix))
{
impl_->sampleRate_ = config::defaultSampleRate;
impl_->desc_ = &LFODescription::getDefault();
}
LFO::~LFO()
{
}
NumericId<LFO> LFO::getId() const noexcept
{
return impl_->id_;
}
void LFO::setSampleRate(double sampleRate)
{
impl_->sampleRate_ = sampleRate;
@ -55,8 +78,9 @@ void LFO::start(unsigned triggerDelay)
const LFODescription& desc = *impl.desc_;
const float sampleRate = impl.sampleRate_;
impl.subPhases_.fill(desc.phase0);
impl.subPhases_.fill(0.0f);
impl.sampleHoldMem_.fill(0.0f);
impl.sampleHoldState_.fill(0);
const float delay = desc.delay;
size_t delayFrames = (delay > 0) ? static_cast<size_t>(std::ceil(sampleRate * delay)) : 0u;
@ -118,75 +142,57 @@ inline float LFO::eval<LFOWave::Saw>(float phase)
}
template <LFOWave W>
void LFO::processWave(unsigned nth, absl::Span<float> out)
void LFO::processWave(unsigned nth, absl::Span<float> out, const float* phaseIn)
{
Impl& impl = *impl_;
const LFODescription& desc = *impl.desc_;
const LFODescription::Sub& sub = desc.sub[nth];
const size_t numFrames = out.size();
const float samplePeriod = 1.0f / impl.sampleRate_;
const float baseFreq = desc.freq;
const float offset = sub.offset;
const float ratio = sub.ratio;
const float scale = sub.scale;
float phase = impl.subPhases_[nth];
for (size_t i = 0; i < numFrames; ++i) {
float phase = phaseIn[i];
out[i] += offset + scale * eval<W>(phase);
// TODO(jpc) lfoN_count: number of repetitions
float incrPhase = ratio * samplePeriod * baseFreq;
phase += incrPhase;
int numWraps = (int)phase;
phase -= numWraps;
}
impl.subPhases_[nth] = phase;
}
template <LFOWave W>
void LFO::processSH(unsigned nth, absl::Span<float> out)
void LFO::processSH(unsigned nth, absl::Span<float> out, const float* phaseIn)
{
Impl& impl = *impl_;
const LFODescription& desc = *impl.desc_;
const LFODescription::Sub& sub = desc.sub[nth];
const size_t numFrames = out.size();
const float samplePeriod = 1.0f / impl.sampleRate_;
const float baseFreq = desc.freq;
const float offset = sub.offset;
const float ratio = sub.ratio;
const float scale = sub.scale;
float sampleHoldValue = impl.sampleHoldMem_[nth];
float phase = impl.subPhases_[nth];
int sampleHoldState = impl.sampleHoldState_[nth];
for (size_t i = 0; i < numFrames; ++i) {
out[i] += offset + scale * sampleHoldValue;
// TODO(jpc) lfoN_count: number of repetitions
float incrPhase = ratio * samplePeriod * baseFreq;
float phase = phaseIn[i];
int oldState = sampleHoldState;
sampleHoldState = phase > 0.5f;
// value updates twice every period
bool updateValue = (int)(phase * 2.0) != (int)((phase + incrPhase) * 2.0);
phase += incrPhase;
int numWraps = (int)phase;
phase -= numWraps;
if (updateValue) {
if (sampleHoldState != oldState) {
std::uniform_real_distribution<float> dist(-1.0f, +1.0f);
sampleHoldValue = dist(Random::randomGenerator);
}
}
impl.subPhases_[nth] = phase;
impl.sampleHoldMem_[nth] = sampleHoldValue;
impl.sampleHoldState_[nth] = sampleHoldState;
}
void LFO::processSteps(absl::Span<float> out)
void LFO::processSteps(absl::Span<float> out, const float* phaseIn)
{
unsigned nth = 0;
Impl& impl = *impl_;
@ -201,29 +207,17 @@ void LFO::processSteps(absl::Span<float> out)
if (numSteps <= 0)
return;
const float samplePeriod = 1.0f / impl.sampleRate_;
const float baseFreq = desc.freq;
const float offset = sub.offset;
const float ratio = sub.ratio;
const float scale = sub.scale;
float phase = impl.subPhases_[nth];
for (size_t i = 0; i < numFrames; ++i) {
float phase = phaseIn[i];
float step = steps[static_cast<int>(phase * numSteps)];
out[i] += offset + scale * step;
// TODO(jpc) lfoN_count: number of repetitions
float incrPhase = ratio * samplePeriod * baseFreq;
phase += incrPhase;
int numWraps = (int)phase;
phase -= numWraps;
}
impl.subPhases_[nth] = phase;
}
void LFO::process(absl::Span<float> out)
void LFO::process(absl::Span<float> out, NumericId<Region> regionId)
{
Impl& impl = *impl_;
const LFODescription& desc = *impl.desc_;
@ -244,39 +238,50 @@ void LFO::process(absl::Span<float> out)
if (countSubs < 1)
return;
auto phasesTemp = impl.bufferPool_.getBuffer(numFrames);
if (!phasesTemp) {
ASSERTFALSE;
fill(out, 0.0f);
return;
}
absl::Span<float> phases = *phasesTemp;
if (desc.seq) {
processSteps(out);
generatePhase(0, phases, regionId);
processSteps(out, phases.data());
++subno;
}
for (; subno < countSubs; ++subno) {
generatePhase(subno, phases, regionId);
switch (desc.sub[subno].wave) {
case LFOWave::Triangle:
processWave<LFOWave::Triangle>(subno, out);
processWave<LFOWave::Triangle>(subno, out, phases.data());
break;
case LFOWave::Sine:
processWave<LFOWave::Sine>(subno, out);
processWave<LFOWave::Sine>(subno, out, phases.data());
break;
case LFOWave::Pulse75:
processWave<LFOWave::Pulse75>(subno, out);
processWave<LFOWave::Pulse75>(subno, out, phases.data());
break;
case LFOWave::Square:
processWave<LFOWave::Square>(subno, out);
processWave<LFOWave::Square>(subno, out, phases.data());
break;
case LFOWave::Pulse25:
processWave<LFOWave::Pulse25>(subno, out);
processWave<LFOWave::Pulse25>(subno, out, phases.data());
break;
case LFOWave::Pulse12_5:
processWave<LFOWave::Pulse12_5>(subno, out);
processWave<LFOWave::Pulse12_5>(subno, out, phases.data());
break;
case LFOWave::Ramp:
processWave<LFOWave::Ramp>(subno, out);
processWave<LFOWave::Ramp>(subno, out, phases.data());
break;
case LFOWave::Saw:
processWave<LFOWave::Saw>(subno, out);
processWave<LFOWave::Saw>(subno, out, phases.data());
break;
case LFOWave::RandomSH:
processSH<LFOWave::RandomSH>(subno, out);
processSH<LFOWave::RandomSH>(subno, out, phases.data());
break;
}
}
@ -306,4 +311,87 @@ void LFO::processFadeIn(absl::Span<float> out)
impl.fadePosition_ = fadePosition;
}
void LFO::generatePhase(unsigned nth, absl::Span<float> phases, NumericId<Region> regionId)
{
Impl& impl = *impl_;
BufferPool& bufferPool = impl.bufferPool_;
BeatClock* beatClock = impl.beatClock_;
ModMatrix* modMatrix = impl.modMatrix_;
const NumericId<LFO> id { impl.id_ };
const LFODescription& desc = *impl.desc_;
const LFODescription::Sub& sub = desc.sub[nth];
const float samplePeriod = 1.0f / impl.sampleRate_;
const float baseFreq = desc.freq;
const float beats = desc.beats;
const float phaseOffset = desc.phase0;
const float ratio = sub.ratio;
float phase = impl.subPhases_[nth];
const size_t numFrames = phases.size();
// TODO(jpc) lfoN_count: number of repetitions
// modulations
const float* beatsMod = nullptr;
const float* freqMod = nullptr;
if (modMatrix && id && regionId) {
// Note(jpc) we might switch between beats and frequency, if host
// switches play state on and off; continually generate both.
ModKey beatsKey = ModKey::createNXYZ(ModId::LFOBeats, regionId, id.number());
ModKey freqKey = ModKey::createNXYZ(ModId::LFOFrequency, regionId, id.number());
beatsMod = modMatrix->getModulationByKey(beatsKey);
freqMod = modMatrix->getModulationByKey(freqKey);
}
if (beatClock && beatClock->isPlaying() && beats > 0) {
// generate using the beat clock
float beatRatio = (ratio > 0) ? (1.0f / ratio) : 0.0f;
if (!beatsMod)
beatClock->calculatePhase(beats * beatRatio, phases.data());
else {
auto temp = bufferPool.getBuffer(numFrames);
if (!temp) {
ASSERTFALSE;
beatClock->calculatePhase(beats * beatRatio, phases.data());
}
else {
fill(*temp, beats);
add(absl::MakeConstSpan(beatsMod, numFrames), *temp);
applyGain1(beatRatio, *temp);
beatClock->calculatePhaseModulated(temp->data(), phases.data());
}
}
}
else {
// generate using the frequency
if (!freqMod) {
for (size_t i = 0; i < numFrames; ++i) {
phases[i] = phase;
float incr = ratio * samplePeriod * baseFreq;
phase += incr;
int numWraps = (int)phase;
phase -= numWraps;
}
}
else {
for (size_t i = 0; i < numFrames; ++i) {
phases[i] = phase;
float incr = ratio * samplePeriod * (baseFreq + freqMod[i]);
phase += incr;
int numWraps = (int)phase;
phase -= numWraps;
}
}
}
// apply phase offsets
for (size_t i = 0; i < numFrames; ++i) {
float withOffset = phases[i] + phaseOffset;
withOffset -= (int)withOffset;
phases[i] = withOffset;
}
impl.subPhases_[nth] = phase;
}
} // namespace sfz

View file

@ -5,10 +5,15 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "utility/NumericId.h"
#include <absl/types/span.h>
#include <memory>
namespace sfz {
class BufferPool;
class BeatClock;
class ModMatrix;
struct Region;
enum class LFOWave : int;
struct LFODescription;
@ -49,9 +54,15 @@ struct LFODescription;
class LFO {
public:
LFO();
explicit LFO(
NumericId<LFO> id,
BufferPool& bufferPool,
BeatClock* beatClock = nullptr,
ModMatrix* modMatrix = nullptr);
~LFO();
NumericId<LFO> getId() const noexcept;
/**
Sets the sample rate.
*/
@ -74,7 +85,7 @@ public:
TODO(jpc) frequency modulations
*/
void process(absl::Span<float> out);
void process(absl::Span<float> out, NumericId<Region> regionId = {});
private:
/**
@ -91,24 +102,29 @@ private:
on wave type inside the frame loop.
*/
template <LFOWave W>
void processWave(unsigned nth, absl::Span<float> out);
void processWave(unsigned nth, absl::Span<float> out, const float* phaseIn);
/**
Process a sample-and-hold subwaveform, adding to the buffer.
*/
template <LFOWave W>
void processSH(unsigned nth, absl::Span<float> out);
void processSH(unsigned nth, absl::Span<float> out, const float* phaseIn);
/**
Process the step sequencer, adding to the buffer.
*/
void processSteps(absl::Span<float> out);
void processSteps(absl::Span<float> out, const float* phaseIn);
/**
Process the fade in gain, and apply it to the buffer.
*/
void processFadeIn(absl::Span<float> out);
/**
Generate the phase of the N-th generator
*/
void generatePhase(unsigned nth, absl::Span<float> phases, NumericId<Region> regionId);
private:
struct Impl;
std::unique_ptr<Impl> impl_;

View file

@ -28,6 +28,7 @@ struct LFODescription {
~LFODescription();
static const LFODescription& getDefault();
float freq = 0; // lfoN_freq
float beats = 0; // lfoN_beats
float phase0 = 0; // lfoN_phase
float delay = 0; // lfoN_delay
float fade = 0; // lfoN_fade

110
src/sfizz/Metronome.cpp Normal file
View file

@ -0,0 +1,110 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "Metronome.h"
#include "Config.h"
namespace sfz {
Metronome::Metronome()
{
fGain = 0.5f;
init(config::defaultSampleRate);
}
void Metronome::init(float sampleRate)
{
fConst0 = std::min<float>(192000.0f, std::max<float>(1.0f, float(sampleRate)));
fConst1 = std::cos((2764.60156f / fConst0));
fConst2 = std::sqrt(std::max<float>(0.0f, ((fConst1 + 1.0f) / (1.0f - fConst1))));
fConst3 = (1.0f / fConst2);
fConst4 = std::cos((5529.20312f / fConst0));
fConst5 = std::sqrt(std::max<float>(0.0f, ((fConst4 + 1.0f) / (1.0f - fConst4))));
fConst6 = (1.0f / fConst5);
fConst7 = std::max<float>(1.0f, (0.00499999989f * fConst0));
fConst8 = (1.0f / fConst7);
fConst9 = (1.0f / std::max<float>(1.0f, (0.100000001f * fConst0)));
clear();
}
void Metronome::clear()
{
for (int l0 = 0; (l0 < 2); l0 = (l0 + 1)) {
iVec0[l0] = 0;
}
for (int l1 = 0; (l1 < 2); l1 = (l1 + 1)) {
iVec1[l1] = 0;
}
for (int l2 = 0; (l2 < 2); l2 = (l2 + 1)) {
iVec2[l2] = 0;
}
for (int l3 = 0; (l3 < 2); l3 = (l3 + 1)) {
iRec0[l3] = 0;
}
for (int l4 = 0; (l4 < 2); l4 = (l4 + 1)) {
fVec3[l4] = 0.0f;
}
for (int l5 = 0; (l5 < 2); l5 = (l5 + 1)) {
fRec1[l5] = 0.0f;
}
for (int l6 = 0; (l6 < 2); l6 = (l6 + 1)) {
fRec2[l6] = 0.0f;
}
for (int l7 = 0; (l7 < 2); l7 = (l7 + 1)) {
fVec4[l7] = 0.0f;
}
for (int l8 = 0; (l8 < 2); l8 = (l8 + 1)) {
fRec3[l8] = 0.0f;
}
for (int l9 = 0; (l9 < 2); l9 = (l9 + 1)) {
fRec4[l9] = 0.0f;
}
for (int l10 = 0; (l10 < 2); l10 = (l10 + 1)) {
iRec5[l10] = 0;
}
}
void Metronome::processAdding(const int* beats, const int* beatsPerBar, float* outputL, float* outputR, int numFrames)
{
float fSlow0 = float(fGain);
for (int i = 0; (i < numFrames); i = (i + 1)) {
int iTemp0 = int(beats[i]);
iVec0[0] = iTemp0;
iVec1[0] = 1;
int iTemp1 = ((iTemp0 - iVec0[1]) > 0);
iVec2[0] = iTemp1;
iRec0[0] = (iTemp1 ? ((iTemp0 % int(beatsPerBar[i])) == 0) : iRec0[1]);
fVec3[0] = fConst2;
float fTemp2 = float((1 - iVec1[1]));
float fTemp3 = (fConst3 * (fRec2[1] * (fTemp2 + fVec3[1])));
float fTemp4 = (fConst1 * (fTemp3 + fRec1[1]));
fRec1[0] = (fTemp4 + (fTemp2 + fTemp3));
fRec2[0] = (fTemp4 - fRec1[1]);
fVec4[0] = fConst5;
float fTemp5 = (fConst6 * (fRec4[1] * (fTemp2 + fVec4[1])));
float fTemp6 = (fConst4 * (fTemp5 + fRec3[1]));
fRec3[0] = (fTemp6 + (fTemp2 + fTemp5));
fRec4[0] = (fTemp6 - fRec3[1]);
iRec5[0] = (((iRec5[1] + (iRec5[1] > 0)) * (iTemp1 <= iVec2[1])) + (iTemp1 > iVec2[1]));
float fTemp7 = float(iRec5[0]);
float fTemp8 = (fSlow0 * ((iRec0[0] ? (0.0f - (fConst5 * fRec4[0])) : (0.0f - (fConst2 * fRec2[0]))) * std::max<float>(0.0f, std::min<float>((fConst8 * fTemp7), ((fConst9 * (fConst7 - fTemp7)) + 1.0f)))));
outputL[i] += float(fTemp8);
outputR[i] += float(fTemp8);
iVec0[1] = iVec0[0];
iVec1[1] = iVec1[0];
iVec2[1] = iVec2[0];
iRec0[1] = iRec0[0];
fVec3[1] = fVec3[0];
fRec1[1] = fRec1[0];
fRec2[1] = fRec2[0];
fVec4[1] = fVec4[0];
fRec3[1] = fRec3[0];
fRec4[1] = fRec4[0];
iRec5[1] = iRec5[0];
}
}
} // namespace sfz

59
src/sfizz/Metronome.h Normal file
View file

@ -0,0 +1,59 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include <algorithm>
#include <cmath>
namespace sfz {
class Metronome {
public:
Metronome();
void init(float sampleRate);
void clear();
void processAdding(const int* beats, const int* beatsPerBar, float* outputL, float* outputR, int numFrames);
void setGain(float gain) { fGain = gain; }
private:
float fGain;
int iVec0[2];
int iVec1[2];
int iVec2[2];
int iRec0[2];
float fConst0;
float fConst1;
float fConst2;
float fVec3[2];
float fConst3;
float fRec1[2];
float fRec2[2];
float fConst4;
float fConst5;
float fVec4[2];
float fConst6;
float fRec3[2];
float fRec4[2];
float fConst7;
float fConst8;
int iRec5[2];
float fConst9;
/*
import("stdfaust.lib");
process(beats, beatsPerBar) = tone : *(envelope) <: (_, _) with {
gain = hslider("[1] Gain", 0.5, 0.0, 1.0, 0.001);
beatNumber = int(beats);
beatIncrement = beatNumber-beatNumber';
tone = (os.oscws(440.0), os.oscws(880.0)) : select2(toneSelect);
toneSelect = x letrec { 'x = ba.if(beatIncrement>0, (beatNumber%int(beatsPerBar))==0, x); };
envelope = (beatIncrement>0) : en.ar(5e-3, 100e-3) : *(gain);
};
*/
};
} // namespace sfz

View file

@ -866,6 +866,36 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
setValueFromOpcode(opcode, lfos[lfoNumber - 1].freq, Default::lfoFreqRange);
}
break;
case_any_ccN("lfo&_freq"):
{
const auto lfoNumber = opcode.parameters.front();
if (lfoNumber == 0)
return false;
if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs))
return false;
processGenericCc(opcode, Default::lfoFreqModRange, ModKey::createNXYZ(ModId::LFOFrequency, id, lfoNumber - 1));
}
break;
case hash("lfo&_beats"):
{
const auto lfoNumber = opcode.parameters.front();
if (lfoNumber == 0)
return false;
if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs))
return false;
setValueFromOpcode(opcode, lfos[lfoNumber - 1].beats, Default::lfoBeatsRange);
}
break;
case_any_ccN("lfo&_beats"):
{
const auto lfoNumber = opcode.parameters.front();
if (lfoNumber == 0)
return false;
if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs))
return false;
processGenericCc(opcode, Default::lfoBeatsModRange, ModKey::createNXYZ(ModId::LFOBeats, id, lfoNumber - 1));
}
break;
case hash("lfo&_phase"):
{
const auto lfoNumber = opcode.parameters.front();

View file

@ -13,6 +13,8 @@
#include "Wavetables.h"
#include "Curve.h"
#include "Tuning.h"
#include "BeatClock.h"
#include "Metronome.h"
#include "modulations/ModMatrix.h"
#include "absl/types/optional.h"
@ -32,11 +34,15 @@ struct Resources
Tuning tuning;
absl::optional<StretchTuning> stretch;
ModMatrix modMatrix;
BeatClock beatClock;
Metronome metronome;
void setSampleRate(float samplerate)
{
midiState.setSampleRate(samplerate);
modMatrix.setSampleRate(samplerate);
beatClock.setSampleRate(samplerate);
metronome.init(samplerate);
}
void setSamplesPerBlock(int samplesPerBlock)
@ -44,6 +50,7 @@ struct Resources
bufferPool.setBufferSize(samplesPerBlock);
midiState.setSamplesPerBlock(samplesPerBlock);
modMatrix.setSamplesPerBlock(samplesPerBlock);
beatClock.setSamplesPerBlock(samplesPerBlock);
}
void clear()
@ -54,6 +61,8 @@ struct Resources
logger.clear();
midiState.reset();
modMatrix.clear();
beatClock.clear();
metronome.clear();
}
};
}

View file

@ -859,6 +859,9 @@ void Synth::renderBlock(AudioSpan<float> buffer) noexcept
ModMatrix& mm = impl.resources_.modMatrix;
mm.beginCycle(numFrames);
BeatClock& bc = impl.resources_.beatClock;
bc.beginCycle(numFrames);
{ // Clear effect busses
ScopedTiming logger { callbackBreakdown.effects };
for (auto& bus : impl.effectBuses_) {
@ -921,9 +924,20 @@ void Synth::renderBlock(AudioSpan<float> buffer) noexcept
// Apply the master volume
buffer.applyGain(db2mag(impl.volume_));
// Process the metronome (debugging tool for host time info)
constexpr bool metronomeEnabled = false;
if (metronomeEnabled) {
impl.resources_.metronome.processAdding(
bc.getRunningBeatNumber().data(), bc.getRunningBeatsPerBar().data(),
buffer.getChannel(0), buffer.getChannel(1), numFrames);
}
// Perform any remaining modulators
mm.endCycle();
// Advance the clock to the end of cycle
bc.endCycle();
{ // Clear events and advance midi time
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
impl.resources_.midiState.advanceTime(buffer.getNumFrames());
@ -1183,36 +1197,33 @@ void Synth::aftertouch(int /* delay */, uint8_t /* aftertouch */) noexcept
Impl& impl = *impl_;
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
}
void Synth::tempo(int /* delay */, float /* secondsPerQuarter */) noexcept
void Synth::tempo(int delay, float secondsPerBeat) noexcept
{
Impl& impl = *impl_;
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
impl.resources_.beatClock.setTempo(delay, secondsPerBeat);
}
void Synth::timeSignature(int delay, int beatsPerBar, int beatUnit)
{
Impl& impl = *impl_;
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
(void)delay;
(void)beatsPerBar;
(void)beatUnit;
impl.resources_.beatClock.setTimeSignature(delay, TimeSignature(beatsPerBar, beatUnit));
}
void Synth::timePosition(int delay, int bar, float barBeat)
void Synth::timePosition(int delay, int bar, double barBeat)
{
Impl& impl = *impl_;
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
(void)delay;
(void)bar;
(void)barBeat;
impl.resources_.beatClock.setTimePosition(delay, BBT(bar, barBeat));
}
void Synth::playbackState(int delay, int playbackState)
{
Impl& impl = *impl_;
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
(void)delay;
(void)playbackState;
impl.resources_.beatClock.setPlaying(delay, playbackState == 1);
}
int Synth::getNumRegions() const noexcept

View file

@ -405,7 +405,7 @@ public:
* @param bar The current bar.
* @param bar_beat The fractional position of the current beat within the bar.
*/
void timePosition(int delay, int bar, float barBeat);
void timePosition(int delay, int bar, double barBeat);
/**
* @brief Send the playback state.
*

View file

@ -1475,10 +1475,13 @@ void Voice::setMaxEQsPerVoice(size_t numFilters)
void Voice::setMaxLFOsPerVoice(size_t numLFOs)
{
Impl& impl = *impl_;
Resources& resources = impl.resources_;
impl.lfos_.resize(numLFOs);
for (size_t i = 0; i < numLFOs; ++i) {
auto lfo = absl::make_unique<LFO>();
const NumericId<LFO> id { static_cast<int>(i) };
auto lfo = absl::make_unique<LFO>(id, resources.bufferPool, &resources.beatClock, &resources.modMatrix);
lfo->setSampleRate(impl.sampleRate_);
impl.lfos_[i] = std::move(lfo);
}

View file

@ -68,6 +68,10 @@ int ModIds::flags(ModId id) noexcept
return kModIsPerVoice|kModIsAdditive;
case ModId::OscillatorModDepth:
return kModIsPerVoice|kModIsPercentMultiplicative;
case ModId::LFOFrequency:
return kModIsPerVoice|kModIsAdditive;
case ModId::LFOBeats:
return kModIsPerVoice|kModIsAdditive;
// unknown
default:

View file

@ -49,6 +49,8 @@ enum class ModId : int {
EqBandwidth,
OscillatorDetune,
OscillatorModDepth,
LFOFrequency,
LFOBeats,
_TargetsEnd,
// [/targets] --------------------------------------------------------------

View file

@ -136,6 +136,10 @@ std::string ModKey::toString() const
return absl::StrCat("OscillatorDetune {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::OscillatorModDepth:
return absl::StrCat("OscillatorModDepth {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::LFOFrequency:
return absl::StrCat("LFOFrequency {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::LFOBeats:
return absl::StrCat("LFOBeats {", region_.number(), ", N=", 1 + params_.N, "}");
default:
return {};

View file

@ -59,7 +59,7 @@ void LFOSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl
}
LFO* lfo = voice->getLFO(lfoIndex);
lfo->process(buffer);
lfo->process(buffer, region->getId());
}
} // namespace sfz

View file

@ -169,7 +169,7 @@ void sfz::Sfizz::timeSignature(int delay, int beatsPerBar, int beatUnit)
synth->timeSignature(delay, beatsPerBar, beatUnit);
}
void sfz::Sfizz::timePosition(int delay, int bar, float barBeat)
void sfz::Sfizz::timePosition(int delay, int bar, double barBeat)
{
synth->timePosition(delay, bar, barBeat);
}

View file

@ -171,7 +171,7 @@ void sfizz_send_time_signature(sfizz_synth_t* synth, int delay, int beats_per_ba
auto* self = reinterpret_cast<sfz::Synth*>(synth);
self->timeSignature(delay, beats_per_bar, beat_unit);
}
void sfizz_send_time_position(sfizz_synth_t* synth, int delay, int bar, float bar_beat)
void sfizz_send_time_position(sfizz_synth_t* synth, int delay, int bar, double bar_beat)
{
auto* self = reinterpret_cast<sfz::Synth*>(synth);
self->timePosition(delay, bar, bar_beat);

View file

@ -13,6 +13,7 @@
static bool computeLFO(DataPoints& dp, const fs::path& sfzPath, double sampleRate, size_t numFrames)
{
sfz::Synth synth;
sfz::Resources& resources = synth.getResources();
if (!synth.loadSfzFile(sfzPath))
return false;
@ -22,23 +23,26 @@ static bool computeLFO(DataPoints& dp, const fs::path& sfzPath, double sampleRat
const std::vector<sfz::LFODescription>& desc = synth.getRegionView(0)->lfos;
size_t numLfos = desc.size();
std::vector<sfz::LFO> lfos(numLfos);
std::vector<std::unique_ptr<sfz::LFO>> lfos(numLfos);
for (size_t l = 0; l < numLfos; ++l) {
lfos[l].setSampleRate(sampleRate);
lfos[l].configure(&desc[l]);
const NumericId<sfz::LFO> id { static_cast<int>(l) };
sfz::LFO* lfo = new sfz::LFO(id, resources.bufferPool);
lfos[l].reset(lfo);
lfo->setSampleRate(sampleRate);
lfo->configure(&desc[l]);
}
std::vector<float> outputMemory(numLfos * numFrames);
for (size_t l = 0; l < numLfos; ++l) {
lfos[l].start(0);
lfos[l]->start(0);
}
std::vector<absl::Span<float>> lfoOutputs(numLfos);
for (size_t l = 0; l < numLfos; ++l) {
lfoOutputs[l] = absl::MakeSpan(&outputMemory[l * numFrames], numFrames);
lfos[l].process(lfoOutputs[l]);
lfos[l]->process(lfoOutputs[l]);
}
dp.rows = numFrames;

View file

@ -297,7 +297,7 @@ void SfizzVstProcessor::updateTimeInfo(const Vst::ProcessContext& context)
double beats = context.projectTimeMusic * 0.25 * _timeSigDenominator;
double bars = beats / _timeSigNumerator;
beats -= int(bars) * _timeSigNumerator;
synth.timePosition(0, int(bars), float(beats));
synth.timePosition(0, int(bars), beats);
}
synth.playbackState(0, (context.state & context.kPlaying) != 0);