gearmulator

Emulation of classic VA synths of the late 90s/2000s that are based on Motorola 56300 family DSPs
Log | Files | Refs | Submodules | README | LICENSE

commit 32cc0ffb7f616e25ae8360ae2880f664803ed4c9
parent 9d83dc8176bd616f907a8142663562a0ba86eb5c
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Wed, 24 Jul 2024 00:07:57 +0200

UC/DSP syncronization

Diffstat:
Msource/nord/n2x/n2xLib/n2xdsp.cpp | 22++++++++++++++++++++++
Msource/nord/n2x/n2xLib/n2xdsp.h | 19+++++++++++++++++++
Msource/nord/n2x/n2xLib/n2xhardware.cpp | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msource/nord/n2x/n2xLib/n2xhardware.h | 39++++++++++++++++++++++++++++++++++++++-
4 files changed, 296 insertions(+), 4 deletions(-)

diff --git a/source/nord/n2x/n2xLib/n2xdsp.cpp b/source/nord/n2x/n2xLib/n2xdsp.cpp @@ -178,6 +178,16 @@ namespace n2x m_thread->setLogToStdout(false); } + void DSP::advanceSamples(const uint32_t _samples, const uint32_t _latency) + { + { + std::lock_guard uLockHalt(m_haltDSPmutex); + m_maxEsaiCallbacks += _samples; + m_esaiLatency = _latency; + } + m_haltDSPcv.notify_one(); + } + void DSP::onUCRxEmpty(bool _needMoreData) { if(_needMoreData) @@ -262,6 +272,18 @@ namespace n2x return _isr; } + void DSP::onEsaiCallback() + { + ++m_numEsaiCallbacks; + + std::unique_lock uLock(m_haltDSPmutex); + m_haltDSPcv.wait(uLock, [&] + { + return (m_maxEsaiCallbacks + m_esaiLatency) > m_numEsaiCallbacks; + }); + m_esaiCallback(); + } + void DSP::transferHostFlagsUc2Dsdp() { const uint32_t hf01 = m_hdiUC.icr() & 0x18; diff --git a/source/nord/n2x/n2xLib/n2xdsp.h b/source/nord/n2x/n2xLib/n2xdsp.h @@ -35,13 +35,23 @@ namespace n2x return m_periphX; } + void setEsaiCallback(std::function<void()>&& _func) + { + m_esaiCallback = std::move(_func); + } + + void advanceSamples(uint32_t _samples, uint32_t _latency); + private: void onUCRxEmpty(bool _needMoreData); void hdiTransferUCtoDSP(uint32_t _word); void hdiSendIrqToDSP(uint8_t _irq); uint8_t hdiUcReadIsr(uint8_t _isr); + void onEsaiCallback(); + public: void transferHostFlagsUc2Dsdp(); + private: bool hdiTransferDSPtoUC(); void waitDspRxEmpty(); @@ -61,5 +71,14 @@ namespace n2x bool m_receivedMagicEsaiPacket = false; uint32_t m_hdiHF01 = 0; + + uint64_t m_numEsaiCallbacks = 0; + uint64_t m_maxEsaiCallbacks = 0; + uint64_t m_esaiLatency = 0; + + std::function<void()> m_esaiCallback = [] {}; + + std::condition_variable m_haltDSPcv; + std::mutex m_haltDSPmutex; }; } diff --git a/source/nord/n2x/n2xLib/n2xhardware.cpp b/source/nord/n2x/n2xLib/n2xhardware.cpp @@ -2,13 +2,25 @@ namespace n2x { + constexpr uint32_t g_syncEsaiFrameRate = 8; + constexpr uint32_t g_syncHaltDspEsaiThreshold = 16; + + static_assert((g_syncEsaiFrameRate & (g_syncEsaiFrameRate - 1)) == 0, "esai frame sync rate must be power of two"); + static_assert(g_syncHaltDspEsaiThreshold >= g_syncEsaiFrameRate * 2, "esai DSP halt threshold must be greater than two times the sync rate"); + Hardware::Hardware() : m_uc(m_rom) , m_dspA(*this, m_uc.getHdi08A(), 0) , m_dspB(*this, m_uc.getHdi08B(), 1) + , m_samplerateInv(1.0 / g_samplerate) { if(!m_rom.isValid()) return; + + m_dspB.setEsaiCallback([this]() + { + onEsaiCallback(); + }); } bool Hardware::isValid() const @@ -16,16 +28,218 @@ namespace n2x return m_rom.isValid(); } - void Hardware::process() + void Hardware::processUC() { - m_uc.exec(); + syncUCtoDSP(); + + const auto deltaCycles = m_uc.exec(); + if(m_esaiFrameIndex > 0) + m_remainingUcCycles -= static_cast<int64_t>(deltaCycles); + m_dspA.transferHostFlagsUc2Dsdp(); m_dspB.transferHostFlagsUc2Dsdp(); } void Hardware::ucYieldLoop(const std::function<bool()>& _continue) { + const auto dspHalted = m_haltDSP; + + resumeDSP(); + while(_continue()) - std::this_thread::yield(); + { +// if(m_processAudio) + { + std::this_thread::yield(); + } +/* else + { + if(m_esaiFrameIndex) + { + std::unique_lock uLock(m_esaiFrameAddedMutex); + m_esaiFrameAddedCv.wait(uLock); + } + } + */ + } + + if(dspHalted) + haltDSP(); + } + + void Hardware::processAudio(uint32_t _frames, uint32_t _latency) + { + ensureBufferSize(_frames); + + dsp56k::TWord* outputs[12]{nullptr}; + outputs[0] = &m_audioOutputs[0].front(); + outputs[1] = &m_audioOutputs[1].front(); + outputs[2] = &m_audioOutputs[2].front(); + outputs[3] = &m_audioOutputs[3].front(); + outputs[4] = m_dummyOutput.data(); + outputs[5] = m_dummyOutput.data(); + outputs[6] = m_dummyOutput.data(); + outputs[7] = m_dummyOutput.data(); + outputs[8] = m_dummyOutput.data(); + outputs[9] = m_dummyOutput.data(); + outputs[10] = m_dummyOutput.data(); + outputs[11] = m_dummyOutput.data(); + + auto& esaiA = m_dspA.getPeriph().getEsai(); + auto& esaiB = m_dspB.getPeriph().getEsai(); + +// LOG("B out " << esaiB.getAudioOutputs().size() << ", A out " << esaiA.getAudioOutputs().size() << ", B in " << esaiB.getAudioInputs().size()); + + while (_frames) + { + const auto processCount = std::min(_frames, static_cast<uint32_t>(16)); + _frames -= processCount; + + m_dspA.advanceSamples(processCount, 0); + m_dspB.advanceSamples(processCount, 0); + + auto* buf = m_dspAtoBBuffer.data(); + + // read data from DSP A... + esaiA.processAudioOutput<dsp56k::TWord>(processCount, [&](size_t _index, dsp56k::Audio::TxFrame& _frame) + { + *buf++ = _frame[0][0]; + *buf++ = _frame[1][0]; + *buf++ = _frame[2][0]; + *buf++ = _frame[3][0]; + }); + + buf = m_dspAtoBBuffer.data(); + + // ...and forward it to DSP B + esaiB.processAudioInput<dsp56k::TWord>(processCount, 0, [&](size_t _s, dsp56k::Audio::RxFrame& _f) + { + _f.resize(4); + _f[0] = dsp56k::Audio::RxSlot{0}; + _f[1] = dsp56k::Audio::RxSlot{0}; + _f[2] = dsp56k::Audio::RxSlot{0}; + _f[3] = dsp56k::Audio::RxSlot{0}; + buf += 4; + }); + +// esaiB.processAudioInput(m_dspAtoBBuffer.data(), processCount * 2, 4, 0); + + // read output of DSP B to regular audio output + esaiB.processAudioOutputInterleaved(outputs, processCount); + + for(uint32_t i=0; i<processCount; ++i) + { + const auto i4 = i<<2; + outputs[0][i] += m_dspAtoBBuffer[i4+2]; + outputs[1][i] += m_dspAtoBBuffer[i4+3]; + outputs[2][i] += m_dspAtoBBuffer[i4+0]; + outputs[3][i] += m_dspAtoBBuffer[i4+1]; + } + + outputs[0] += processCount; + outputs[1] += processCount; + outputs[2] += processCount; + outputs[3] += processCount; + } + } + + void Hardware::ensureBufferSize(const uint32_t _frames) + { + if(m_dummyInput.size() >= _frames) + return; + + m_dummyInput.resize(_frames, 0); + m_dummyOutput.resize(_frames, 0); + + for (auto& audioOutput : m_audioOutputs) + audioOutput.resize(_frames, 0); + + m_dspAtoBBuffer.resize(_frames * 4); + } + + void Hardware::onEsaiCallback() + { + ++m_esaiFrameIndex; + +// processMidiInput(); + + if((m_esaiFrameIndex & (g_syncEsaiFrameRate-1)) == 0) + m_esaiFrameAddedCv.notify_one(); + + m_requestedFramesAvailableMutex.lock(); + + if(m_requestedFrames && m_dspB.getPeriph().getEsai().getAudioOutputs().size() >= m_requestedFrames) + { + m_requestedFramesAvailableMutex.unlock(); + m_requestedFramesAvailableCv.notify_one(); + } + else + { + m_requestedFramesAvailableMutex.unlock(); + } + + std::unique_lock uLock(m_haltDSPmutex); + m_haltDSPcv.wait(uLock, [&]{ return m_haltDSP == false; }); + } + + void Hardware::syncUCtoDSP() + { + if(m_remainingUcCycles > 0) + return; + + // we can only use ESAI to clock the uc once it has been enabled + if(m_esaiFrameIndex <= 0) + return; + + if(m_esaiFrameIndex == m_lastEsaiFrameIndex) + { + resumeDSP(); + std::unique_lock uLock(m_esaiFrameAddedMutex); + m_esaiFrameAddedCv.wait(uLock, [this]{return m_esaiFrameIndex > m_lastEsaiFrameIndex;}); + } + + const auto esaiFrameIndex = m_esaiFrameIndex; + + const auto ucClock = m_uc.getSim().getSystemClockHz(); + + const double ucCyclesPerFrame = static_cast<double>(ucClock) * m_samplerateInv; + + const auto esaiDelta = esaiFrameIndex - m_lastEsaiFrameIndex; + + m_remainingUcCyclesD += ucCyclesPerFrame * static_cast<double>(esaiDelta); + m_remainingUcCycles = static_cast<int64_t>(m_remainingUcCyclesD); + m_remainingUcCyclesD -= static_cast<double>(m_remainingUcCycles); + + if(esaiDelta > g_syncHaltDspEsaiThreshold) + { + haltDSP(); + } + else + { + resumeDSP(); + } + + m_lastEsaiFrameIndex = esaiFrameIndex; + } + + void Hardware::haltDSP() + { + if(m_haltDSP) + return; + + std::lock_guard uLockHalt(m_haltDSPmutex); + m_haltDSP = true; + } + + void Hardware::resumeDSP() + { + if(!m_haltDSP) + return; + + { + std::lock_guard uLockHalt(m_haltDSPmutex); + m_haltDSP = false; + } + m_haltDSPcv.notify_one(); } } diff --git a/source/nord/n2x/n2xLib/n2xhardware.h b/source/nord/n2x/n2xLib/n2xhardware.h @@ -1,4 +1,5 @@ #pragma once + #include "n2xdsp.h" #include "n2xmc.h" #include "n2xrom.h" @@ -8,19 +9,55 @@ namespace n2x class Hardware { public: + using AudioOutputs = std::array<std::vector<dsp56k::TWord>, 4>; Hardware(); bool isValid() const; - void process(); + void processUC(); Microcontroller& getUC() {return m_uc; } void ucYieldLoop(const std::function<bool()>& _continue); + const auto& getAudioOutputs() const { return m_audioOutputs; } + + void processAudio(const uint32_t _frames) + { + return processAudio(_frames, _frames); + } + private: + void processAudio(uint32_t _frames, uint32_t _latency); + void ensureBufferSize(uint32_t _frames); + void onEsaiCallback(); + void syncUCtoDSP(); + void haltDSP(); + void resumeDSP(); + Rom m_rom; Microcontroller m_uc; DSP m_dspA; DSP m_dspB; + + std::vector<dsp56k::TWord> m_dummyInput; + std::vector<dsp56k::TWord> m_dummyOutput; + std::vector<dsp56k::TWord> m_dspAtoBBuffer; + + AudioOutputs m_audioOutputs; + + // timing + const double m_samplerateInv; + uint32_t m_esaiFrameIndex = 0; + uint32_t m_lastEsaiFrameIndex = 0; + int64_t m_remainingUcCycles = 0; + double m_remainingUcCyclesD = 0; + std::mutex m_esaiFrameAddedMutex; + std::condition_variable m_esaiFrameAddedCv; + std::mutex m_requestedFramesAvailableMutex; + std::condition_variable m_requestedFramesAvailableCv; + size_t m_requestedFrames = 0; + bool m_haltDSP = false; + std::condition_variable m_haltDSPcv; + std::mutex m_haltDSPmutex; }; }