| @@ -1344,19 +1344,18 @@ void CarlaEngine::setFileCallback(const FileCallbackFunc func, void* const ptr) | |||
| void CarlaEngine::transportPlay() noexcept | |||
| { | |||
| pData->timeInfo.playing = true; | |||
| pData->time.fillEngineTimeInfo(0); | |||
| pData->time.setNeedsReset(); | |||
| } | |||
| void CarlaEngine::transportPause() noexcept | |||
| { | |||
| pData->timeInfo.playing = false; | |||
| pData->time.fillEngineTimeInfo(0); | |||
| pData->time.setNeedsReset(); | |||
| } | |||
| void CarlaEngine::transportRelocate(const uint64_t frame) noexcept | |||
| { | |||
| pData->timeInfo.frame = frame; | |||
| pData->time.fillEngineTimeInfo(0); | |||
| pData->time.relocate(frame); | |||
| } | |||
| // ----------------------------------------------------------------------- | |||
| @@ -1415,10 +1414,13 @@ void CarlaEngine::setOption(const EngineOption option, const int value, const ch | |||
| case ENGINE_OPTION_TRANSPORT_MODE: | |||
| CARLA_SAFE_ASSERT_RETURN(value >= ENGINE_TRANSPORT_MODE_INTERNAL && value <= ENGINE_TRANSPORT_MODE_BRIDGE,); | |||
| CARLA_SAFE_ASSERT_RETURN(getType() == kEngineTypeJack || value != ENGINE_TRANSPORT_MODE_JACK,); | |||
| pData->options.transportMode = static_cast<EngineTransportMode>(value); | |||
| delete[] pData->options.transportExtra; | |||
| pData->options.transportExtra = (valueStr != nullptr) ? carla_strdup_safe(valueStr) : nullptr; | |||
| pData->time.setNeedsReset(); | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| // enable link now if needed | |||
| { | |||
| @@ -1659,9 +1661,6 @@ void CarlaEngine::bufferSizeChanged(const uint32_t newBufferSize) | |||
| pData->time.updateAudioValues(newBufferSize, pData->sampleRate); | |||
| if (pData->options.transportMode == ENGINE_TRANSPORT_MODE_INTERNAL) | |||
| pData->time.fillEngineTimeInfo(0); | |||
| for (uint i=0; i < pData->curPluginCount; ++i) | |||
| { | |||
| CarlaPlugin* const plugin(pData->plugins[i].plugin); | |||
| @@ -1687,9 +1686,6 @@ void CarlaEngine::sampleRateChanged(const double newSampleRate) | |||
| pData->time.updateAudioValues(pData->bufferSize, newSampleRate); | |||
| if (pData->options.transportMode == ENGINE_TRANSPORT_MODE_INTERNAL) | |||
| pData->time.fillEngineTimeInfo(0); | |||
| for (uint i=0; i < pData->curPluginCount; ++i) | |||
| { | |||
| CarlaPlugin* const plugin(pData->plugins[i].plugin); | |||
| @@ -61,72 +61,60 @@ void EngineInternalEvents::clear() noexcept | |||
| static const float kTicksPerBeat = 1920.0f; | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| static uint32_t calculate_link_latency(const double bufferSize, const double sampleRate) noexcept | |||
| { | |||
| CARLA_SAFE_ASSERT_RETURN(carla_isNotZero(sampleRate), 0); | |||
| const long long int latency = llround(1.0e6 * bufferSize / sampleRate); | |||
| CARLA_SAFE_ASSERT_RETURN(latency >= 0 && latency < UINT32_MAX, 0); | |||
| return static_cast<uint32_t>(latency); | |||
| } | |||
| #endif | |||
| EngineInternalTime::EngineInternalTime(EngineTimeInfo& ti, const EngineTransportMode& tm) noexcept | |||
| : bpm(120.0), | |||
| : beatsPerBar(4.0), | |||
| beatsPerMinute(120.0), | |||
| bufferSize(0.0), | |||
| sampleRate(0.0), | |||
| tick(0.0), | |||
| needsReset(true), | |||
| needsReset(false), | |||
| nextFrame(0), | |||
| timeInfo(ti), | |||
| transportMode(tm) {} | |||
| EngineInternalTime::~EngineInternalTime() noexcept | |||
| { | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| hylia_cleanup(hylia.instance); | |||
| #endif | |||
| } | |||
| void EngineInternalTime::init(const uint32_t bufferSize, const double sr) | |||
| void EngineInternalTime::init(const uint32_t bsize, const double srate) | |||
| { | |||
| sampleRate = sr; | |||
| bufferSize = bsize; | |||
| sampleRate = srate; | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| hylia.instance = hylia_create(bpm, bufferSize, sr); | |||
| if (hylia.instance != nullptr) | |||
| { | |||
| hylia_set_beats_per_bar(hylia.instance, beatsPerBar); | |||
| hylia_set_beats_per_minute(hylia.instance, beatsPerMinute); | |||
| hylia_set_output_latency(hylia.instance, calculate_link_latency(bsize, srate)); | |||
| if (hylia.enabled) | |||
| hylia_enable(hylia.instance, true, bpm); | |||
| if (hylia.enabled) | |||
| hylia_enable(hylia.instance, true); | |||
| } | |||
| #endif | |||
| fillEngineTimeInfo(0); | |||
| needsReset = true; | |||
| } | |||
| void EngineInternalTime::updateAudioValues(const uint32_t bufferSize, double sampleRate) | |||
| void EngineInternalTime::updateAudioValues(const uint32_t bsize, const double srate) | |||
| { | |||
| // TODO | |||
| } | |||
| bufferSize = bsize; | |||
| sampleRate = srate; | |||
| void EngineInternalTime::preProcess(const uint32_t numFrames) | |||
| { | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| if (hylia.enabled) | |||
| { | |||
| hylia_process(hylia.instance, numFrames, &hylia.timeInfo); | |||
| const double new_bpm = hylia.timeInfo.bpm; | |||
| if (new_bpm > 0.0 && bpm != new_bpm) | |||
| { | |||
| bpm = new_bpm; | |||
| needsReset = true; | |||
| if (transportMode == ENGINE_TRANSPORT_MODE_INTERNAL) | |||
| fillEngineTimeInfo(0); | |||
| } | |||
| } | |||
| #else | |||
| return; | |||
| // unused | |||
| (void)numFrames; | |||
| if (hylia.instance != nullptr) | |||
| hylia_set_output_latency(hylia.instance, calculate_link_latency(bsize, srate)); | |||
| #endif | |||
| } | |||
| void EngineInternalTime::postProcess(const uint32_t numFrames) | |||
| { | |||
| if (timeInfo.playing && transportMode == ENGINE_TRANSPORT_MODE_INTERNAL) | |||
| { | |||
| timeInfo.frame += numFrames; | |||
| fillEngineTimeInfo(numFrames); | |||
| } | |||
| needsReset = true; | |||
| } | |||
| void EngineInternalTime::enableLink(const bool enable) | |||
| @@ -135,146 +123,221 @@ void EngineInternalTime::enableLink(const bool enable) | |||
| if (hylia.enabled == enable) | |||
| return; | |||
| hylia.enabled = enable; | |||
| if (hylia.instance != nullptr) | |||
| hylia_enable(hylia.instance, enable, bpm); | |||
| { | |||
| hylia.enabled = enable; | |||
| hylia_enable(hylia.instance, enable); | |||
| } | |||
| #else | |||
| return; | |||
| // unused | |||
| (void)enable; | |||
| #endif | |||
| needsReset = true; | |||
| } | |||
| void EngineInternalTime::setNeedsReset() | |||
| void EngineInternalTime::setNeedsReset() noexcept | |||
| { | |||
| needsReset = true; | |||
| } | |||
| void EngineInternalTime::relocate(const uint64_t frame) noexcept | |||
| { | |||
| timeInfo.frame = frame; | |||
| nextFrame = frame; | |||
| needsReset = true; | |||
| } | |||
| void EngineInternalTime::fillEngineTimeInfo(const uint32_t newFrames) noexcept | |||
| { | |||
| CARLA_SAFE_ASSERT_RETURN(carla_isNotZero(sampleRate),); | |||
| CARLA_SAFE_ASSERT_RETURN(newFrames > 0,); | |||
| timeInfo.usecs = 0; | |||
| if (newFrames == 0 || needsReset) | |||
| if (transportMode == ENGINE_TRANSPORT_MODE_INTERNAL) | |||
| timeInfo.frame = nextFrame; | |||
| if (needsReset) | |||
| { | |||
| timeInfo.valid = EngineTimeInfo::kValidBBT; | |||
| timeInfo.bbt.beatsPerBar = 4.0f; | |||
| timeInfo.bbt.beatsPerBar = beatsPerBar; | |||
| timeInfo.bbt.beatType = 4.0f; | |||
| timeInfo.bbt.ticksPerBeat = kTicksPerBeat; | |||
| timeInfo.bbt.beatsPerMinute = bpm; | |||
| timeInfo.bbt.beatsPerMinute = beatsPerMinute; | |||
| double abs_beat, abs_tick; | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| if (hylia.enabled && hylia.timeInfo.beats >= 0.0) | |||
| if (hylia.enabled) | |||
| { | |||
| const double beats = hylia.timeInfo.beats; | |||
| abs_beat = std::floor(beats); | |||
| abs_tick = beats * kTicksPerBeat; | |||
| if (hylia.timeInfo.beat >= 0.0) | |||
| { | |||
| const double beat = hylia.timeInfo.beat; | |||
| abs_beat = std::floor(beat); | |||
| abs_tick = beat * kTicksPerBeat; | |||
| } | |||
| else | |||
| { | |||
| abs_beat = 0.0; | |||
| abs_tick = 0.0; | |||
| timeInfo.playing = false; | |||
| } | |||
| } | |||
| else | |||
| #endif | |||
| { | |||
| const double min = timeInfo.frame / (sampleRate * 60.0); | |||
| abs_tick = min * bpm * kTicksPerBeat; | |||
| abs_tick = min * beatsPerMinute * kTicksPerBeat; | |||
| abs_beat = abs_tick / kTicksPerBeat; | |||
| needsReset = false; | |||
| } | |||
| timeInfo.bbt.bar = abs_beat / timeInfo.bbt.beatsPerBar; | |||
| timeInfo.bbt.bar = (int32_t)(std::floor(abs_beat / timeInfo.bbt.beatsPerBar) + 0.5); | |||
| timeInfo.bbt.beat = abs_beat - (timeInfo.bbt.bar * timeInfo.bbt.beatsPerBar) + 1; | |||
| tick = abs_tick - (abs_beat * kTicksPerBeat); | |||
| timeInfo.bbt.barStartTick = timeInfo.bbt.bar * timeInfo.bbt.beatsPerBar * kTicksPerBeat; | |||
| timeInfo.bbt.barStartTick = timeInfo.bbt.bar * beatsPerBar * kTicksPerBeat; | |||
| timeInfo.bbt.bar++; | |||
| tick = abs_tick - timeInfo.bbt.barStartTick; | |||
| } | |||
| else | |||
| { | |||
| tick += newFrames * kTicksPerBeat * bpm / (sampleRate * 60); | |||
| tick += newFrames * kTicksPerBeat * beatsPerMinute / (sampleRate * 60); | |||
| while (tick >= kTicksPerBeat) | |||
| { | |||
| tick -= kTicksPerBeat; | |||
| if (++timeInfo.bbt.beat > timeInfo.bbt.beatsPerBar) | |||
| if (++timeInfo.bbt.beat > beatsPerBar) | |||
| { | |||
| timeInfo.bbt.beat = 1; | |||
| ++timeInfo.bbt.bar; | |||
| timeInfo.bbt.barStartTick += timeInfo.bbt.beatsPerBar * kTicksPerBeat; | |||
| timeInfo.bbt.barStartTick += beatsPerBar * kTicksPerBeat; | |||
| } | |||
| } | |||
| } | |||
| timeInfo.bbt.tick = (int)(tick + 0.5); | |||
| needsReset = false; | |||
| timeInfo.bbt.tick = (int32_t)(tick + 0.5); | |||
| if (transportMode == ENGINE_TRANSPORT_MODE_INTERNAL && timeInfo.playing) | |||
| nextFrame += newFrames; | |||
| } | |||
| void EngineInternalTime::fillJackTimeInfo(jack_position_t* const pos, const uint32_t newFrames) noexcept | |||
| { | |||
| CARLA_SAFE_ASSERT_RETURN(carla_isNotZero(sampleRate),); | |||
| CARLA_SAFE_ASSERT_RETURN(newFrames > 0,); | |||
| if (newFrames == 0 || needsReset) | |||
| if (needsReset) | |||
| { | |||
| pos->valid = JackPositionBBT; | |||
| pos->beats_per_bar = 4.0f; | |||
| pos->beats_per_bar = beatsPerBar; | |||
| pos->beat_type = 4.0f; | |||
| pos->ticks_per_beat = kTicksPerBeat; | |||
| pos->beats_per_minute = bpm; | |||
| pos->beats_per_minute = beatsPerMinute; | |||
| double abs_beat, abs_tick; | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| if (hylia.enabled && hylia.timeInfo.beats >= 0.0) | |||
| if (hylia.enabled) | |||
| { | |||
| const double beats = hylia.timeInfo.beats; | |||
| abs_beat = std::floor(beats); | |||
| abs_tick = beats * kTicksPerBeat; | |||
| if (hylia.timeInfo.beat >= 0.0) | |||
| { | |||
| const double beat = hylia.timeInfo.beat; | |||
| abs_beat = std::floor(beat); | |||
| abs_tick = beat * kTicksPerBeat; | |||
| } | |||
| else | |||
| { | |||
| abs_beat = 0.0; | |||
| abs_tick = 0.0; | |||
| timeInfo.playing = false; | |||
| } | |||
| } | |||
| else | |||
| #endif | |||
| { | |||
| const double min = pos->frame / (sampleRate * 60.0); | |||
| abs_tick = min * bpm * kTicksPerBeat; | |||
| const double min = static_cast<double>(pos->frame) / (sampleRate * 60.0); | |||
| abs_tick = min * beatsPerMinute * kTicksPerBeat; | |||
| abs_beat = abs_tick / kTicksPerBeat; | |||
| needsReset = false; | |||
| } | |||
| pos->bar = abs_beat / pos->beats_per_bar; | |||
| pos->bar = (int32_t)(std::floor(abs_beat / pos->beats_per_bar) + 0.5); | |||
| pos->beat = abs_beat - (pos->bar * pos->beats_per_bar) + 1; | |||
| tick = abs_tick - (abs_beat * kTicksPerBeat); | |||
| pos->bar_start_tick = pos->bar * pos->beats_per_bar * kTicksPerBeat; | |||
| pos->bar++; | |||
| tick = abs_tick - pos->bar_start_tick; | |||
| } | |||
| else | |||
| { | |||
| tick += newFrames * kTicksPerBeat * bpm / (sampleRate * 60); | |||
| tick += newFrames * kTicksPerBeat * beatsPerMinute / (sampleRate * 60); | |||
| while (tick >= kTicksPerBeat) | |||
| { | |||
| tick -= kTicksPerBeat; | |||
| if (++pos->beat > pos->beats_per_bar) | |||
| if (++pos->beat > beatsPerBar) | |||
| { | |||
| pos->beat = 1; | |||
| ++pos->bar; | |||
| pos->bar_start_tick += pos->beats_per_bar * pos->ticks_per_beat; | |||
| pos->bar_start_tick += beatsPerBar * kTicksPerBeat; | |||
| } | |||
| } | |||
| } | |||
| pos->tick = (int)(tick + 0.5); | |||
| needsReset = false; | |||
| pos->tick = (int32_t)(tick + 0.5); | |||
| } | |||
| void EngineInternalTime::preProcess(const uint32_t numFrames) | |||
| { | |||
| #if defined(HAVE_HYLIA) && !defined(BUILD_BRIDGE) | |||
| if (hylia.enabled) | |||
| { | |||
| hylia_process(hylia.instance, numFrames, &hylia.timeInfo); | |||
| const double new_bpb = hylia.timeInfo.beatsPerBar; | |||
| const double new_bpm = hylia.timeInfo.beatsPerMinute; | |||
| if (new_bpb >= 1.0 && beatsPerBar != new_bpb) | |||
| { | |||
| beatsPerBar = new_bpb; | |||
| needsReset = true; | |||
| } | |||
| if (new_bpm > 0.0 && beatsPerMinute != new_bpm) | |||
| { | |||
| beatsPerMinute = new_bpm; | |||
| needsReset = true; | |||
| } | |||
| } | |||
| #endif | |||
| if (transportMode == ENGINE_TRANSPORT_MODE_INTERNAL) | |||
| fillEngineTimeInfo(numFrames); | |||
| } | |||
| // ----------------------------------------------------------------------- | |||
| // EngineInternalTime::Hylia | |||
| #ifndef BUILD_BRIDGE | |||
| EngineInternalTime::Hylia::Hylia() | |||
| : enabled(false), | |||
| # ifdef HAVE_HYLIA | |||
| instance(hylia_create()) | |||
| # else | |||
| instance(nullptr) | |||
| # endif | |||
| { | |||
| carla_zeroStruct(timeInfo); | |||
| } | |||
| EngineInternalTime::Hylia::~Hylia() | |||
| { | |||
| # ifdef HAVE_HYLIA | |||
| hylia_cleanup(instance); | |||
| # endif | |||
| } | |||
| #endif | |||
| // ----------------------------------------------------------------------- | |||
| @@ -580,12 +643,6 @@ PendingRtEventsRunner::PendingRtEventsRunner(CarlaEngine* const engine, const ui | |||
| PendingRtEventsRunner::~PendingRtEventsRunner() noexcept | |||
| { | |||
| pData->doNextPluginAction(true); | |||
| if (pData->timeInfo.playing && pData->options.transportMode == ENGINE_TRANSPORT_MODE_INTERNAL) | |||
| { | |||
| pData->timeInfo.frame += numFrames; | |||
| pData->time.fillEngineTimeInfo(numFrames); | |||
| } | |||
| } | |||
| // ----------------------------------------------------------------------- | |||
| @@ -116,38 +116,44 @@ private: | |||
| class EngineInternalTime { | |||
| public: | |||
| EngineInternalTime(EngineTimeInfo& timeInfo, const EngineTransportMode& transportMode) noexcept; | |||
| ~EngineInternalTime() noexcept; | |||
| void init(const uint32_t bufferSize, double sampleRate); | |||
| void updateAudioValues(const uint32_t bufferSize, double sampleRate); | |||
| void preProcess(const uint32_t numFrames); | |||
| void postProcess(const uint32_t numFrames); | |||
| void updateAudioValues(const uint32_t bufferSize, const double sampleRate); | |||
| void enableLink(const bool enable); | |||
| void setNeedsReset(); | |||
| void fillEngineTimeInfo(const uint32_t newFrames) noexcept; | |||
| void fillJackTimeInfo(jack_position_t* const pos, const uint32_t newFrames) noexcept; | |||
| void setNeedsReset() noexcept; | |||
| void relocate(const uint64_t frame) noexcept; | |||
| private: | |||
| double bpm; | |||
| double beatsPerBar; | |||
| double beatsPerMinute; | |||
| double bufferSize; | |||
| double sampleRate; | |||
| double tick; | |||
| bool needsReset; | |||
| uint64_t nextFrame; | |||
| #ifndef BUILD_BRIDGE | |||
| struct Hylia { | |||
| bool enabled; | |||
| hylia_t* instance; | |||
| hylia_time_info_t timeInfo; | |||
| Hylia(); | |||
| ~Hylia(); | |||
| } hylia; | |||
| #endif | |||
| EngineTimeInfo& timeInfo; | |||
| const EngineTransportMode& transportMode; | |||
| friend class PendingRtEventsRunner; | |||
| void preProcess(const uint32_t numFrames); | |||
| void fillEngineTimeInfo(const uint32_t newFrames) noexcept; | |||
| friend class CarlaEngineJack; | |||
| void fillJackTimeInfo(jack_position_t* const pos, const uint32_t newFrames) noexcept; | |||
| CARLA_DECLARE_NON_COPY_STRUCT(EngineInternalTime) | |||
| }; | |||
| @@ -1725,7 +1725,7 @@ protected: | |||
| void handleJackTimebaseCallback(jack_nframes_t nframes, jack_position_t* const pos, const int new_pos) | |||
| { | |||
| if (new_pos) | |||
| nframes = 0; | |||
| pData->time.setNeedsReset(); | |||
| pData->time.fillJackTimeInfo(pos, nframes); | |||
| } | |||
| @@ -23,56 +23,60 @@ | |||
| class HyliaTransport { | |||
| public: | |||
| HyliaTransport(double bpm, double bufferSize, double sampleRate) | |||
| : link(bpm), | |||
| HyliaTransport() | |||
| : link(120.0), | |||
| engine(link), | |||
| outputLatency(0), | |||
| sampleTime(0) | |||
| { | |||
| outputLatency = std::chrono::microseconds(llround(1.0e6 * bufferSize / sampleRate)); | |||
| } | |||
| void setEnabled(bool enabled, double bpm) | |||
| void setEnabled(const bool enabled) | |||
| { | |||
| link.enable(enabled); | |||
| if (enabled) | |||
| { | |||
| sampleTime = 0; | |||
| engine.setTempo(bpm); | |||
| } | |||
| link.enable(enabled); | |||
| } | |||
| void setTempo(double tempo) | |||
| void setQuantum(const double quantum) | |||
| { | |||
| engine.setQuantum(quantum); | |||
| } | |||
| void setTempo(const double tempo) | |||
| { | |||
| engine.setTempo(tempo); | |||
| } | |||
| void process(uint32_t frames, LinkTimeInfo* info) | |||
| void setOutputLatency(const uint32_t latency) noexcept | |||
| { | |||
| const auto hostTime = hostTimeFilter.sampleTimeToHostTime(sampleTime); | |||
| const auto bufferBeginAtOutput = hostTime + outputLatency; | |||
| outputLatency = latency; | |||
| } | |||
| engine.timelineCallback(bufferBeginAtOutput, info); | |||
| void process(const uint32_t frames, LinkTimeInfo* const info) | |||
| { | |||
| const std::chrono::microseconds hostTime = hostTimeFilter.sampleTimeToHostTime(sampleTime) | |||
| + std::chrono::microseconds(outputLatency); | |||
| engine.timelineCallback(hostTime, info); | |||
| sampleTime += frames; | |||
| } | |||
| private: | |||
| ableton::Link link; | |||
| ableton::linkaudio::AudioEngine engine; | |||
| ableton::link::AudioEngine engine; | |||
| ableton::link::HostTimeFilter<ableton::link::platform::Clock> hostTimeFilter; | |||
| ableton::link::HostTimeFilter<ableton::platforms::stl::Clock> hostTimeFilter; | |||
| std::chrono::microseconds outputLatency; | |||
| uint32_t sampleTime; | |||
| uint32_t outputLatency, sampleTime; | |||
| }; | |||
| hylia_t* hylia_create(double bpm, uint32_t buffer_size, uint32_t sample_rate) | |||
| hylia_t* hylia_create(void) | |||
| { | |||
| HyliaTransport* t; | |||
| try { | |||
| t = new HyliaTransport(bpm, buffer_size, sample_rate); | |||
| t = new HyliaTransport(); | |||
| } catch (...) { | |||
| return nullptr; | |||
| } | |||
| @@ -80,14 +84,24 @@ hylia_t* hylia_create(double bpm, uint32_t buffer_size, uint32_t sample_rate) | |||
| return (hylia_t*)t; | |||
| } | |||
| void hylia_enable(hylia_t* link, bool on, double bpm) | |||
| void hylia_enable(hylia_t* link, bool on) | |||
| { | |||
| ((HyliaTransport*)link)->setEnabled(on); | |||
| } | |||
| void hylia_set_beats_per_bar(hylia_t* link, double beatsPerBar) | |||
| { | |||
| ((HyliaTransport*)link)->setQuantum(beatsPerBar); | |||
| } | |||
| void hylia_set_beats_per_minute(hylia_t* link, double beatsPerMinute) | |||
| { | |||
| ((HyliaTransport*)link)->setEnabled(on, bpm); | |||
| ((HyliaTransport*)link)->setTempo(beatsPerMinute); | |||
| } | |||
| void hylia_set_tempo(hylia_t* link, double bpm) | |||
| void hylia_set_output_latency(hylia_t* link, uint32_t latency) | |||
| { | |||
| ((HyliaTransport*)link)->setTempo(bpm); | |||
| ((HyliaTransport*)link)->setOutputLatency(latency); | |||
| } | |||
| void hylia_process(hylia_t* link, uint32_t frames, hylia_time_info_t* info) | |||
| @@ -29,13 +29,15 @@ extern "C" { | |||
| typedef struct _hylia_t hylia_t; | |||
| typedef struct _hylia_time_info_t { | |||
| double bpm, beats, phase; | |||
| double beatsPerBar, beatsPerMinute, beat, phase; | |||
| } hylia_time_info_t; | |||
| hylia_t* hylia_create(double bpm, uint32_t buffer_size, uint32_t sample_rate); | |||
| void hylia_enable(hylia_t* link, bool on, double bpm); | |||
| hylia_t* hylia_create(void); | |||
| void hylia_enable(hylia_t* link, bool on); | |||
| void hylia_process(hylia_t* link, uint32_t frames, hylia_time_info_t* info); | |||
| void hylia_set_tempo(hylia_t* link, double tempo); | |||
| void hylia_set_beats_per_bar(hylia_t* link, double beatsPerBar); | |||
| void hylia_set_beats_per_minute(hylia_t* link, double beatsPerMinute); | |||
| void hylia_set_output_latency(hylia_t* link, uint32_t latency); | |||
| void hylia_cleanup(hylia_t* link); | |||
| #ifdef __cplusplus | |||
| @@ -25,7 +25,7 @@ | |||
| namespace ableton | |||
| { | |||
| namespace linkaudio | |||
| namespace link | |||
| { | |||
| AudioEngine::AudioEngine(Link& link) | |||
| @@ -42,7 +42,7 @@ double AudioEngine::beatTime() const | |||
| void AudioEngine::setTempo(double tempo) | |||
| { | |||
| std::lock_guard<std::mutex> lock(mEngineDataGuard); | |||
| const std::lock_guard<std::mutex> lock(mEngineDataGuard); | |||
| mSharedEngineData.requestedTempo = tempo; | |||
| } | |||
| @@ -53,7 +53,7 @@ double AudioEngine::quantum() const | |||
| void AudioEngine::setQuantum(double quantum) | |||
| { | |||
| std::lock_guard<std::mutex> lock(mEngineDataGuard); | |||
| const std::lock_guard<std::mutex> lock(mEngineDataGuard); | |||
| mSharedEngineData.quantum = quantum; | |||
| mSharedEngineData.resetBeatTime = true; | |||
| } | |||
| @@ -61,10 +61,12 @@ void AudioEngine::setQuantum(double quantum) | |||
| AudioEngine::EngineData AudioEngine::pullEngineData() | |||
| { | |||
| auto engineData = EngineData{}; | |||
| if (mEngineDataGuard.try_lock()) | |||
| { | |||
| engineData.requestedTempo = mSharedEngineData.requestedTempo; | |||
| mSharedEngineData.requestedTempo = 0; | |||
| engineData.resetBeatTime = mSharedEngineData.resetBeatTime; | |||
| engineData.quantum = mSharedEngineData.quantum; | |||
| mSharedEngineData.resetBeatTime = false; | |||
| @@ -98,10 +100,11 @@ void AudioEngine::timelineCallback(const std::chrono::microseconds hostTime, Lin | |||
| mLink.commitAudioTimeline(timeline); | |||
| // Save timeline info | |||
| info->bpm = timeline.tempo(); | |||
| info->beats = timeline.beatAtTime(hostTime, engineData.quantum); | |||
| info->phase = timeline.phaseAtTime(hostTime, engineData.quantum); | |||
| info->beatsPerBar = engineData.quantum; | |||
| info->beatsPerMinute = timeline.tempo(); | |||
| info->beat = timeline.beatAtTime(hostTime, engineData.quantum); | |||
| info->phase = timeline.phaseAtTime(hostTime, engineData.quantum); | |||
| } | |||
| } // namespace linkaudio | |||
| } // namespace link | |||
| } // namespace ableton | |||
| @@ -25,12 +25,12 @@ | |||
| #include <mutex> | |||
| struct LinkTimeInfo { | |||
| double bpm, beats, phase; | |||
| double beatsPerBar, beatsPerMinute, beat, phase; | |||
| }; | |||
| namespace ableton | |||
| { | |||
| namespace linkaudio | |||
| namespace link | |||
| { | |||
| class AudioEngine | |||
| @@ -57,10 +57,8 @@ private: | |||
| Link& mLink; | |||
| EngineData mSharedEngineData; | |||
| std::mutex mEngineDataGuard; | |||
| friend class AudioPlatform; | |||
| }; | |||
| } // namespace linkaudio | |||
| } // namespace link | |||
| } // namespace ableton | |||
| @@ -58,20 +58,20 @@ namespace ableton | |||
| * concurrently is not advised and will potentially lead to | |||
| * unexpected behavior. | |||
| */ | |||
| class Link | |||
| template <typename Clock> | |||
| class BasicLink | |||
| { | |||
| public: | |||
| using Clock = link::platform::Clock; | |||
| class Timeline; | |||
| /*! @brief Construct with an initial tempo. */ | |||
| Link(double bpm); | |||
| BasicLink(double bpm); | |||
| /*! @brief Link instances cannot be copied or moved */ | |||
| Link(const Link&) = delete; | |||
| Link& operator=(const Link&) = delete; | |||
| Link(Link&&) = delete; | |||
| Link& operator=(Link&&) = delete; | |||
| BasicLink(const BasicLink<Clock>&) = delete; | |||
| BasicLink& operator=(const BasicLink<Clock>&) = delete; | |||
| BasicLink(BasicLink<Clock>&&) = delete; | |||
| BasicLink& operator=(BasicLink<Clock>&&) = delete; | |||
| /*! @brief Is Link currently enabled? | |||
| * Thread-safe: yes | |||
| @@ -287,20 +287,28 @@ public: | |||
| void forceBeatAtTime(double beat, std::chrono::microseconds time, double quantum); | |||
| private: | |||
| friend Link; | |||
| friend BasicLink<Clock>; | |||
| link::Timeline mOriginalTimeline; | |||
| bool mbRespectQuantum; | |||
| link::Timeline mTimeline; | |||
| }; | |||
| private: | |||
| using Controller = ableton::link::Controller<link::PeerCountCallback, | |||
| link::TempoCallback, | |||
| Clock, | |||
| link::platform::IoContext>; | |||
| std::mutex mCallbackMutex; | |||
| link::PeerCountCallback mPeerCountCallback; | |||
| link::TempoCallback mTempoCallback; | |||
| Clock mClock; | |||
| link::platform::Controller mController; | |||
| Controller mController; | |||
| }; | |||
| using Link = BasicLink<link::platform::Clock>; | |||
| } // ableton | |||
| #include <ableton/Link.ipp> | |||
| @@ -24,10 +24,10 @@ | |||
| namespace ableton | |||
| { | |||
| inline Link::Link(const double bpm) | |||
| template <typename Clock> | |||
| inline BasicLink<Clock>::BasicLink(const double bpm) | |||
| : mPeerCountCallback([](std::size_t) {}) | |||
| , mTempoCallback([](link::Tempo) {}) | |||
| , mClock{} | |||
| , mController(link::Tempo(bpm), | |||
| [this](const std::size_t peers) { | |||
| std::lock_guard<std::mutex> lock(mCallbackMutex); | |||
| @@ -42,46 +42,54 @@ inline Link::Link(const double bpm) | |||
| { | |||
| } | |||
| inline bool Link::isEnabled() const | |||
| template <typename Clock> | |||
| inline bool BasicLink<Clock>::isEnabled() const | |||
| { | |||
| return mController.isEnabled(); | |||
| } | |||
| inline void Link::enable(const bool bEnable) | |||
| template <typename Clock> | |||
| inline void BasicLink<Clock>::enable(const bool bEnable) | |||
| { | |||
| mController.enable(bEnable); | |||
| } | |||
| inline std::size_t Link::numPeers() const | |||
| template <typename Clock> | |||
| inline std::size_t BasicLink<Clock>::numPeers() const | |||
| { | |||
| return mController.numPeers(); | |||
| } | |||
| template <typename Clock> | |||
| template <typename Callback> | |||
| void Link::setNumPeersCallback(Callback callback) | |||
| void BasicLink<Clock>::setNumPeersCallback(Callback callback) | |||
| { | |||
| std::lock_guard<std::mutex> lock(mCallbackMutex); | |||
| mPeerCountCallback = [callback](const std::size_t numPeers) { callback(numPeers); }; | |||
| } | |||
| template <typename Clock> | |||
| template <typename Callback> | |||
| void Link::setTempoCallback(Callback callback) | |||
| void BasicLink<Clock>::setTempoCallback(Callback callback) | |||
| { | |||
| std::lock_guard<std::mutex> lock(mCallbackMutex); | |||
| mTempoCallback = [callback](const link::Tempo tempo) { callback(tempo.bpm()); }; | |||
| } | |||
| inline Link::Clock Link::clock() const | |||
| template <typename Clock> | |||
| inline Clock BasicLink<Clock>::clock() const | |||
| { | |||
| return mClock; | |||
| } | |||
| inline Link::Timeline Link::captureAudioTimeline() const | |||
| template <typename Clock> | |||
| inline typename BasicLink<Clock>::Timeline BasicLink<Clock>::captureAudioTimeline() const | |||
| { | |||
| return Link::Timeline{mController.timelineRtSafe(), numPeers() > 0}; | |||
| return BasicLink<Clock>::Timeline{mController.timelineRtSafe(), numPeers() > 0}; | |||
| } | |||
| inline void Link::commitAudioTimeline(const Link::Timeline timeline) | |||
| template <typename Clock> | |||
| inline void BasicLink<Clock>::commitAudioTimeline(const Timeline timeline) | |||
| { | |||
| if (timeline.mOriginalTimeline != timeline.mTimeline) | |||
| { | |||
| @@ -89,12 +97,14 @@ inline void Link::commitAudioTimeline(const Link::Timeline timeline) | |||
| } | |||
| } | |||
| inline Link::Timeline Link::captureAppTimeline() const | |||
| template <typename Clock> | |||
| inline typename BasicLink<Clock>::Timeline BasicLink<Clock>::captureAppTimeline() const | |||
| { | |||
| return Link::Timeline{mController.timeline(), numPeers() > 0}; | |||
| return Timeline{mController.timeline(), numPeers() > 0}; | |||
| } | |||
| inline void Link::commitAppTimeline(const Link::Timeline timeline) | |||
| template <typename Clock> | |||
| inline void BasicLink<Clock>::commitAppTimeline(const Timeline timeline) | |||
| { | |||
| if (timeline.mOriginalTimeline != timeline.mTimeline) | |||
| { | |||
| @@ -102,21 +112,27 @@ inline void Link::commitAppTimeline(const Link::Timeline timeline) | |||
| } | |||
| } | |||
| // Link::Timeline | |||
| //////////////////// | |||
| // Link::Timeline // | |||
| //////////////////// | |||
| inline Link::Timeline::Timeline(const link::Timeline timeline, const bool bRespectQuantum) | |||
| template <typename Clock> | |||
| inline BasicLink<Clock>::Timeline::Timeline( | |||
| const link::Timeline timeline, const bool bRespectQuantum) | |||
| : mOriginalTimeline(timeline) | |||
| , mbRespectQuantum(bRespectQuantum) | |||
| , mTimeline(timeline) | |||
| { | |||
| } | |||
| inline double Link::Timeline::tempo() const | |||
| template <typename Clock> | |||
| inline double BasicLink<Clock>::Timeline::tempo() const | |||
| { | |||
| return mTimeline.tempo.bpm(); | |||
| } | |||
| inline void Link::Timeline::setTempo( | |||
| template <typename Clock> | |||
| inline void BasicLink<Clock>::Timeline::setTempo( | |||
| const double bpm, const std::chrono::microseconds atTime) | |||
| { | |||
| const auto desiredTl = | |||
| @@ -125,26 +141,30 @@ inline void Link::Timeline::setTempo( | |||
| mTimeline.timeOrigin = desiredTl.fromBeats(mTimeline.beatOrigin); | |||
| } | |||
| inline double Link::Timeline::beatAtTime( | |||
| template <typename Clock> | |||
| inline double BasicLink<Clock>::Timeline::beatAtTime( | |||
| const std::chrono::microseconds time, const double quantum) const | |||
| { | |||
| return link::toPhaseEncodedBeats(mTimeline, time, link::Beats{quantum}).floating(); | |||
| } | |||
| inline double Link::Timeline::phaseAtTime( | |||
| template <typename Clock> | |||
| inline double BasicLink<Clock>::Timeline::phaseAtTime( | |||
| const std::chrono::microseconds time, const double quantum) const | |||
| { | |||
| return link::phase(link::Beats{beatAtTime(time, quantum)}, link::Beats{quantum}) | |||
| .floating(); | |||
| } | |||
| inline std::chrono::microseconds Link::Timeline::timeAtBeat( | |||
| template <typename Clock> | |||
| inline std::chrono::microseconds BasicLink<Clock>::Timeline::timeAtBeat( | |||
| const double beat, const double quantum) const | |||
| { | |||
| return link::fromPhaseEncodedBeats(mTimeline, link::Beats{beat}, link::Beats{quantum}); | |||
| } | |||
| inline void Link::Timeline::requestBeatAtTime( | |||
| template <typename Clock> | |||
| inline void BasicLink<Clock>::Timeline::requestBeatAtTime( | |||
| const double beat, std::chrono::microseconds time, const double quantum) | |||
| { | |||
| if (mbRespectQuantum) | |||
| @@ -157,7 +177,8 @@ inline void Link::Timeline::requestBeatAtTime( | |||
| forceBeatAtTime(beat, time, quantum); | |||
| } | |||
| inline void Link::Timeline::forceBeatAtTime( | |||
| template <typename Clock> | |||
| inline void BasicLink<Clock>::Timeline::forceBeatAtTime( | |||
| const double beat, const std::chrono::microseconds time, const double quantum) | |||
| { | |||
| // There are two components to the beat adjustment: a phase shift | |||
| @@ -0,0 +1,113 @@ | |||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | |||
| * | |||
| * This program is free software: you can redistribute it and/or modify | |||
| * it under the terms of the GNU General Public License as published by | |||
| * the Free Software Foundation, either version 2 of the License, or | |||
| * (at your option) any later version. | |||
| * | |||
| * This program is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| * GNU General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU General Public License | |||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
| * | |||
| * If you would like to incorporate Link into a proprietary software application, | |||
| * please contact <link-devs@ableton.com>. | |||
| */ | |||
| #pragma once | |||
| #include <array> | |||
| #include <atomic> | |||
| #include <cassert> | |||
| namespace ableton | |||
| { | |||
| namespace link | |||
| { | |||
| // Single producer, single consumer lockfree Fifo | |||
| template <typename Type, std::size_t size> | |||
| class CircularFifo | |||
| { | |||
| public: | |||
| struct PoppedItem | |||
| { | |||
| Type item; | |||
| bool valid; | |||
| }; | |||
| CircularFifo() | |||
| : tail(0) | |||
| , head(0) | |||
| { | |||
| assert(head.is_lock_free() && tail.is_lock_free()); | |||
| } | |||
| bool push(Type item) | |||
| { | |||
| const auto currentTail = tail.load(); | |||
| const auto nextTail = nextIndex(currentTail); | |||
| if (nextTail != head.load()) | |||
| { | |||
| data[currentTail] = std::move(item); | |||
| tail.store(nextTail); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| PoppedItem pop() | |||
| { | |||
| const auto currentHead = head.load(); | |||
| if (currentHead == tail.load()) | |||
| { | |||
| return {Type{}, false}; | |||
| } | |||
| auto item = data[currentHead]; | |||
| head.store(nextIndex(currentHead)); | |||
| return {std::move(item), true}; | |||
| } | |||
| PoppedItem clearAndPopLast() | |||
| { | |||
| auto hasData = false; | |||
| auto currentHead = head.load(); | |||
| while (currentHead != tail.load()) | |||
| { | |||
| currentHead = nextIndex(currentHead); | |||
| hasData = true; | |||
| } | |||
| auto item = data[previousIndex(currentHead)]; | |||
| head.store(currentHead); | |||
| return {std::move(item), hasData}; | |||
| } | |||
| bool isEmpty() const | |||
| { | |||
| return tail == head; | |||
| } | |||
| private: | |||
| size_t nextIndex(const size_t index) const | |||
| { | |||
| return (index + 1) % (size + 1); | |||
| } | |||
| size_t previousIndex(const size_t index) const | |||
| { | |||
| return (index + size) % (size + 1); | |||
| } | |||
| std::atomic_size_t tail; | |||
| std::atomic_size_t head; | |||
| std::array<Type, size + 1> data; | |||
| }; | |||
| } // link | |||
| } // ableton | |||
| @@ -20,6 +20,7 @@ | |||
| #pragma once | |||
| #include <ableton/discovery/Service.hpp> | |||
| #include <ableton/link/CircularFifo.hpp> | |||
| #include <ableton/link/ClientSessionTimelines.hpp> | |||
| #include <ableton/link/Gateway.hpp> | |||
| #include <ableton/link/GhostXForm.hpp> | |||
| @@ -45,7 +46,8 @@ GhostXForm initXForm(const Clock& clock) | |||
| // The timespan in which local modifications to the timeline will be | |||
| // preferred over any modifications coming from the network. | |||
| const auto kLocalModGracePeriod = std::chrono::seconds(1); | |||
| const auto kLocalModGracePeriod = std::chrono::milliseconds(1000); | |||
| const auto kRtHandlerFallbackPeriod = kLocalModGracePeriod / 2; | |||
| } // namespace detail | |||
| @@ -61,8 +63,6 @@ template <typename PeerCountCallback, | |||
| class Controller | |||
| { | |||
| public: | |||
| using Ticks = typename Clock::Ticks; | |||
| Controller(Tempo tempo, | |||
| PeerCountCallback peerCallback, | |||
| TempoCallback tempoCallback, | |||
| @@ -81,7 +81,7 @@ public: | |||
| , mSessionPeerCounter(*this, std::move(peerCallback)) | |||
| , mEnabled(false) | |||
| , mIo(std::move(io)) | |||
| , mRealtimeIo(util::injectRef(*mIo)) | |||
| , mRtTimelineSetter(*this) | |||
| , mPeers(util::injectRef(*mIo), | |||
| std::ref(mSessionPeerCounter), | |||
| SessionTimelineCallback{*this}) | |||
| @@ -109,7 +109,7 @@ public: | |||
| const bool bWasEnabled = mEnabled.exchange(bEnable); | |||
| if (bWasEnabled != bEnable) | |||
| { | |||
| mRealtimeIo.async([this, bEnable] { | |||
| mIo->async([this, bEnable] { | |||
| if (bEnable) | |||
| { | |||
| // Always reset when first enabling to avoid hijacking | |||
| @@ -164,7 +164,7 @@ public: | |||
| // cached version of the timeline. | |||
| const auto now = mClock.micros(); | |||
| if (now - mRtClientTimelineTimestamp > detail::kLocalModGracePeriod | |||
| && mSessionTimingGuard.try_lock()) | |||
| && !mRtTimelineSetter.hasPendingTimelines() && mSessionTimingGuard.try_lock()) | |||
| { | |||
| const auto clientTimeline = updateClientTimelineFromSession( | |||
| mRtClientTimeline, mSessionTimeline, now, mGhostXForm); | |||
| @@ -184,21 +184,17 @@ public: | |||
| void setTimelineRtSafe(Timeline newTimeline, const std::chrono::microseconds atTime) | |||
| { | |||
| newTimeline = clampTempo(newTimeline); | |||
| // Cache the new timeline for serving back to the client | |||
| mRtClientTimeline = newTimeline; | |||
| mRtClientTimelineTimestamp = | |||
| isEnabled() ? mClock.micros() : std::chrono::microseconds(0); | |||
| // Update the session timeline from the new client timeline | |||
| mRealtimeIo.async([this, newTimeline, atTime] { | |||
| // Synchronize with the non-rt version of the client timeline | |||
| { | |||
| std::lock_guard<std::mutex> lock(mClientTimelineGuard); | |||
| mClientTimeline = newTimeline; | |||
| } | |||
| handleTimelineFromClient(updateSessionTimelineFromClient( | |||
| mSessionTimeline, newTimeline, atTime, mGhostXForm)); | |||
| }); | |||
| // This will fail in case the Fifo in the RtTimelineSetter is full. This indicates a | |||
| // very high rate of calls to the setter. In this case we ignore one value because we | |||
| // expect the setter to be called again soon. | |||
| if (mRtTimelineSetter.tryPush(newTimeline, atTime)) | |||
| { | |||
| // Cache the new timeline for serving back to the client | |||
| mRtClientTimeline = newTimeline; | |||
| mRtClientTimelineTimestamp = | |||
| isEnabled() ? mClock.micros() : std::chrono::microseconds(0); | |||
| } | |||
| } | |||
| private: | |||
| @@ -248,6 +244,16 @@ private: | |||
| mSessions.sawSessionTimeline(std::move(id), std::move(timeline)), mGhostXForm); | |||
| } | |||
| void handleRtTimeline(const Timeline timeline, const std::chrono::microseconds time) | |||
| { | |||
| { | |||
| std::lock_guard<std::mutex> lock(mClientTimelineGuard); | |||
| mClientTimeline = timeline; | |||
| } | |||
| handleTimelineFromClient( | |||
| updateSessionTimelineFromClient(mSessionTimeline, timeline, time, mGhostXForm)); | |||
| } | |||
| void joinSession(const Session& session) | |||
| { | |||
| const bool sessionIdChanged = mSessionId != session.sessionId; | |||
| @@ -293,6 +299,60 @@ private: | |||
| Controller& mController; | |||
| }; | |||
| struct RtTimelineSetter | |||
| { | |||
| using CallbackDispatcher = | |||
| typename IoContext::template LockFreeCallbackDispatcher<std::function<void()>, | |||
| std::chrono::milliseconds>; | |||
| using RtTimeline = std::pair<Timeline, std::chrono::microseconds>; | |||
| RtTimelineSetter(Controller& controller) | |||
| : mController(controller) | |||
| , mHasPendingTimelines(false) | |||
| , mCallbackDispatcher( | |||
| [this] { processPendingTimelines(); }, detail::kRtHandlerFallbackPeriod) | |||
| { | |||
| } | |||
| bool tryPush(const Timeline timeline, const std::chrono::microseconds time) | |||
| { | |||
| mHasPendingTimelines = true; | |||
| const auto success = mFifo.push({timeline, time}); | |||
| if (success) | |||
| { | |||
| mCallbackDispatcher.invoke(); | |||
| } | |||
| return success; | |||
| } | |||
| bool hasPendingTimelines() const | |||
| { | |||
| return mHasPendingTimelines; | |||
| } | |||
| private: | |||
| void processPendingTimelines() | |||
| { | |||
| auto result = mFifo.clearAndPopLast(); | |||
| if (result.valid) | |||
| { | |||
| auto timeline = std::move(result.item); | |||
| mController.mIo->async([this, timeline]() { | |||
| mController.handleRtTimeline(timeline.first, timeline.second); | |||
| mHasPendingTimelines = false; | |||
| }); | |||
| } | |||
| } | |||
| Controller& mController; | |||
| // Assuming a wake up time of one ms for the threads owned by the CallbackDispatcher | |||
| // and the ioService, buffering 16 timelines allows to set eight timelines per ms. | |||
| CircularFifo<RtTimeline, 16> mFifo; | |||
| std::atomic<bool> mHasPendingTimelines; | |||
| CallbackDispatcher mCallbackDispatcher; | |||
| }; | |||
| struct SessionPeerCounter | |||
| { | |||
| SessionPeerCounter(Controller& controller, PeerCountCallback callback) | |||
| @@ -424,9 +484,8 @@ private: | |||
| std::atomic<bool> mEnabled; | |||
| util::Injected<IoContext> mIo; | |||
| // A realtime facade over the provided IoContext. This should only | |||
| // be used by realtime code, non-realtime code should use mIo. | |||
| typename IoType::template RealTimeContext<IoType&> mRealtimeIo; | |||
| RtTimelineSetter mRtTimelineSetter; | |||
| ControllerPeers mPeers; | |||
| @@ -22,15 +22,7 @@ | |||
| #include <array> | |||
| #include <cfloat> | |||
| #include <cmath> | |||
| #include <limits> | |||
| #if LINK_PLATFORM_WINDOWS | |||
| // Windows.h (or more specifically, minwindef.h) define the max(a, b) macro | |||
| // which conflicts with the symbol provided by std::numeric_limits. | |||
| #ifdef max | |||
| #undef max | |||
| #endif | |||
| #endif | |||
| namespace ableton | |||
| { | |||
| @@ -42,9 +34,6 @@ struct Kalman | |||
| { | |||
| Kalman() | |||
| : mValue(0) | |||
| , mGain(0) | |||
| , mVVariance(1) | |||
| , mWVariance(1) | |||
| , mCoVariance(1) | |||
| , mVarianceLength(n) | |||
| , mCounter(mVarianceLength) | |||
| @@ -124,21 +113,18 @@ struct Kalman | |||
| // prediction equations | |||
| const double prevFilterValue = mFilterValues[(mCounter - 1) % mVarianceLength]; | |||
| mFilterValues[currentIndex] = prevFilterValue; | |||
| mWVariance = calculateWVariance(); | |||
| const double coVarianceEstimation = mCoVariance + mWVariance; | |||
| const auto wVariance = calculateWVariance(); | |||
| const double coVarianceEstimation = mCoVariance + wVariance; | |||
| // update equations | |||
| mVVariance = calculateVVariance(); | |||
| if ((coVarianceEstimation + mVVariance) != 0) | |||
| { | |||
| mGain = coVarianceEstimation / (coVarianceEstimation + mVVariance); | |||
| } | |||
| else | |||
| { | |||
| mGain = std::numeric_limits<double>::max(); | |||
| } | |||
| mValue = prevFilterValue + mGain * (value - prevFilterValue); | |||
| mCoVariance = (1 - mGain) * coVarianceEstimation; | |||
| const auto vVariance = calculateVVariance(); | |||
| // Gain defines how easily the filter will adjust to a new condition | |||
| // With gain = 1 the output equals the input, with gain = 0 the input | |||
| // is ignored and the output equals the last filtered value | |||
| const auto divisor = coVarianceEstimation + vVariance; | |||
| const auto gain = divisor != 0. ? coVarianceEstimation / divisor : 0.7; | |||
| mValue = prevFilterValue + gain * (value - prevFilterValue); | |||
| mCoVariance = (1 - gain) * coVarianceEstimation; | |||
| } | |||
| mFilterValues[currentIndex] = mValue; | |||
| @@ -146,9 +132,6 @@ struct Kalman | |||
| } | |||
| double mValue; | |||
| double mGain; | |||
| double mVVariance; | |||
| double mWVariance; | |||
| double mCoVariance; | |||
| size_t mVarianceLength; | |||
| size_t mCounter; | |||
| @@ -32,8 +32,8 @@ | |||
| #include <ableton/platforms/posix/ScanIpIfAddrs.hpp> | |||
| #elif LINK_PLATFORM_LINUX | |||
| #include <ableton/platforms/asio/Context.hpp> | |||
| #include <ableton/platforms/linux/Clock.hpp> | |||
| #include <ableton/platforms/posix/ScanIpIfAddrs.hpp> | |||
| #include <ableton/platforms/stl/Clock.hpp> | |||
| #endif | |||
| namespace ableton | |||
| @@ -54,7 +54,11 @@ using IoContext = | |||
| platforms::asio::Context<platforms::posix::ScanIpIfAddrs, util::NullLog>; | |||
| #elif LINK_PLATFORM_LINUX | |||
| using Clock = platforms::stl::Clock; | |||
| #ifdef __ARM_ARCH_7A__ | |||
| using Clock = platforms::linux::ClockMonotonicRaw; | |||
| #else | |||
| using Clock = platforms::linux::ClockMonotonic; | |||
| #endif | |||
| using IoContext = | |||
| platforms::asio::Context<platforms::posix::ScanIpIfAddrs, util::NullLog>; | |||
| #endif | |||
| @@ -22,7 +22,7 @@ | |||
| #include <ableton/discovery/IpV4Interface.hpp> | |||
| #include <ableton/platforms/asio/AsioTimer.hpp> | |||
| #include <ableton/platforms/asio/AsioWrapper.hpp> | |||
| #include <ableton/platforms/asio/PooledHandlerContext.hpp> | |||
| #include <ableton/platforms/asio/LockFreeCallbackDispatcher.hpp> | |||
| #include <ableton/platforms/asio/Socket.hpp> | |||
| #include <thread> | |||
| @@ -40,12 +40,12 @@ public: | |||
| using Timer = AsioTimer; | |||
| using Log = LogT; | |||
| template <typename Handler, typename Duration> | |||
| using LockFreeCallbackDispatcher = LockFreeCallbackDispatcher<Handler, Duration>; | |||
| template <std::size_t BufferSize> | |||
| using Socket = asio::Socket<BufferSize>; | |||
| template <typename IoContext> | |||
| using RealTimeContext = PooledHandlerContext<IoContext>; | |||
| Context() | |||
| : Context(DefaultHandler{}) | |||
| { | |||
| @@ -0,0 +1,89 @@ | |||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | |||
| * | |||
| * This program is free software: you can redistribute it and/or modify | |||
| * it under the terms of the GNU General Public License as published by | |||
| * the Free Software Foundation, either version 2 of the License, or | |||
| * (at your option) any later version. | |||
| * | |||
| * This program is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| * GNU General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU General Public License | |||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
| * | |||
| * If you would like to incorporate Link into a proprietary software application, | |||
| * please contact <link-devs@ableton.com>. | |||
| */ | |||
| #pragma once | |||
| #include <atomic> | |||
| #include <condition_variable> | |||
| #include <thread> | |||
| namespace ableton | |||
| { | |||
| namespace platforms | |||
| { | |||
| // Utility to signal invocation of a callback on another thread in a lock free manner. | |||
| // The callback is evoked on a thread owned by the instance of this class. | |||
| // | |||
| // A condition variable is used to notify a waiting thread, but only if the required | |||
| // lock can be acquired immediately. If that fails, we fall back on signaling | |||
| // after a timeout. This gives us a guaranteed minimum signalling rate which is defined | |||
| // by the fallbackPeriod parameter. | |||
| template <typename Callback, typename Duration> | |||
| class LockFreeCallbackDispatcher | |||
| { | |||
| public: | |||
| LockFreeCallbackDispatcher(Callback callback, Duration fallbackPeriod) | |||
| : mCallback(std::move(callback)) | |||
| , mFallbackPeriod(std::move(fallbackPeriod)) | |||
| , mRunning(true) | |||
| , mThread([this] { run(); }) | |||
| { | |||
| } | |||
| ~LockFreeCallbackDispatcher() | |||
| { | |||
| mRunning = false; | |||
| mCondition.notify_one(); | |||
| mThread.join(); | |||
| } | |||
| void invoke() | |||
| { | |||
| if (mMutex.try_lock()) | |||
| { | |||
| mCondition.notify_one(); | |||
| mMutex.unlock(); | |||
| } | |||
| } | |||
| private: | |||
| void run() | |||
| { | |||
| while (mRunning.load()) | |||
| { | |||
| { | |||
| std::unique_lock<std::mutex> lock(mMutex); | |||
| mCondition.wait_for(lock, mFallbackPeriod); | |||
| } | |||
| mCallback(); | |||
| } | |||
| } | |||
| Callback mCallback; | |||
| Duration mFallbackPeriod; | |||
| std::atomic<bool> mRunning; | |||
| std::mutex mMutex; | |||
| std::condition_variable mCondition; | |||
| std::thread mThread; | |||
| }; | |||
| } // platforms | |||
| } // ableton | |||
| @@ -1,115 +0,0 @@ | |||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | |||
| * | |||
| * This program is free software: you can redistribute it and/or modify | |||
| * it under the terms of the GNU General Public License as published by | |||
| * the Free Software Foundation, either version 2 of the License, or | |||
| * (at your option) any later version. | |||
| * | |||
| * This program is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| * GNU General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU General Public License | |||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
| * | |||
| * If you would like to incorporate Link into a proprietary software application, | |||
| * please contact <link-devs@ableton.com>. | |||
| */ | |||
| #pragma once | |||
| #include <ableton/platforms/asio/AsioWrapper.hpp> | |||
| #include <ableton/util/Injected.hpp> | |||
| namespace ableton | |||
| { | |||
| namespace platforms | |||
| { | |||
| namespace asio | |||
| { | |||
| template <typename IoContext, | |||
| std::size_t MaxNumHandlers = 32, | |||
| std::size_t MaxHandlerSize = 128> | |||
| struct PooledHandlerContext | |||
| { | |||
| PooledHandlerContext(util::Injected<IoContext> io) | |||
| : mIo(std::move(io)) | |||
| { | |||
| // initialize the handler free store | |||
| mFreeStack.reserve(MaxNumHandlers); | |||
| for (std::size_t i = 0; i < MaxNumHandlers; ++i) | |||
| { | |||
| mFreeStack.push_back(reinterpret_cast<void*>(mRaw + i)); | |||
| } | |||
| } | |||
| template <typename Handler> | |||
| void async(Handler handler) | |||
| { | |||
| try | |||
| { | |||
| mIo->async(HandlerWrapper<Handler>{*this, std::move(handler)}); | |||
| } | |||
| catch (std::bad_alloc) | |||
| { | |||
| warning(mIo->log()) << "Handler dropped due to low memory pool"; | |||
| } | |||
| } | |||
| template <typename Handler> | |||
| struct HandlerWrapper | |||
| { | |||
| HandlerWrapper(PooledHandlerContext& context, Handler handler) | |||
| : mContext(context) | |||
| , mHandler(std::move(handler)) | |||
| { | |||
| } | |||
| void operator()() | |||
| { | |||
| mHandler(); | |||
| } | |||
| // Use pooled allocation so that posting handlers will not cause | |||
| // system allocation | |||
| friend void* asio_handler_allocate( | |||
| const std::size_t size, HandlerWrapper* const pHandler) | |||
| { | |||
| if (size > MaxHandlerSize || pHandler->mContext.mFreeStack.empty()) | |||
| { | |||
| // Going over the max handler size is a programming error, as | |||
| // this is not a dynamically variable value. | |||
| assert(size <= MaxHandlerSize); | |||
| throw std::bad_alloc(); | |||
| } | |||
| else | |||
| { | |||
| const auto p = pHandler->mContext.mFreeStack.back(); | |||
| pHandler->mContext.mFreeStack.pop_back(); | |||
| return p; | |||
| } | |||
| } | |||
| friend void asio_handler_deallocate( | |||
| void* const p, std::size_t, HandlerWrapper* const pHandler) | |||
| { | |||
| pHandler->mContext.mFreeStack.push_back(p); | |||
| } | |||
| PooledHandlerContext& mContext; | |||
| Handler mHandler; | |||
| }; | |||
| using MemChunk = typename std::aligned_storage<MaxHandlerSize, | |||
| std::alignment_of<max_align_t>::value>::type; | |||
| MemChunk mRaw[MaxNumHandlers]; | |||
| std::vector<void*> mFreeStack; | |||
| util::Injected<IoContext> mIo; | |||
| }; | |||
| } // namespace asio | |||
| } // namespace platforms | |||
| } // namespace ableton | |||
| @@ -0,0 +1,64 @@ | |||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | |||
| * | |||
| * This program is free software: you can redistribute it and/or modify | |||
| * it under the terms of the GNU General Public License as published by | |||
| * the Free Software Foundation, either version 2 of the License, or | |||
| * (at your option) any later version. | |||
| * | |||
| * This program is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| * GNU General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU General Public License | |||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
| * | |||
| * If you would like to incorporate Link into a proprietary software application, | |||
| * please contact <link-devs@ableton.com>. | |||
| */ | |||
| #pragma once | |||
| #include <chrono> | |||
| #include <cmath> | |||
| #include <ctime> | |||
| namespace ableton | |||
| { | |||
| namespace platforms | |||
| { | |||
| #ifdef linux | |||
| #undef linux | |||
| #endif | |||
| namespace linux | |||
| { | |||
| template <clockid_t CLOCK> | |||
| class Clock | |||
| { | |||
| public: | |||
| std::chrono::microseconds micros() const | |||
| { | |||
| ::timespec ts; | |||
| ::clock_gettime(CLOCK, &ts); | |||
| std::uint64_t ns = ts.tv_sec * 1000000000ULL + ts.tv_nsec; | |||
| return std::chrono::microseconds(ns / 1000ULL); | |||
| } | |||
| }; | |||
| using ClockMonotonic = Clock<CLOCK_MONOTONIC>; | |||
| using ClockMonotonicRaw = Clock<CLOCK_MONOTONIC_RAW>; | |||
| /* Warning: the realtime clock can be subject to time change. | |||
| * One of the hard requirements of Ableton Link is that the clock must be | |||
| * monotonic. | |||
| * Only use the Realtime Clock if you can't use the monotonic ones, and | |||
| * beware that things might go wrong if time jumps. | |||
| */ | |||
| using ClockRealtime = Clock<CLOCK_REALTIME>; | |||
| } // namespace linux | |||
| } // namespace platforms | |||
| } // namespace ableton | |||
| @@ -30,20 +30,12 @@ namespace stl | |||
| struct Clock | |||
| { | |||
| using Ticks = std::uint64_t; | |||
| Clock() | |||
| { | |||
| mStartTime = std::chrono::high_resolution_clock::now(); | |||
| } | |||
| std::chrono::microseconds micros() const | |||
| { | |||
| using namespace std::chrono; | |||
| return duration_cast<microseconds>(high_resolution_clock::now() - mStartTime); | |||
| auto nowInMicros = time_point_cast<microseconds>(steady_clock::now()); | |||
| return nowInMicros.time_since_epoch(); | |||
| } | |||
| std::chrono::high_resolution_clock::time_point mStartTime; | |||
| }; | |||
| } // namespace stl | |||