commit 32cc0ffb7f616e25ae8360ae2880f664803ed4c9
parent 9d83dc8176bd616f907a8142663562a0ba86eb5c
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date: Wed, 24 Jul 2024 00:07:57 +0200
UC/DSP syncronization
Diffstat:
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;
};
}