From b394ddc181121499e570038f044b100bcbefa185 Mon Sep 17 00:00:00 2001 From: stoneface <44489054+stoneface86@users.noreply.github.com> Date: Tue, 6 Jul 2021 23:07:25 -0400 Subject: [PATCH] Engine::Frame is now just Frame Tempo effect now changes speed immediately remove SyncWorker --- CMakeLists.txt | 3 +- README.md | 3 - include/trackerboy/engine/Engine.hpp | 28 +--- include/trackerboy/engine/Frame.hpp | 56 ++++++++ include/trackerboy/engine/MusicRuntime.hpp | 16 +-- include/trackerboy/export/Player.hpp | 2 +- include/trackerboy/trackerboy.hpp | 40 +++--- libtrackerboy/engine/Engine.cpp | 26 ++-- libtrackerboy/engine/MusicRuntime.cpp | 59 ++++----- libtrackerboy/engine/Timer.cpp | 4 +- libtrackerboy/export/Player.cpp | 6 +- ui/CMakeLists.txt | 1 - ui/core/SyncWorker.cpp | 146 --------------------- ui/core/SyncWorker.hpp | 59 --------- ui/core/WavExporter.cpp | 5 +- ui/core/audio/RenderFrame.hpp | 29 ---- ui/core/audio/Renderer.cpp | 9 +- ui/core/audio/Renderer.hpp | 5 +- ui/forms/MainWindow.cpp | 142 ++++++++++++++------ ui/forms/MainWindow.hpp | 14 +- ui/resources/stylesheet.qss | 5 + 21 files changed, 244 insertions(+), 414 deletions(-) create mode 100644 include/trackerboy/engine/Frame.hpp delete mode 100644 ui/core/SyncWorker.cpp delete mode 100644 ui/core/SyncWorker.hpp delete mode 100644 ui/core/audio/RenderFrame.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5264e0b7..ef818bde 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,8 @@ if (SOUND_REQUIRED) # 1. exporting to WAV (wav encoder) # 2. Ringbuffer, ma_rb target_compile_definitions(miniaudio PUBLIC - MA_NO_DECODING + MA_NO_MP3 + MA_NO_FLAC MA_NO_GENERATION MA_NO_DEVICE_IO MA_NO_THREADING diff --git a/README.md b/README.md index 50d3c948..1309cbce 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ --- [![build-lib][build-badge]][build-link] -[![tests][tests-badge]][tests-link] [![Discord](https://img.shields.io/discord/770034905231917066?svg=true)](https://discord.gg/m6wcAK3) @@ -49,5 +48,3 @@ This project is licensed under the MIT License - See [LICENSE](LICENSE) for more [build-badge]: https://github.com/stoneface86/trackerboy/workflows/build/badge.svg [build-link]: https://github.com/stoneface86/trackerboy/actions?query=workflow%3Abuild -[tests-badge]: https://github.com/stoneface86/trackerboy/workflows/tests/badge.svg -[tests-link]: https://github.com/stoneface86/trackerboy/actions?query=workflow%3Atests diff --git a/include/trackerboy/engine/Engine.hpp b/include/trackerboy/engine/Engine.hpp index cf446b22..eeb8c492 100644 --- a/include/trackerboy/engine/Engine.hpp +++ b/include/trackerboy/engine/Engine.hpp @@ -25,6 +25,7 @@ #pragma once #include "trackerboy/data/Module.hpp" +#include "trackerboy/engine/Frame.hpp" #include "trackerboy/engine/IApu.hpp" #include "trackerboy/engine/MusicRuntime.hpp" #include "trackerboy/engine/RuntimeContext.hpp" @@ -39,28 +40,6 @@ class Engine { public: - struct Frame { - constexpr Frame() : - halted(false), - startedNewRow(false), - startedNewPattern(false), - time(0), - speed(0), - order(0), - row(0) - { - } - - bool halted; // halt status - bool startedNewRow; - bool startedNewPattern; - uint32_t time; // time index - Speed speed; // the current engine speed - int order; // current order index - int row; // current row index - }; - - Engine(IApu &apu, Module const* mod = nullptr); Module const* getModule() const; @@ -111,15 +90,12 @@ class Engine { void clearChannel(ChType ch); IApu &mApu; - //Module &mModule; Module const* mModule; std::optional mRc; std::optional mMusicContext; //TODO: sfx runtime - - // frames elapsed since last reset - uint32_t mTime; + int mTime; bool mPatternRepeat; diff --git a/include/trackerboy/engine/Frame.hpp b/include/trackerboy/engine/Frame.hpp new file mode 100644 index 00000000..95a2b020 --- /dev/null +++ b/include/trackerboy/engine/Frame.hpp @@ -0,0 +1,56 @@ +/* +** Trackerboy - Gameboy / Gameboy Color music tracker +** Copyright (C) 2019-2021 stoneface86 +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in all +** copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +** SOFTWARE. +** +*/ + +#pragma once + +#include "trackerboy/trackerboy.hpp" + +namespace trackerboy { + +// +// Informational struct about the current frame being stepped. +// +struct Frame { + + constexpr Frame() : + halted(false), + startedNewRow(false), + startedNewPattern(false), + speed(0), + time(0), + order(0), + row(0) + { + } + + bool halted; // halt status + bool startedNewRow; + bool startedNewPattern; + Speed speed; // the current engine speed + int time; // time index + int order; // current order index + int row; // current row index +}; + +} diff --git a/include/trackerboy/engine/MusicRuntime.hpp b/include/trackerboy/engine/MusicRuntime.hpp index f2d2e2d5..ec1f3bd7 100644 --- a/include/trackerboy/engine/MusicRuntime.hpp +++ b/include/trackerboy/engine/MusicRuntime.hpp @@ -2,6 +2,7 @@ #pragma once #include "trackerboy/data/Song.hpp" +#include "trackerboy/engine/Frame.hpp" #include "trackerboy/engine/FrequencyControl.hpp" #include "trackerboy/engine/GlobalState.hpp" #include "trackerboy/engine/InstrumentRuntime.hpp" @@ -27,16 +28,6 @@ class MusicRuntime { public: MusicRuntime(Song const& song, int orderNo, int patternRow, bool patternRepeat = false); - int currentOrder() const noexcept; - - int currentRow() const noexcept; - - Speed currentSpeed() const noexcept; - - bool hasNewRow() const noexcept; - - bool hasNewPattern() const noexcept; - void halt(RuntimeContext const& rc); void lock(RuntimeContext const& rc, ChType ch); @@ -47,7 +38,7 @@ class MusicRuntime { void unlock(RuntimeContext const& rc, ChType ch); - bool step(RuntimeContext const& rc); + bool step(RuntimeContext const& rc, Frame &frame); void repeatPattern(bool repeat); @@ -73,9 +64,6 @@ class MusicRuntime { int mOrderCounter; int mRowCounter; - bool mHasNewPattern; - bool mHasNewRow; - bool mPatternRepeat; Timer mTimer; diff --git a/include/trackerboy/export/Player.hpp b/include/trackerboy/export/Player.hpp index e30053b1..a73f90c2 100644 --- a/include/trackerboy/export/Player.hpp +++ b/include/trackerboy/export/Player.hpp @@ -59,7 +59,7 @@ class Player { Engine &mEngine; - Engine::Frame mLastFrame; + Frame mLastFrame; bool mPlaying; ContextVariant mContext; diff --git a/include/trackerboy/trackerboy.hpp b/include/trackerboy/trackerboy.hpp index 638b6544..b96902f3 100644 --- a/include/trackerboy/trackerboy.hpp +++ b/include/trackerboy/trackerboy.hpp @@ -92,6 +92,25 @@ enum class Panning : uint8_t { middle }; +template +constexpr T GB_CLOCK_SPEED = T(4194304); + +constexpr float GB_FRAMERATE_DMG = 59.7f; +constexpr float GB_FRAMERATE_SGB = 61.1f; + +// each channel has 5 registers +constexpr unsigned GB_CHANNEL_REGS = 5; + +// +// Maximum frequency setting for channels 1, 2 and 3 +// +constexpr uint16_t GB_MAX_FREQUENCY = 2047; + +// +// CH3 waveram is 16 bytes +// +constexpr size_t GB_WAVERAM_SIZE = 16; + // // The speed type determines the tempo during pattern playback. Its unit is // frames per row in Q4.4 format. Speeds with a fractional component will @@ -111,29 +130,16 @@ constexpr float speedToFloat(Speed speed) { return speed * (1.0f / (1 << SPEED_FRACTION_BITS)); } +constexpr float speedToTempo(float speed, int rowsPerBeat = 4, float framerate = GB_FRAMERATE_DMG) { + return (framerate * 60.0f) / (speed * rowsPerBeat); +} + constexpr size_t TABLE_SIZE = 64; constexpr size_t MAX_INSTRUMENTS = TABLE_SIZE; constexpr size_t MAX_WAVEFORMS = TABLE_SIZE; constexpr size_t MAX_PATTERNS = 256; -template -constexpr T GB_CLOCK_SPEED = T(4194304); - -constexpr float GB_FRAMERATE_DMG = 59.7f; -constexpr float GB_FRAMERATE_SGB = 61.1f; - -// each channel has 5 registers -constexpr unsigned GB_CHANNEL_REGS = 5; -// -// Maximum frequency setting for channels 1, 2 and 3 -// -constexpr uint16_t GB_MAX_FREQUENCY = 2047; - -// -// CH3 waveram is 16 bytes -// -constexpr size_t GB_WAVERAM_SIZE = 16; diff --git a/libtrackerboy/engine/Engine.cpp b/libtrackerboy/engine/Engine.cpp index cd706f1e..75304337 100644 --- a/libtrackerboy/engine/Engine.cpp +++ b/libtrackerboy/engine/Engine.cpp @@ -38,7 +38,6 @@ void Engine::setModule(Module const* mod) { void Engine::reset() { mMusicContext.reset(); - mTime = 0; } void Engine::play(int orderNo, int patternRow) { @@ -54,6 +53,7 @@ void Engine::play(int orderNo, int patternRow) { } mMusicContext.emplace(song, orderNo, patternRow, mPatternRepeat); + mTime = 0; } } @@ -94,26 +94,19 @@ void Engine::repeatPattern(bool repeat) { void Engine::step(Frame &frame) { if (mMusicContext) { - frame.halted = mMusicContext->step(*mRc); - frame.startedNewRow = mMusicContext->hasNewRow(); - frame.startedNewPattern = mMusicContext->hasNewPattern(); - frame.order = mMusicContext->currentOrder(); - frame.row = mMusicContext->currentRow(); - frame.speed = mMusicContext->currentSpeed(); + frame.time = mTime; + frame.halted = mMusicContext->step(*mRc, frame); + + // increment timestamp for next frame + if (!frame.halted) { + ++mTime; + } } else { - // no runtime, do nothing frame.halted = true; - frame.order = 0; - frame.row = 0; - frame.speed = 0; } - - frame.time = mTime; - // TODO: sound effects - // increment timestamp for next frame - ++mTime; + } void Engine::clearChannel(ChType ch) { @@ -137,5 +130,4 @@ void Engine::clearChannel(ChType ch) { } - } diff --git a/libtrackerboy/engine/MusicRuntime.cpp b/libtrackerboy/engine/MusicRuntime.cpp index 6b78d364..11b8611d 100644 --- a/libtrackerboy/engine/MusicRuntime.cpp +++ b/libtrackerboy/engine/MusicRuntime.cpp @@ -10,9 +10,9 @@ MusicRuntime::MusicRuntime(Song const& song, int orderNo, int patternRow, bool p mSong(song), mOrderCounter(orderNo), mRowCounter(patternRow), - mHasNewPattern(false), - mHasNewRow(false), mPatternRepeat(patternRepeat), + mTimer(), + mGlobal(), mFlags(DEFAULT_FLAGS), mStates{ ChannelState(ChType::ch1), @@ -28,26 +28,6 @@ MusicRuntime::MusicRuntime(Song const& song, int orderNo, int patternRow, bool p mTimer.setPeriod(song.speed()); } -int MusicRuntime::currentOrder() const noexcept { - return mOrderCounter; -} - -int MusicRuntime::currentRow() const noexcept { - return mRowCounter; -} - -Speed MusicRuntime::currentSpeed() const noexcept { - return mTimer.period(); -} - -bool MusicRuntime::hasNewRow() const noexcept { - return mHasNewRow; -} - -bool MusicRuntime::hasNewPattern() const noexcept { - return mHasNewPattern; -} - void MusicRuntime::halt(RuntimeContext const &rc) { mFlags.set(FLAG_HALT); haltChannels(rc); @@ -98,8 +78,9 @@ void MusicRuntime::repeatPattern(bool repeat) { mPatternRepeat = repeat; } -bool MusicRuntime::step(RuntimeContext const& rc) { +bool MusicRuntime::step(RuntimeContext const& rc, Frame &frame) { if (mFlags.test(FLAG_HALT)) { + // runtime is halted, do nothing return true; } @@ -112,11 +93,13 @@ bool MusicRuntime::step(RuntimeContext const& rc) { mFlags.reset(FLAG_INIT); } - mHasNewPattern = false; - // if timer is active, we are starting a new row - mHasNewRow = mTimer.active(); - if (mHasNewRow) { + frame.startedNewRow = mTimer.active(); + // this gets set to true if: + // 1. we have started a new row + // 2. a pattern command was set (jump or next) + frame.startedNewPattern = false; + if (frame.startedNewRow) { // change the current pattern if needed if (mGlobal.patternCommand != Operation::PatternCommand::none && mPatternRepeat) { @@ -133,19 +116,18 @@ bool MusicRuntime::step(RuntimeContext const& rc) { } mRowCounter = mGlobal.patternCommandParam; mGlobal.patternCommand = Operation::PatternCommand::none; - mHasNewPattern = true; + frame.startedNewPattern = true; break; case Operation::PatternCommand::jump: mRowCounter = 0; // if the parameter goes past the last one, use the last one mOrderCounter = std::min(mGlobal.patternCommandParam, (uint8_t)(mSong.order().size() - 1)); mGlobal.patternCommand = Operation::PatternCommand::none; - mHasNewPattern = true; + frame.startedNewPattern = true; break; } } - // set row data to our track controls mTc1.setRow(mSong.getRow(ChType::ch1, mOrderCounter, mRowCounter)); mTc2.setRow(mSong.getRow(ChType::ch2, mOrderCounter, mRowCounter)); @@ -154,20 +136,24 @@ bool MusicRuntime::step(RuntimeContext const& rc) { if (mGlobal.halt) { halt(rc); + // halting is immediate, do not continue this row return true; } - // change the speed if the Fxx effect was used - if (mGlobal.speed) { - mTimer.setPeriod(mGlobal.speed); - mGlobal.speed = 0; - } - + frame.row = mRowCounter; + frame.order = mOrderCounter; + } // update channel state and write to registers on locked channels update(rc); + // change the speed if the Fxx effect was used + if (mGlobal.speed) { + mTimer.setPeriod(mGlobal.speed); + mGlobal.speed = 0; + } + frame.speed = mTimer.period(); if (mTimer.step()) { // timer overflow, advance row counter @@ -182,6 +168,7 @@ bool MusicRuntime::step(RuntimeContext const& rc) { } + // runtime did not halt return false; } diff --git a/libtrackerboy/engine/Timer.cpp b/libtrackerboy/engine/Timer.cpp index 9be8b3d1..13af73bb 100644 --- a/libtrackerboy/engine/Timer.cpp +++ b/libtrackerboy/engine/Timer.cpp @@ -43,7 +43,9 @@ void Timer::reset() noexcept { void Timer::setPeriod(Speed period) noexcept { mPeriod = std::min(std::max(period, SPEED_MIN), SPEED_MAX); - // might need to adjust mCounter + // if the counter exceeds the new period, clamp it to 1 unit less + // this way, the timer will overflow on the next call to step + mCounter = std::min(mCounter, (Speed)(mPeriod - UNIT_SPEED)); } bool Timer::step() noexcept { diff --git a/libtrackerboy/export/Player.cpp b/libtrackerboy/export/Player.cpp index 18c98276..118b8823 100644 --- a/libtrackerboy/export/Player.cpp +++ b/libtrackerboy/export/Player.cpp @@ -74,7 +74,7 @@ bool Player::isPlaying() const { } int Player::progress() const { - return std::visit([this](auto&& ctx) { + return std::visit([](auto&& ctx) { using T = std::decay_t; if constexpr (std::is_same_v) { @@ -88,7 +88,7 @@ int Player::progress() const { } int Player::progressMax() const { - return std::visit([this](auto&& ctx) { + return std::visit([](auto&& ctx) { using T = std::decay_t; if constexpr (std::is_same_v) { @@ -103,7 +103,7 @@ int Player::progressMax() const { void Player::step() { if (mPlaying) { - Engine::Frame frame; + Frame frame; mEngine.step(frame); if (frame.halted) { diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index dffc7f84..6886d00d 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -51,7 +51,6 @@ set(UI_SRC "core/PatternSelection.cpp" "core/PianoInput.cpp" "core/samplerates.cpp" - "core/SyncWorker.cpp" "core/WavExporter.cpp" "forms/AboutDialog.cpp" diff --git a/ui/core/SyncWorker.cpp b/ui/core/SyncWorker.cpp deleted file mode 100644 index 0a477af6..00000000 --- a/ui/core/SyncWorker.cpp +++ /dev/null @@ -1,146 +0,0 @@ - -#include "core/SyncWorker.hpp" - -#include -#include - -// -// SyncWorker handles audio synchronization in a separate thread. -// - -SyncWorker::SyncWorker(Renderer &renderer) : //, AudioScope &leftScope, AudioScope &rightScope) : - QObject(), - mRenderer(renderer), - //mLeftScope(leftScope), - //mRightScope(rightScope), - mMutex(), - mSampleBuffer(nullptr), - mSamplesPerFrame(0), - mPeakLeft(0), - mPeakRight(0) -{ - //connect(this, &SyncWorker::updateScopes, &mLeftScope, qOverload<>(&AudioScope::update), Qt::QueuedConnection); - //connect(this, &SyncWorker::updateScopes, &mRightScope, qOverload<>(&AudioScope::update), Qt::QueuedConnection); - connect(&renderer, &Renderer::audioStarted, this, &SyncWorker::onAudioStart, Qt::QueuedConnection); - connect(&renderer, &Renderer::audioStopped, this, &SyncWorker::onAudioStop, Qt::QueuedConnection); - connect(&renderer, &Renderer::frameSync, this, &SyncWorker::onFrameSync, Qt::QueuedConnection); -} - -void SyncWorker::setSamplesPerFrame(size_t samples) { - QMutexLocker locker(&mMutex); - - if (samples != mSamplesPerFrame) { - mSamplesPerFrame = samples; - mSampleBuffer.reset(new int16_t[samples * 2]); - } -} - -void SyncWorker::onAudioStart() { - mLastFrame = {}; -} - -void SyncWorker::onAudioStop() { - // ignore any pending reads on the return buffer - // as we are no longer processing them - //mRenderer.returnBuffer().flush(); - //mRenderer.frameReturnBuffer().flush(); - - // clear peaks - setPeaks(0, 0); - - // clear and update the scopes - //mLeftScope.clear(); - //mRightScope.clear(); - - emit updateScopes(); -} - -void SyncWorker::onFrameSync() { - QMutexLocker locker(&mMutex); - - auto frame = mRenderer.currentFrame(); - // check if the player position changed - if (mLastFrame.engineFrame.order != frame.order || - mLastFrame.engineFrame.row != frame.row) { - emit positionChanged({ (int)frame.order, (int)frame.row }); - } - - // check if the speed changed - auto speed = frame.speed; - if (mLastFrame.engineFrame.speed != speed) { - float speedF = (speed >> 4) + ((speed & 0xF) * (1.0f / 16.0f)); - emit speedChanged(tr("%1 FPR").arg(speedF, 0, 'f', 3)); - } - - mLastFrame.engineFrame = frame; -} - -// void SyncWorker::onAudioSync() { -// QMutexLocker locker(&mMutex); - -// // check for any new frames -// auto frameReturn = mRenderer.frameReturnBuffer(); -// size_t toRead = 1; -// auto rframe = frameReturn.acquireRead(toRead); -// if (toRead) { -// // new frame, process it - -// // check if the player position changed -// if (mLastFrame.engineFrame.order != rframe->engineFrame.order || -// mLastFrame.engineFrame.row != rframe->engineFrame.row) { -// emit positionChanged({ rframe->engineFrame.order, rframe->engineFrame.row }); -// } - -// // check if the speed changed -// auto speed = rframe->engineFrame.speed; -// if (mLastFrame.engineFrame.speed != speed) { -// float speedF = (speed >> 4) + ((speed & 0xF) * (1.0f / 16.0f)); -// emit speedChanged(tr("%1 FPR").arg(speedF, 0, 'f', 3)); -// } - -// mLastFrame = *rframe; -// } -// frameReturn.commitRead(rframe, toRead); - -// auto returnBuffer = mRenderer.returnBuffer(); -// auto avail = returnBuffer.availableRead(); -// auto frameCount = avail / mSamplesPerFrame; -// if (frameCount) { -// if (frameCount > 1) { -// // skip these frames -// returnBuffer.seekRead((frameCount - 1) * mSamplesPerFrame); -// } -// // read the frame -// returnBuffer.fullRead(mSampleBuffer.get(), mSamplesPerFrame); - -// // determine peak amplitudes for each channel -// int16_t peakLeft = 0; -// int16_t peakRight = 0; -// auto samplePtr = mSampleBuffer.get(); -// for (size_t i = 0; i != mSamplesPerFrame; ++i) { -// auto sampleLeft = (int16_t)abs(*samplePtr++); -// auto sampleRight = (int16_t)abs(*samplePtr++); -// peakLeft = std::max(sampleLeft, peakLeft); -// peakRight = std::max(sampleRight, peakRight); -// } -// setPeaks(peakLeft, peakRight); - -// // send to visualizers -// mLeftScope.render(mSampleBuffer.get(), mSamplesPerFrame); -// mRightScope.render(mSampleBuffer.get() + 1, mSamplesPerFrame); - -// // calls AudioScope::update via event queue (cannot call directly as this thread is not the GUI thread) -// emit updateScopes(); - -// } - -// } - - -void SyncWorker::setPeaks(int16_t peakLeft, int16_t peakRight) { - if (mPeakLeft != peakLeft || mPeakRight != peakRight) { - mPeakLeft = peakLeft; - mPeakRight = peakRight; - emit peaksChanged(peakLeft, peakRight); - } -} diff --git a/ui/core/SyncWorker.hpp b/ui/core/SyncWorker.hpp deleted file mode 100644 index 39203816..00000000 --- a/ui/core/SyncWorker.hpp +++ /dev/null @@ -1,59 +0,0 @@ - -#pragma once - -#include "core/audio/Renderer.hpp" -#include "widgets/visualizers/AudioScope.hpp" - -#include -#include -#include -#include - -// -// Worker object that updates visualizers, tracker position, etc, on audio sync. -// -class SyncWorker : public QObject { - Q_OBJECT - -public: - SyncWorker(Renderer &renderer); //, AudioScope &left, AudioScope &right); - - void setSamplesPerFrame(size_t samples); - -signals: - void updateScopes(); - - // - // Tracker position is stored as a QPoint with x = pattern id, y = row - // - void positionChanged(QPoint const point); - - void speedChanged(QString const& speed); - - void peaksChanged(qint16 peakLeft, qint16 peakRight); - -public slots: - void onAudioStop(); - void onAudioStart(); - void onFrameSync(); - -private: - Q_DISABLE_COPY(SyncWorker) - - void setPeaks(int16_t peakLeft, int16_t peakRight); - - Renderer &mRenderer; - //AudioScope &mLeftScope; - //AudioScope &mRightScope; - - QMutex mMutex; - std::unique_ptr mSampleBuffer; - size_t mSamplesPerFrame; - - // current volume peaks - int16_t mPeakLeft; - int16_t mPeakRight; - - RenderFrame mLastFrame; - -}; diff --git a/ui/core/WavExporter.cpp b/ui/core/WavExporter.cpp index 845ade60..e2675838 100644 --- a/ui/core/WavExporter.cpp +++ b/ui/core/WavExporter.cpp @@ -64,7 +64,7 @@ void WavExporter::run() { emit progress(lastProgress); auto &apu = mSynth.apu(); - while (player.isPlaying()) { + for (;;) { mMutex.lock(); if (mAbort) { @@ -81,6 +81,9 @@ void WavExporter::run() { } player.step(); + if (!player.isPlaying()) { + break; + } mSynth.run(); auto samplesRead = apu.readSamples(buffer.data(), buffer.size()); diff --git a/ui/core/audio/RenderFrame.hpp b/ui/core/audio/RenderFrame.hpp deleted file mode 100644 index b03c9af0..00000000 --- a/ui/core/audio/RenderFrame.hpp +++ /dev/null @@ -1,29 +0,0 @@ - -#pragma once - -#include "trackerboy/engine/Engine.hpp" - -#include "gbapu.hpp" - -#include -#include - -// -// Structure for a previously renderered frame, for visualizers -// -struct RenderFrame { - - constexpr RenderFrame() : - engineFrame(), - registers{ 0 } - { - - } - - // engine information - trackerboy::Engine::Frame engineFrame; - - // APU register dump - gbapu::Registers registers; - -}; diff --git a/ui/core/audio/Renderer.cpp b/ui/core/audio/Renderer.cpp index a97b7590..c53b37a5 100644 --- a/ui/core/audio/Renderer.cpp +++ b/ui/core/audio/Renderer.cpp @@ -68,6 +68,11 @@ Renderer::Renderer(QObject *parent) : Renderer::~Renderer() { mTimer->stop(); + + if (mStream.isRunning()) { + mStream.stop(); + } + mTimerThread.quit(); mTimerThread.wait(); } @@ -108,7 +113,7 @@ ModuleDocument* Renderer::documentPlayingMusic() { return handle->musicDocument; } -trackerboy::Engine::Frame Renderer::currentFrame() { +trackerboy::Frame Renderer::currentFrame() { auto handle = mContext.access(); return handle->currentEngineFrame; } @@ -140,7 +145,7 @@ bool Renderer::setConfig(Config::Sound const &soundConfig) { auto const samplerate = SAMPLERATE_TABLE[soundConfig.samplerateIndex]; if (samplerate != handle->synth.samplerate()) { handle->synth.setSamplingRate(samplerate); - reloadRegisters = true; + reloadRegisters = wasRunning; } handle->synth.apu().setQuality(static_cast(soundConfig.quality)); handle->synth.setupBuffers(); diff --git a/ui/core/audio/Renderer.hpp b/ui/core/audio/Renderer.hpp index fe952f31..582131d3 100644 --- a/ui/core/audio/Renderer.hpp +++ b/ui/core/audio/Renderer.hpp @@ -2,7 +2,6 @@ #pragma once #include "core/audio/AudioStream.hpp" -#include "core/audio/RenderFrame.hpp" #include "core/audio/Ringbuffer.hpp" #include "core/audio/VisualizerBuffer.hpp" #include "core/model/ModuleDocument.hpp" @@ -96,7 +95,7 @@ class Renderer : public QObject { // // Note: Function is thread-safe // - trackerboy::Engine::Frame currentFrame(); + trackerboy::Frame currentFrame(); // // Configures the output device with the given Sound config. If device @@ -232,7 +231,7 @@ private slots: PreviewState previewState; trackerboy::ChType previewChannel; - trackerboy::Engine::Frame currentEngineFrame; + trackerboy::Frame currentEngineFrame; State state; int stopCounter; diff --git a/ui/forms/MainWindow.cpp b/ui/forms/MainWindow.cpp index a9aff9a9..2e253e3a 100644 --- a/ui/forms/MainWindow.cpp +++ b/ui/forms/MainWindow.cpp @@ -74,9 +74,7 @@ MainWindow::MainWindow() : mSidebar(), mPatternEditor(mPianoInput), mPlayAndStopShortcut(&mPatternEditor), - mRenderer(), - mSyncWorker(new SyncWorker(mRenderer)), //, mLeftScope, mRightScope)), - mSyncWorkerThread() + mRenderer() { mMidiReceiver = &mPatternEditor; @@ -131,13 +129,6 @@ MainWindow::MainWindow() : // initialize document state onTabChanged(-1); - // audio sync worker thread - mSyncWorker->moveToThread(&mSyncWorkerThread); - connect(&mSyncWorkerThread, &QThread::finished, mSyncWorker, &SyncWorker::deleteLater); - mSyncWorkerThread.setObjectName(QStringLiteral("sync worker thread")); - mSyncWorkerThread.start(); - - // apply the read in configuration onConfigApplied(Config::CategoryAll); @@ -145,14 +136,6 @@ MainWindow::MainWindow() : MainWindow::~MainWindow() { - // force stop any ongoing render - //QMetaObject::invokeMethod(mRenderer, &Renderer::forceStop, Qt::BlockingQueuedConnection); - //mRenderer.forceStop(); - - // quit and wait for threads to finish - mSyncWorkerThread.quit(); - mSyncWorkerThread.wait(); - } QMenu* MainWindow::createPopupMenu() { @@ -294,11 +277,6 @@ void MainWindow::onConfigApplied(Config::Categories categories) { auto samplerate = SAMPLERATE_TABLE[sound.samplerateIndex]; mStatusSamplerate.setText(tr("%1 Hz").arg(samplerate)); - auto samplesPerFrame = samplerate / 60; - mSyncWorker->setSamplesPerFrame(samplesPerFrame); - //mLeftScope.setDuration(samplesPerFrame); - //mRightScope.setDuration(samplesPerFrame); - mErrorSinceLastConfig = !mRenderer.setConfig(sound); if (mErrorSinceLastConfig) { setPlayingStatus(PlayingStatusText::error); @@ -391,19 +369,13 @@ void MainWindow::showExportWavDialog() { delete dialog; } -void MainWindow::trackerPositionChanged(QPoint const pos) { - auto pattern = pos.x(); - auto row = pos.y(); - - auto doc = mBrowserModel.currentDocument(); - if (doc == mRenderer.documentPlayingMusic()) { - doc->patternModel().setTrackerCursor(row, pattern); +void MainWindow::onAudioStart() { + if (!mRenderer.isRunning()) { + return; } - mStatusPos.setText(QStringLiteral("%1 / %2").arg(pattern).arg(row)); -} - -void MainWindow::onAudioStart() { + mLastEngineFrame = {}; + mFrameSkip = 0; setPlayingStatus(PlayingStatusText::playing); auto doc = mRenderer.documentPlayingMusic(); if (doc) { @@ -429,6 +401,10 @@ void MainWindow::onAudioError() { } void MainWindow::onAudioStop() { + if (mRenderer.isRunning()) { + return; // sometimes it takes too long for this signal to get here + } + if (!mErrorSinceLastConfig) { setPlayingStatus(PlayingStatusText::ready); } @@ -439,6 +415,68 @@ void MainWindow::onAudioStop() { } } +void MainWindow::onFrameSync() { + // this slot is called when the renderer has renderered a new frame + // sync is a bit misleading here, as this slot is called when this frame + // is in process of being bufferred. It is not the current frame being played out. + + auto frame = mRenderer.currentFrame(); + auto doc = mBrowserModel.currentDocument(); + if (doc == nullptr) { + return; + } + + bool const docIsPlaying = doc == mRenderer.documentPlayingMusic(); + + // check if the player position changed + if (frame.startedNewRow) { + if (docIsPlaying) { + // update tracker position + doc->patternModel().setTrackerCursor(frame.row, frame.order); + } + + // update position status + mStatusPos.setText(QStringLiteral("%1 / %2") + .arg(frame.order, 2, 10, QChar('0')) + .arg(frame.row, 2, 10, QChar('0'))); + } + + // check if the speed changed + if (mLastEngineFrame.speed != frame.speed) { + auto speedF = trackerboy::speedToFloat(frame.speed); + // update speed status + mStatusSpeed.setText(tr("%1 FPR").arg(speedF, 0, 'f', 3)); + auto tempo = trackerboy::speedToTempo(speedF, doc->songModel().rowsPerBeat()); + mStatusTempo.setText(tr("%1 BPM").arg(tempo, 0, 'f', 2)); + } + + constexpr auto FRAME_SKIP = 30; + + if (mLastEngineFrame.time != frame.time) { + // determine elapsed time + if (mFrameSkip == 0) { + + auto framerate = doc->framerate(); + int elapsed = frame.time / framerate; + int secs = elapsed; + int mins = secs / 60; + secs = secs % 60; + + QString str = QStringLiteral("%1:%2") + .arg(mins, 2, 10, QChar('0')) + .arg(secs, 2, 10, QChar('0')); + mStatusElapsed.setText(str); + + + mFrameSkip = FRAME_SKIP; + } else { + --mFrameSkip; + } + } + + mLastEngineFrame = frame; +} + void MainWindow::onTabChanged(int tabIndex) { auto previousDocument = mBrowserModel.currentDocument(); mBrowserModel.setCurrentDocument(tabIndex); @@ -970,8 +1008,8 @@ void MainWindow::setupUi() { mToolbarInput.addWidget(&mOctaveSpin); mToolbarInput.addWidget(&mEditStepLabel); mToolbarInput.addWidget(&mEditStepSpin); + mToolbarInput.addSeparator(); mToolbarInput.addAction(&mActions[ActionEditKeyRepetition]); - mToolbarInput.setStyleSheet(QStringLiteral("spacing: 8px;")); mOctaveSpin.setRange(2, 8); mOctaveSpin.setValue(mPianoInput.octave()); mEditStepSpin.setRange(0, 255); @@ -1011,18 +1049,39 @@ void MainWindow::setupUi() { // STATUSBAR ============================================================== auto statusbar = statusBar(); + + mStatusRenderer.setMinimumWidth(60); + mStatusSpeed.setMinimumWidth(60); + mStatusTempo.setMinimumWidth(60); + mStatusElapsed.setMinimumWidth(40); + mStatusPos.setMinimumWidth(40); + mStatusSamplerate.setMinimumWidth(60); + + { + for (auto label : { &mStatusRenderer, &mStatusSpeed, &mStatusTempo, &mStatusElapsed, &mStatusPos, &mStatusSamplerate }) { + label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + } + } - mStatusRenderer.setText(tr("Ready")); statusbar->addPermanentWidget(&mStatusRenderer); - statusbar->addPermanentWidget(&mStatusFramerate); statusbar->addPermanentWidget(&mStatusSpeed); statusbar->addPermanentWidget(&mStatusTempo); - mStatusElapsed.setText(QStringLiteral("00:00:00")); statusbar->addPermanentWidget(&mStatusElapsed); - mStatusPos.setText(QStringLiteral("00 / 00")); statusbar->addPermanentWidget(&mStatusPos); statusbar->addPermanentWidget(&mStatusSamplerate); + statusbar->showMessage(tr("Trackerboy v%1.%2.%3") + .arg(trackerboy::VERSION.major) + .arg(trackerboy::VERSION.minor) + .arg(trackerboy::VERSION.patch)); + + // default statuses + setPlayingStatus(PlayingStatusText::ready); + mStatusSpeed.setText(tr("6.000 FPR")); + mStatusTempo.setText(tr("150 BPM")); + mStatusElapsed.setText(QStringLiteral("00:00")); + mStatusPos.setText(QStringLiteral("00 / 00")); + // no need to set samplerate, it is done so in onConfigApplied // CONNECTIONS ============================================================ @@ -1194,14 +1253,11 @@ void MainWindow::setupUi() { mMidiNoteDown = false; }); - // sync worker - //connect(mSyncWorker, &SyncWorker::peaksChanged, &mPeakMeter, &PeakMeter::setPeaks, Qt::QueuedConnection); - connect(mSyncWorker, &SyncWorker::positionChanged, this, &MainWindow::trackerPositionChanged, Qt::QueuedConnection); - connect(mSyncWorker, &SyncWorker::speedChanged, &mStatusSpeed, &QLabel::setText, Qt::QueuedConnection); connect(&mRenderer, &Renderer::audioStarted, this, &MainWindow::onAudioStart); connect(&mRenderer, &Renderer::audioStopped, this, &MainWindow::onAudioStop); connect(&mRenderer, &Renderer::audioError, this, &MainWindow::onAudioError); + connect(&mRenderer, &Renderer::frameSync, this, &MainWindow::onFrameSync); connect(&mMenuWindow, &QMenu::aboutToShow, this, &MainWindow::updateWindowMenu); diff --git a/ui/forms/MainWindow.hpp b/ui/forms/MainWindow.hpp index d64d5af0..63304eca 100644 --- a/ui/forms/MainWindow.hpp +++ b/ui/forms/MainWindow.hpp @@ -6,7 +6,6 @@ #include "core/model/ModuleDocument.hpp" #include "core/model/ModuleModel.hpp" #include "core/model/InstrumentChoiceModel.hpp" -#include "core/SyncWorker.hpp" #include "forms/AboutDialog.hpp" #include "forms/AudioDiagDialog.hpp" #include "forms/ConfigDialog.hpp" @@ -78,12 +77,10 @@ private slots: void showConfigDialog(); void showExportWavDialog(); - // statusbar - void trackerPositionChanged(QPoint const pos); - void onAudioStart(); void onAudioError(); void onAudioStop(); + void onFrameSync(); void onTabChanged(int tabIndex); @@ -158,6 +155,8 @@ private slots: ModuleModel mBrowserModel; bool mErrorSinceLastConfig; + trackerboy::Frame mLastEngineFrame; + int mFrameSkip; // dialogs AboutDialog *mAboutDialog; @@ -207,7 +206,6 @@ private slots: // statusbar widgets QLabel mStatusRenderer; - QLabel mStatusFramerate; QLabel mStatusSpeed; QLabel mStatusTempo; QLabel mStatusElapsed; @@ -309,10 +307,4 @@ private slots: Renderer mRenderer; - // workers / threading - - SyncWorker *mSyncWorker; - QThread mSyncWorkerThread; - - }; diff --git a/ui/resources/stylesheet.qss b/ui/resources/stylesheet.qss index d2cdc12c..b5bc247e 100644 --- a/ui/resources/stylesheet.qss +++ b/ui/resources/stylesheet.qss @@ -56,3 +56,8 @@ Sidebar QTableView QHeaderView::section { GraphEdit { background-color: black; } + +QToolBar QLabel { + padding-left: 3px; + padding-right: 3px; +}