From: IOhannes m zmölnig (Debian/GNU) Date: Mon, 31 Jan 2022 10:57:19 +0000 (+0100) Subject: New upstream version 0.20.0+ds1 X-Git-Tag: archive/raspbian/0.20.1+ds1-1+rpi1~1^2~12^2~1 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=9cc1e2e8e9ce4e31539c6837f920e2d256d99026;p=giada.git New upstream version 0.20.0+ds1 --- diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c4427e..1a0ca06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ list(APPEND SOURCES src/gui/dispatcher.cpp src/gui/updater.cpp src/gui/drawing.cpp + src/gui/dialogs/progress.cpp src/gui/dialogs/keyGrabber.cpp src/gui/dialogs/about.cpp src/gui/dialogs/mainWindow.cpp @@ -307,8 +308,9 @@ endif() set(FLTK_SKIP_FLUID TRUE) # Don't search for FLTK's fluid set(FLTK_SKIP_OPENGL TRUE) # Don't search for FLTK's OpenGL -find_package(FLTK CONFIG REQUIRED) -list(APPEND LIBRARIES fltk fltk_gl fltk_forms fltk_images) +find_package(FLTK REQUIRED NO_MODULE) +list(APPEND LIBRARIES fltk) +list(APPEND INCLUDE_DIRS ${FLTK_INCLUDE_DIRS}) message("FLTK library found in " ${FLTK_DIR}) # Libsndfile @@ -366,7 +368,7 @@ endif() find_package(SampleRate CONFIG) if (SampleRate_FOUND) list(APPEND LIBRARIES SampleRate::samplerate) - message("Libsndfile library found in " ${SampleRate_DIR}) + message("Libsamplerate library found in " ${SampleRate_DIR}) else() # Fallback to find_library mode (in case Libsamplerate is too old). find_library(LIBRARY_SAMPLERATE @@ -581,4 +583,4 @@ elseif(DEFINED OS_MACOS) set_target_properties(giada PROPERTIES XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES) -endif() \ No newline at end of file +endif() diff --git a/ChangeLog b/ChangeLog index 815cc33..4adddd4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,21 @@ -------------------------------------------------------------------------------- +0.20.0 --- 2022 . 01 . 24 +- Show progress bar for long operations +- Improved rendering algorithm for sample channels +- Fix wrong sample tail rendering when pitch != 1.0 +- Always display play head in Action Editor (fix #534) +- Fix re-initialization order of engine sub-components (fixes #533) +- Change 'kill chan' wording to 'stop note' in Action Editor (fixes #532) +- Update solo count when deleting a channel (fixes #540) +- Update Main Window title saving a new project (fixes #541) +- [Config] Don't skip MIDI device fetching if one of the ports fail to open +- [CMake] Include FLTK as suggested in the official docs +- Add more unit tests for some Channel components +- Minor cleanups and refactoring + + 0.19.2 --- 2021 . 12 . 16 - Fix wrong computation of soloed channels diff --git a/src/core/channels/channel.cpp b/src/core/channels/channel.cpp index 5aa00ab..6c9cb87 100644 --- a/src/core/channels/channel.cpp +++ b/src/core/channels/channel.cpp @@ -60,14 +60,14 @@ mcl::AudioBuffer::Pan calcPanning_(float pan) /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ -Channel::Shared::Shared(Frame bufferSize) +ChannelShared::ChannelShared(Frame bufferSize) : audioBuffer(bufferSize, G_MAX_IO_CHANS) { } /* -------------------------------------------------------------------------- */ -Channel::Channel(ChannelType type, ID id, ID columnId, Shared& s) +Channel::Channel(ChannelType type, ID id, ID columnId, ChannelShared& s) : shared(&s) , id(id) , type(type) @@ -88,14 +88,14 @@ Channel::Channel(ChannelType type, ID id, ID columnId, Shared& s) case ChannelType::SAMPLE: samplePlayer.emplace(&(shared->resampler.value())); sampleAdvancer.emplace(); - sampleReactor.emplace(id, g_engine.sequencer, g_engine.model); + sampleReactor.emplace(*this, id); audioReceiver.emplace(); sampleActionRecorder.emplace(g_engine.actionRecorder, g_engine.sequencer); break; case ChannelType::PREVIEW: samplePlayer.emplace(&(shared->resampler.value())); - sampleReactor.emplace(id, g_engine.sequencer, g_engine.model); + sampleReactor.emplace(*this, id); break; case ChannelType::MIDI: @@ -116,7 +116,7 @@ Channel::Channel(ChannelType type, ID id, ID columnId, Shared& s) /* -------------------------------------------------------------------------- */ -Channel::Channel(const Patch::Channel& p, Shared& s, float samplerateRatio, Wave* wave) +Channel::Channel(const Patch::Channel& p, ChannelShared& s, float samplerateRatio, Wave* wave) : shared(&s) , id(p.id) , type(p.type) @@ -145,14 +145,14 @@ Channel::Channel(const Patch::Channel& p, Shared& s, float samplerateRatio, Wave case ChannelType::SAMPLE: samplePlayer.emplace(p, samplerateRatio, &(shared->resampler.value()), wave); sampleAdvancer.emplace(); - sampleReactor.emplace(id, g_engine.sequencer, g_engine.model); + sampleReactor.emplace(*this, id); audioReceiver.emplace(p); sampleActionRecorder.emplace(g_engine.actionRecorder, g_engine.sequencer); break; case ChannelType::PREVIEW: samplePlayer.emplace(p, samplerateRatio, &(shared->resampler.value()), nullptr); - sampleReactor.emplace(id, g_engine.sequencer, g_engine.model); + sampleReactor.emplace(*this, id); break; case ChannelType::MIDI: @@ -308,16 +308,19 @@ void Channel::initCallbacks() if (samplePlayer) { - samplePlayer->onLastFrame = [this]() { - sampleAdvancer->onLastFrame(*this, g_engine.sequencer.isRunning()); + samplePlayer->onLastFrame = [this](bool natural) { + sampleAdvancer->onLastFrame(*this, g_engine.sequencer.isRunning(), natural); }; } } /* -------------------------------------------------------------------------- */ -void Channel::advance(const Sequencer::EventBuffer& events) const +void Channel::advance(const Sequencer::EventBuffer& events, Range block, Frame quantizerStep) const { + if (shared->quantizer) + shared->quantizer->advance(block, quantizerStep); + for (const Sequencer::Event& e : events) { if (midiController) @@ -439,8 +442,14 @@ void Channel::renderChannel(mcl::AudioBuffer& out, mcl::AudioBuffer& in, bool au { shared->audioBuffer.clear(); - if (samplePlayer) - samplePlayer->render(*this); + if (samplePlayer && isPlaying()) + { + SamplePlayer::Render render; + while (shared->renderQueue->pop(render)) + ; + samplePlayer->render(*shared, render); + } + if (audioReceiver) audioReceiver->render(*this, in); diff --git a/src/core/channels/channel.h b/src/core/channels/channel.h index 1d1d81c..aa91a9a 100644 --- a/src/core/channels/channel.h +++ b/src/core/channels/channel.h @@ -57,36 +57,44 @@ namespace giada::m { class Plugin; -class Channel final + +struct ChannelShared final { -public: - struct Shared - { - Shared(Frame bufferSize); + ChannelShared(Frame bufferSize); - mcl::AudioBuffer audioBuffer; + mcl::AudioBuffer audioBuffer; #ifdef WITH_VST - juce::MidiBuffer midiBuffer; - Queue midiQueue; + juce::MidiBuffer midiBuffer; + Queue midiQueue; #endif - WeakAtomic tracker = 0; - WeakAtomic playStatus = ChannelStatus::OFF; - WeakAtomic recStatus = ChannelStatus::OFF; - WeakAtomic readActions = false; - bool rewinding = false; - Frame offset = 0; + WeakAtomic tracker = 0; + WeakAtomic playStatus = ChannelStatus::OFF; + WeakAtomic recStatus = ChannelStatus::OFF; + WeakAtomic readActions = false; + + std::optional quantizer; + + /* Optional render queue for sample-based channels. Used by SampleReactor + and SampleAdvancer to instruct SamplePlayer how to render audio. */ + + std::optional> renderQueue = {}; - /* Optional resampler for sample-based channels. Unfortunately a Resampler + /* Optional resampler for sample-based channels. Unfortunately a Resampler object (based on libsamplerate) doesn't like to get copied while rendering audio, so can't live inside WaveReader object (which is copied on model changes by the Swapper mechanism). Let's put it in the shared state here. */ - std::optional resampler = {}; - }; + std::optional resampler = {}; +}; + +/* -------------------------------------------------------------------------- */ - Channel(ChannelType t, ID id, ID columnId, Shared&); - Channel(const Patch::Channel& p, Shared&, float samplerateRatio, Wave* w); +class Channel final +{ +public: + Channel(ChannelType t, ID id, ID columnId, ChannelShared&); + Channel(const Patch::Channel& p, ChannelShared&, float samplerateRatio, Wave* w); Channel(const Channel& o); Channel(Channel&& o) = default; @@ -98,7 +106,7 @@ public: Advances internal state by processing static events (e.g. pre-recorded actions or sequencer events) in the current block. */ - void advance(const Sequencer::EventBuffer& e) const; + void advance(const Sequencer::EventBuffer&, Range, Frame quantizerStep) const; /* render Renders audio data to I/O buffers. */ @@ -123,18 +131,18 @@ public: void setMute(bool); void setSolo(bool); - Shared* shared; - ID id; - ChannelType type; - ID columnId; - float volume; - float volume_i; // Internal volume used for velocity-drives-volume mode on Sample Channels - float pan; - bool armed; - int key; - bool hasActions; - std::string name; - Pixel height; + ChannelShared* shared; + ID id; + ChannelType type; + ID columnId; + float volume; + float volume_i; // Internal volume used for velocity-drives-volume mode on Sample Channels + float pan; + bool armed; + int key; + bool hasActions; + std::string name; + Pixel height; #ifdef WITH_VST std::vector plugins; #endif diff --git a/src/core/channels/channelManager.cpp b/src/core/channels/channelManager.cpp index da24d72..30c5578 100644 --- a/src/core/channels/channelManager.cpp +++ b/src/core/channels/channelManager.cpp @@ -154,14 +154,18 @@ const Patch::Channel ChannelManager::serializeChannel(const Channel& c) /* -------------------------------------------------------------------------- */ -Channel::Shared& ChannelManager::makeShared(ChannelType type, int bufferSize) +ChannelShared& ChannelManager::makeShared(ChannelType type, int bufferSize) { - std::unique_ptr shared = std::make_unique(bufferSize); + std::unique_ptr shared = std::make_unique(bufferSize); if (type == ChannelType::SAMPLE || type == ChannelType::PREVIEW) - shared->resampler = Resampler(static_cast(m_conf.rsmpQuality), G_MAX_IO_CHANS); + { + shared->quantizer.emplace(); + shared->renderQueue.emplace(); + shared->resampler.emplace(static_cast(m_conf.rsmpQuality), G_MAX_IO_CHANS); + } m_model.addShared(std::move(shared)); - return m_model.backShared(); + return m_model.backShared(); } } // namespace giada::m diff --git a/src/core/channels/channelManager.h b/src/core/channels/channelManager.h index 0e32e04..2e1e582 100644 --- a/src/core/channels/channelManager.h +++ b/src/core/channels/channelManager.h @@ -74,7 +74,7 @@ public: const Patch::Channel serializeChannel(const Channel& c); private: - Channel::Shared& makeShared(ChannelType type, int bufferSize); + ChannelShared& makeShared(ChannelType type, int bufferSize); IdManager m_channelId; diff --git a/src/core/channels/sampleAdvancer.cpp b/src/core/channels/sampleAdvancer.cpp index e029c34..9f0391a 100644 --- a/src/core/channels/sampleAdvancer.cpp +++ b/src/core/channels/sampleAdvancer.cpp @@ -30,7 +30,7 @@ namespace giada::m { -void SampleAdvancer::onLastFrame(const Channel& ch, bool seqIsRunning) const +void SampleAdvancer::onLastFrame(const Channel& ch, bool seqIsRunning, bool natural) const { const SamplePlayerMode mode = ch.samplePlayer->mode; const bool isLoop = ch.samplePlayer->isAnyLoopMode(); @@ -45,14 +45,14 @@ void SampleAdvancer::onLastFrame(const Channel& ch, bool seqIsRunning) const mode == SamplePlayerMode::SINGLE_BASIC_PAUSE || mode == SamplePlayerMode::SINGLE_PRESS || mode == SamplePlayerMode::SINGLE_RETRIG) || - (isLoop && !seqIsRunning)) - stop(ch, 0); + (isLoop && !seqIsRunning) || !natural) + ch.shared->playStatus.store(ChannelStatus::OFF); else if (mode == SamplePlayerMode::LOOP_ONCE || mode == SamplePlayerMode::LOOP_ONCE_BAR) - wait(ch); + ch.shared->playStatus.store(ChannelStatus::WAIT); break; case ChannelStatus::ENDING: - stop(ch, 0); + ch.shared->playStatus.store(ChannelStatus::OFF); break; default: @@ -75,7 +75,8 @@ void SampleAdvancer::advance(const Channel& ch, const Sequencer::Event& e) const break; case Sequencer::EventType::REWIND: - rewind(ch, e.delta); + if (ch.samplePlayer->isAnyLoopMode()) + rewind(ch, e.delta); break; case Sequencer::EventType::ACTIONS: @@ -92,22 +93,14 @@ void SampleAdvancer::advance(const Channel& ch, const Sequencer::Event& e) const void SampleAdvancer::rewind(const Channel& ch, Frame localFrame) const { - ch.shared->rewinding = true; - ch.shared->offset = localFrame; + ch.shared->renderQueue->push({SamplePlayer::Render::Mode::REWIND, localFrame}); } /* -------------------------------------------------------------------------- */ void SampleAdvancer::stop(const Channel& ch, Frame localFrame) const { - ch.shared->playStatus.store(ChannelStatus::OFF); - ch.shared->tracker.store(ch.samplePlayer->begin); - - /* Clear data in range [localFrame, (buffer.size)) if the event occurs in - the middle of the buffer. TODO - samplePlayer should be responsible for this*/ - - if (localFrame != 0) - ch.shared->audioBuffer.clear(localFrame); + ch.shared->renderQueue->push({SamplePlayer::Render::Mode::STOP, localFrame}); } /* -------------------------------------------------------------------------- */ @@ -115,15 +108,7 @@ void SampleAdvancer::stop(const Channel& ch, Frame localFrame) const void SampleAdvancer::play(const Channel& ch, Frame localFrame) const { ch.shared->playStatus.store(ChannelStatus::PLAY); - ch.shared->offset = localFrame; -} - -/* -------------------------------------------------------------------------- */ - -void SampleAdvancer::wait(const Channel& ch) const -{ - ch.shared->playStatus.store(ChannelStatus::WAIT); - ch.shared->tracker.store(ch.samplePlayer->begin); + ch.shared->renderQueue->push({SamplePlayer::Render::Mode::NORMAL, localFrame}); } /* -------------------------------------------------------------------------- */ @@ -232,7 +217,8 @@ void SampleAdvancer::parseActions(const Channel& ch, case MidiEvent::NOTE_OFF: case MidiEvent::NOTE_KILL: - stop(ch, localFrame); + if (ch.shared->playStatus.load() == ChannelStatus::PLAY) + stop(ch, localFrame); break; default: diff --git a/src/core/channels/sampleAdvancer.h b/src/core/channels/sampleAdvancer.h index bf5a8d4..e3246c0 100644 --- a/src/core/channels/sampleAdvancer.h +++ b/src/core/channels/sampleAdvancer.h @@ -35,14 +35,13 @@ class Channel; class SampleAdvancer final { public: - void onLastFrame(const Channel& ch, bool seqIsRunning) const; + void onLastFrame(const Channel& ch, bool seqIsRunning, bool natural) const; void advance(const Channel& ch, const Sequencer::Event& e) const; private: void rewind(const Channel& ch, Frame localFrame) const; void stop(const Channel& ch, Frame localFrame) const; void play(const Channel& ch, Frame localFrame) const; - void wait(const Channel& ch) const; void onFirstBeat(const Channel& ch, Frame localFrame) const; void onBar(const Channel& ch, Frame localFrame) const; void onNoteOn(const Channel& ch, Frame localFrame) const; diff --git a/src/core/channels/samplePlayer.cpp b/src/core/channels/samplePlayer.cpp index 350d341..108a8ef 100644 --- a/src/core/channels/samplePlayer.cpp +++ b/src/core/channels/samplePlayer.cpp @@ -31,6 +31,8 @@ #include #include +using namespace mcl; + namespace giada::m { SamplePlayer::SamplePlayer(Resampler* r) @@ -106,35 +108,54 @@ void SamplePlayer::react(const EventDispatcher::Event& e) /* -------------------------------------------------------------------------- */ -void SamplePlayer::render(const Channel& ch) const +void SamplePlayer::render(ChannelShared& shared, Render renderInfo) const { - if (!isPlaying(ch)) + if (waveReader.wave == nullptr) return; - /* Make sure tracker stays within begin-end range. */ + AudioBuffer& buf = shared.audioBuffer; + Frame tracker = std::clamp(shared.tracker.load(), begin, end); /* Make sure tracker stays within begin-end range. */ + const ChannelStatus status = shared.playStatus.load(); - Frame tracker = std::clamp(ch.shared->tracker.load(), begin, end); + if (renderInfo.mode == Render::Mode::NORMAL) + { + tracker = render(buf, tracker, renderInfo.offset, status); + } + else + { + /* Both modes: 1st = [abcdefghijklmnopq] + No need for fancy render() here. You don't want the chance to trigger + onLastFrame() at this point which would invalidate the rewind (a listener + might stop the rendering): fillBuffer() is just enough. Just notify + waveReader this is the last read before rewind. */ - /* If rewinding, fill the tail first, then reset the tracker to the begin - point. The rest is performed as usual. */ + tracker = fillBuffer(buf, tracker, 0).used; + waveReader.last(); - if (ch.shared->rewinding) - { - if (tracker < end) - { - fillBuffer(ch, tracker, 0); - waveReader.last(); - } - ch.shared->rewinding = false; - tracker = begin; + /* Mode::REWIND: 2nd = [abcdefghi|abcdfefg] + Mode::STOP: 2nd = [abcdefghi|--------] */ + + if (renderInfo.mode == Render::Mode::REWIND) + tracker = render(buf, begin, renderInfo.offset, status); + else + tracker = stop(buf, renderInfo.offset); } - WaveReader::Result res = fillBuffer(ch, tracker, ch.shared->offset); + shared.tracker.store(tracker); +} + +/* -------------------------------------------------------------------------- */ + +Frame SamplePlayer::render(AudioBuffer& buf, Frame tracker, Frame offset, ChannelStatus status) const +{ + /* First pass rendering. */ + + WaveReader::Result res = fillBuffer(buf, tracker, offset); tracker += res.used; - /* If tracker has looped, special care is needed for the rendering. If the - channel is in loop mode, fill the second part of the buffer with data - coming from the sample's head. */ + /* Second pass rendering: if tracker has looped, special care is needed. If + the channel is in loop mode, fill the second part of the buffer with data + coming from the sample's head, starting at 'res.generated' offset. */ if (tracker >= end) { @@ -142,37 +163,40 @@ void SamplePlayer::render(const Channel& ch) const tracker = begin; waveReader.last(); - onLastFrame(); - if (shouldLoop(ch) && res.generated < ch.shared->audioBuffer.countFrames()) - tracker += fillBuffer(ch, tracker, res.generated).used; + onLastFrame(/*natural=*/true); + + if (shouldLoop(status) && res.generated < buf.countFrames()) + tracker += fillBuffer(buf, tracker, res.generated).used; } - ch.shared->offset = 0; - ch.shared->tracker.store(tracker); + return tracker; } /* -------------------------------------------------------------------------- */ -void SamplePlayer::loadWave(Channel& ch, Wave* w) +Frame SamplePlayer::stop(AudioBuffer& buf, Frame offset) const +{ + assert(onLastFrame != nullptr); + + onLastFrame(/*natural=*/false); + + if (offset != 0) + buf.clear(offset); + + return begin; +} + +/* -------------------------------------------------------------------------- */ + +void SamplePlayer::loadWave(ChannelShared& shared, Wave* w) { waveReader.wave = w; - ch.shared->tracker.store(0); + shared.tracker.store(0); + shared.playStatus.store(w != nullptr ? ChannelStatus::OFF : ChannelStatus::EMPTY); shift = 0; begin = 0; - - if (w != nullptr) - { - ch.shared->playStatus.store(ChannelStatus::OFF); - ch.name = w->getBasename(/*ext=*/false); - end = w->getBuffer().countFrames() - 1; - } - else - { - ch.shared->playStatus.store(ChannelStatus::EMPTY); - ch.name = ""; - end = 0; - } + end = w != nullptr ? w->getBuffer().countFrames() - 1 : 0; } /* -------------------------------------------------------------------------- */ @@ -197,35 +221,26 @@ void SamplePlayer::setWave(Wave* w, float samplerateRatio) /* -------------------------------------------------------------------------- */ -void SamplePlayer::kickIn(Channel& ch, Frame f) -{ - ch.shared->tracker.store(f); - ch.shared->playStatus.store(ChannelStatus::PLAY); -} - -/* -------------------------------------------------------------------------- */ - -bool SamplePlayer::isPlaying(const Channel& ch) const +void SamplePlayer::kickIn(ChannelShared& shared, Frame f) { - return waveReader.wave != nullptr && ch.isPlaying(); + shared.tracker.store(f); + shared.playStatus.store(ChannelStatus::PLAY); } /* -------------------------------------------------------------------------- */ -WaveReader::Result SamplePlayer::fillBuffer(const Channel& ch, Frame start, Frame offset) const +WaveReader::Result SamplePlayer::fillBuffer(AudioBuffer& buf, Frame start, Frame offset) const { - return waveReader.fill(ch.shared->audioBuffer, start, end, offset, pitch); + return waveReader.fill(buf, start, end, offset, pitch); } /* -------------------------------------------------------------------------- */ -bool SamplePlayer::shouldLoop(const Channel& ch) const +bool SamplePlayer::shouldLoop(ChannelStatus status) const { - const ChannelStatus playStatus = ch.shared->playStatus.load(); - return (mode == SamplePlayerMode::LOOP_BASIC || mode == SamplePlayerMode::LOOP_REPEAT || mode == SamplePlayerMode::SINGLE_ENDLESS) && - playStatus == ChannelStatus::PLAY; + status == ChannelStatus::PLAY; // Don't loop if ENDING } } // namespace giada::m \ No newline at end of file diff --git a/src/core/channels/samplePlayer.h b/src/core/channels/samplePlayer.h index d9bcbcf..2e18b17 100644 --- a/src/core/channels/samplePlayer.h +++ b/src/core/channels/samplePlayer.h @@ -37,10 +37,32 @@ namespace giada::m { +struct ChannelShared; class Channel; class SamplePlayer final { public: + /* Render + Determines how the render() function should behave. + Mode::NORMAL - normal rendering, starting at offset 'offset'; + Mode::REWIND - two-step rendering, used when the sample must rewind at some + point ('offset') in the audio buffer; + Mode::STOP - abort rendering. The audio buffer is silenced starting at + 'offset'. Also triggers onLastFrame(). */ + + struct Render + { + enum class Mode + { + NORMAL, + REWIND, + STOP + }; + + Mode mode = Mode::NORMAL; + Frame offset = 0; + }; + SamplePlayer(Resampler* r); SamplePlayer(const Patch::Channel& p, float samplerateRatio, Resampler* r, Wave* w); @@ -51,14 +73,15 @@ public: ID getWaveId() const; Frame getWaveSize() const; Wave* getWave() const; - void render(const Channel& ch) const; + void render(ChannelShared&, Render) const; void react(const EventDispatcher::Event& e); /* loadWave - Loads Wave 'w' into channel ch and sets it up (name, markers, ...). */ + Loads Wave and sets it up (name, markers, ...). Also updates Channel's shared + state accordingly. */ - void loadWave(Channel& ch, Wave* w); + void loadWave(ChannelShared&, Wave*); /* setWave Just sets the pointer to a Wave object. Used during de-serialization. The @@ -71,7 +94,7 @@ public: Starts the player right away at frame 'f'. Used when launching a loop after being live recorded. */ - void kickIn(Channel& ch, Frame f); + void kickIn(ChannelShared&, Frame f); float pitch; SamplePlayerMode mode; @@ -81,12 +104,30 @@ public: bool velocityAsVol; // Velocity drives volume WaveReader waveReader; - std::function onLastFrame; + /* onLastFrame + Callback fired when the last frame has been reached. 'natural' == true + if the rendering has ended because the end of the sample has ben reached. + 'natural' == false if the rendering has been manually interrupted (by + a Render::Mode::STOP type). */ + + std::function onLastFrame; private: - bool isPlaying(const Channel& ch) const; - WaveReader::Result fillBuffer(const Channel& ch, Frame start, Frame offset) const; - bool shouldLoop(const Channel& ch) const; + /* render + Renders audio into the buffer. Reads audio data from 'tracker' and copies it + into the audio buffer at position 'offset'. May fire 'onLastFrame' callback + if the sample end is reached. */ + + Frame render(mcl::AudioBuffer&, Frame tracker, Frame offset, ChannelStatus) const; + + /* stop + Silences the last part of the audio buffer, starting at 'offset'. Used to + terminate rendering. It also fire the 'onLastFrame' callback. */ + + Frame stop(mcl::AudioBuffer&, Frame offset) const; + + WaveReader::Result fillBuffer(mcl::AudioBuffer&, Frame start, Frame offset) const; + bool shouldLoop(ChannelStatus) const; }; } // namespace giada::m diff --git a/src/core/channels/sampleReactor.cpp b/src/core/channels/sampleReactor.cpp index 1358a5f..b009d7b 100644 --- a/src/core/channels/sampleReactor.cpp +++ b/src/core/channels/sampleReactor.cpp @@ -36,24 +36,25 @@ namespace giada::m namespace { constexpr int Q_ACTION_PLAY = 0; -constexpr int Q_ACTION_REWIND = 1; +constexpr int Q_ACTION_REWIND = 10000; // Avoid clash with Q_ACTION_PLAY + channelId } // namespace /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ -SampleReactor::SampleReactor(ID channelId, Sequencer& sequencer, model::Model& model) +SampleReactor::SampleReactor(Channel& ch, ID channelId) { - sequencer.quantizer.schedule(Q_ACTION_PLAY + channelId, [channelId, &model](Frame delta) { - Channel& ch = model.get().getChannel(channelId); - ch.shared->offset = delta; - ch.shared->playStatus.store(ChannelStatus::PLAY); + ch.shared->quantizer->schedule(Q_ACTION_PLAY + channelId, [this, shared = ch.shared](Frame delta) { + play(*shared, delta); }); - sequencer.quantizer.schedule(Q_ACTION_REWIND + channelId, [this, channelId, &model](Frame delta) { - Channel& ch = model.get().getChannel(channelId); - ch.isPlaying() ? rewind(ch, delta) : reset(ch); + ch.shared->quantizer->schedule(Q_ACTION_REWIND + channelId, [this, shared = ch.shared](Frame delta) { + ChannelStatus status = shared->playStatus.load(); + if (status == ChannelStatus::OFF) + play(*shared, delta); + else if (status == ChannelStatus::PLAY || status == ChannelStatus::ENDING) + rewind(*shared, delta); }); } @@ -72,21 +73,18 @@ void SampleReactor::react(Channel& ch, const EventDispatcher::Event& e, break; case EventDispatcher::EventType::KEY_RELEASE: - release(ch, sequencer); + release(ch); break; case EventDispatcher::EventType::KEY_KILL: - kill(ch); + if (ch.shared->playStatus.load() == ChannelStatus::PLAY) + stop(*ch.shared); break; case EventDispatcher::EventType::SEQUENCER_STOP: onStopBySeq(ch, conf.chansStopOnSeqHalt); break; - case EventDispatcher::EventType::CHANNEL_TOGGLE_READ_ACTIONS: - toggleReadActions(ch, sequencer.isRunning(), conf.treatRecsAsLoops); - break; - default: break; } @@ -94,17 +92,24 @@ void SampleReactor::react(Channel& ch, const EventDispatcher::Event& e, /* -------------------------------------------------------------------------- */ -void SampleReactor::rewind(Channel& ch, Frame localFrame) const +void SampleReactor::rewind(ChannelShared& shared, Frame localFrame) const { - ch.shared->rewinding = true; - ch.shared->offset = localFrame; + shared.renderQueue->push({SamplePlayer::Render::Mode::REWIND, localFrame}); } /* -------------------------------------------------------------------------- */ -void SampleReactor::reset(Channel& ch) const +void SampleReactor::play(ChannelShared& shared, Frame localFrame) const { - ch.shared->tracker.store(ch.samplePlayer->begin); + shared.playStatus.store(ChannelStatus::PLAY); + shared.renderQueue->push({SamplePlayer::Render::Mode::NORMAL, localFrame}); +} + +/* -------------------------------------------------------------------------- */ + +void SampleReactor::stop(ChannelShared& shared) const +{ + shared.renderQueue->push({SamplePlayer::Render::Mode::STOP, 0}); } /* -------------------------------------------------------------------------- */ @@ -120,7 +125,7 @@ ChannelStatus SampleReactor::pressWhileOff(Channel& ch, Sequencer& sequencer, if (sequencer.canQuantize()) { - sequencer.quantizer.trigger(Q_ACTION_PLAY + ch.id); + ch.shared->quantizer->trigger(Q_ACTION_PLAY + ch.id); return ChannelStatus::OFF; } else @@ -139,17 +144,17 @@ ChannelStatus SampleReactor::pressWhilePlay(Channel& ch, Sequencer& sequencer, { case SamplePlayerMode::SINGLE_RETRIG: if (sequencer.canQuantize()) - sequencer.quantizer.trigger(Q_ACTION_REWIND + ch.id); + ch.shared->quantizer->trigger(Q_ACTION_REWIND + ch.id); else - rewind(ch, /*localFrame=*/0); + rewind(*ch.shared, /*localFrame=*/0); return ChannelStatus::PLAY; case SamplePlayerMode::SINGLE_ENDLESS: return ChannelStatus::ENDING; case SamplePlayerMode::SINGLE_BASIC: - reset(ch); - return ChannelStatus::OFF; + stop(*ch.shared); + return ChannelStatus::PLAY; // Let SamplePlayer stop it once done default: return ChannelStatus::OFF; @@ -192,14 +197,7 @@ void SampleReactor::press(Channel& ch, Sequencer& sequencer, int velocity) const /* -------------------------------------------------------------------------- */ -void SampleReactor::kill(Channel& ch) const -{ - ch.shared->playStatus.store(ChannelStatus::OFF); - ch.shared->tracker.store(ch.samplePlayer->begin); -} -/* -------------------------------------------------------------------------- */ - -void SampleReactor::release(Channel& ch, Sequencer& sequencer) const +void SampleReactor::release(Channel& ch) const { /* Key release is meaningful only for SINGLE_PRESS modes. */ @@ -211,9 +209,9 @@ void SampleReactor::release(Channel& ch, Sequencer& sequencer) const disable it. */ if (ch.shared->playStatus.load() == ChannelStatus::PLAY) - kill(ch); - else if (sequencer.quantizer.hasBeenTriggered()) - sequencer.quantizer.clear(); + stop(*ch.shared); // Let SamplePlayer stop it once done + else if (ch.shared->quantizer->hasBeenTriggered()) + ch.shared->quantizer->clear(); } /* -------------------------------------------------------------------------- */ @@ -237,19 +235,11 @@ void SampleReactor::onStopBySeq(Channel& ch, bool chansStopOnSeqHalt) const case ChannelStatus::PLAY: if (chansStopOnSeqHalt && (isLoop || isReadingActions)) - kill(ch); + stop(*ch.shared); break; default: break; } } - -/* -------------------------------------------------------------------------- */ - -void SampleReactor::toggleReadActions(Channel& ch, bool isSequencerRunning, bool treatRecsAsLoops) const -{ - if (isSequencerRunning && ch.shared->recStatus.load() == ChannelStatus::PLAY && !treatRecsAsLoops) - kill(ch); -} } // namespace giada::m \ No newline at end of file diff --git a/src/core/channels/sampleReactor.h b/src/core/channels/sampleReactor.h index cfb725c..8761f2e 100644 --- a/src/core/channels/sampleReactor.h +++ b/src/core/channels/sampleReactor.h @@ -39,6 +39,7 @@ class Model; namespace giada::m { class Channel; +class ChannelShared; class Sequencer; /* SampleReactor @@ -48,20 +49,25 @@ sequencer stop, ... . */ class SampleReactor final { public: - SampleReactor(ID channelId, Sequencer&, model::Model&); + struct Event + { + int type; + Frame offset; + }; + + SampleReactor(Channel&, ID channelId); void react(Channel&, const EventDispatcher::Event&, Sequencer&, const Conf::Data&) const; private: - void toggleReadActions(Channel&, bool isSequencerRunning, bool treatRecsAsLoops) const; void onStopBySeq(Channel&, bool chansStopOnSeqHalt) const; - void release(Channel&, Sequencer&) const; - void kill(Channel&) const; + void release(Channel&) const; void press(Channel&, Sequencer&, int velocity) const; ChannelStatus pressWhilePlay(Channel&, Sequencer&, SamplePlayerMode, bool isLoop) const; ChannelStatus pressWhileOff(Channel&, Sequencer&, int velocity, bool isLoop) const; - void reset(Channel&) const; - void rewind(Channel&, Frame localFrame) const; + void rewind(ChannelShared&, Frame localFrame) const; + void play(ChannelShared&, Frame localFrame) const; + void stop(ChannelShared&) const; }; } // namespace giada::m diff --git a/src/core/const.h b/src/core/const.h index 35eccbb..a2e7356 100644 --- a/src/core/const.h +++ b/src/core/const.h @@ -59,10 +59,10 @@ /* -- version --------------------------------------------------------------- */ constexpr auto G_APP_NAME = "Giada"; -constexpr auto G_VERSION_STR = "0.19.1"; +constexpr auto G_VERSION_STR = "0.20.0"; constexpr int G_VERSION_MAJOR = 0; -constexpr int G_VERSION_MINOR = 19; -constexpr int G_VERSION_PATCH = 1; +constexpr int G_VERSION_MINOR = 20; +constexpr int G_VERSION_PATCH = 0; constexpr auto CONF_FILENAME = "giada.conf"; @@ -116,18 +116,13 @@ constexpr float G_MIN_PITCH = 0.1f; constexpr float G_MAX_PITCH = 4.0f; constexpr float G_MAX_PAN = 1.0f; constexpr float G_MAX_VOLUME = 1.0f; -constexpr int G_MAX_GRID_VAL = 64; -constexpr int G_MIN_BUF_SIZE = 8; -constexpr int G_MAX_BUF_SIZE = 4096; constexpr int G_MIN_GUI_WIDTH = 816; constexpr int G_MIN_GUI_HEIGHT = 510; constexpr int G_MAX_IO_CHANS = 2; constexpr int G_MAX_VELOCITY = 0x7F; constexpr int G_MAX_MIDI_CHANS = 16; -constexpr int G_MAX_POLYPHONY = 32; constexpr int G_MAX_DISPATCHER_EVENTS = 32; constexpr int G_MAX_SEQUENCER_EVENTS = 128; // Per block -constexpr int G_MAX_QUANTIZER_SIZE = 32; /* -- kernel audio ---------------------------------------------------------- */ constexpr int G_SYS_API_NONE = 0; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index a71c863..3139443 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -67,7 +67,7 @@ Engine::Engine() eventDispatcher.onProcessChannels = [this](const EventDispatcher::EventBuffer& eb) { for (Channel& ch : model.get().channels) ch.react(eb); - model.swap(model::SwapType::SOFT); // TODO - is this necessary??? + model.swap(model::SwapType::SOFT); }; eventDispatcher.onProcessSequencer = [this](const EventDispatcher::EventBuffer& eb) { sequencer.react(eb); @@ -194,17 +194,24 @@ void Engine::init() void Engine::reset() { + /* Managers first, due to the internal ID numbering. */ + + channelManager.reset(); + waveManager.reset(); +#ifdef WITH_VST + pluginManager.reset(static_cast(conf.data.pluginSortMethod)); +#endif + + /* Then all other components. */ + model.reset(); mixerHandler.reset(sequencer.getMaxFramesInLoop(kernelAudio.getSampleRate()), kernelAudio.getBufferSize(), channelManager); - channelManager.reset(); - waveManager.reset(); synchronizer.reset(); sequencer.reset(kernelAudio.getSampleRate()); actionRecorder.reset(); #ifdef WITH_VST pluginHost.reset(kernelAudio.getBufferSize()); - pluginManager.reset(static_cast(conf.data.pluginSortMethod)); #endif } @@ -280,10 +287,15 @@ int Engine::audioCallback(KernelAudio::CallbackInfo kernelInfo) if (layout_RT.sequencer.isRunning()) { - const Sequencer::EventBuffer& events = sequencer.advance(in.countFrames(), actionRecorder); + const Frame currentFrame = sequencer.getCurrentFrame(); + const Frame bufferSize = in.countFrames(); + const Frame quantizerStep = sequencer.getQuantizerStep(); // TODO pass this to sequencer.advance - or better, Advancer class + const Range renderRange = {currentFrame, currentFrame + bufferSize}; // TODO pass this to sequencer.advance - or better, Advancer class + + const Sequencer::EventBuffer& events = sequencer.advance(bufferSize, actionRecorder); sequencer.render(out); if (!layout_RT.locked) - mixer.advanceChannels(events, layout_RT); + mixer.advanceChannels(events, layout_RT, renderRange, quantizerStep); } /* Then render Mixer: render channels, process I/O. */ @@ -296,8 +308,10 @@ int Engine::audioCallback(KernelAudio::CallbackInfo kernelInfo) /* -------------------------------------------------------------------------- */ bool Engine::store(const std::string& projectName, const std::string& projectPath, - const std::string& patchPath) + const std::string& patchPath, std::function progress) { + progress(0.0f); + if (!u::fs::mkdir(projectPath)) { u::log::print("[Engine::store] Unable to make project directory!\n"); @@ -315,11 +329,15 @@ bool Engine::store(const std::string& projectName, const std::string& projectPat waveManager.save(*w, w->getPath()); // TODO - error checking } + progress(0.3f); + /* Write Model into Patch, then into file. */ patch.data.name = projectName; model::store(patch.data); + progress(0.6f); + if (!patch.write(patchPath)) return false; @@ -330,25 +348,34 @@ bool Engine::store(const std::string& projectName, const std::string& projectPat u::log::print("[Engine::store] Project patch saved as %s\n", patchPath); + progress(1.0f); + return true; } /* -------------------------------------------------------------------------- */ -int Engine::load(const std::string& projectPath, const std::string& patchPath) +int Engine::load(const std::string& projectPath, const std::string& patchPath, + std::function progress) { u::log::print("[Engine::load] Load project from %s\n", projectPath); + progress(0.0f); + patch.reset(); if (int res = patch.read(patchPath, projectPath); res != G_PATCH_OK) return res; + progress(0.3f); + /* Then suspend Mixer, reset and fill the model. */ mixer.disable(); reset(); m::model::load(patch.data); + progress(0.6f); + /* Prepare the engine. Recorder has to recompute the actions positions if the current samplerate != patch samplerate. Clock needs to update frames in sequencer. */ @@ -358,6 +385,8 @@ int Engine::load(const std::string& projectPath, const std::string& patchPath) sequencer.recomputeFrames(kernelAudio.getSampleRate()); mixer.allocRecBuffer(sequencer.getMaxFramesInLoop(kernelAudio.getSampleRate())); + progress(0.9f); + /* Store the parent folder the project belongs to, in order to reuse it the next time. */ @@ -367,6 +396,8 @@ int Engine::load(const std::string& projectPath, const std::string& patchPath) mixer.enable(); + progress(1.0f); + return G_PATCH_OK; } diff --git a/src/core/engine.h b/src/core/engine.h index b081162..5fa6ae5 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -65,13 +65,14 @@ public: on success. */ bool store(const std::string& projectName, const std::string& projectPath, - const std::string& patchPath); + const std::string& patchPath, std::function progress); /* load Reads a Patch from file and then de-serialize its content into the model. Returns G_PATCH_OK on success or any G_PATCH_* on failure. */ - int load(const std::string& projectPath, const std::string& patchPath); + int load(const std::string& projectPath, const std::string& patchPath, + std::function progress); /* updateMixerModel Updates some values in model::Mixer data struct needed by m::Mixer for the diff --git a/src/core/init.cpp b/src/core/init.cpp index 9e1504e..bd4b98f 100644 --- a/src/core/init.cpp +++ b/src/core/init.cpp @@ -37,10 +37,12 @@ #define CATCH_CONFIG_RUNNER #include "tests/actionRecorder.cpp" #include "tests/midiLighter.cpp" +#include "tests/samplePlayer.cpp" #include "tests/utils.cpp" #include "tests/wave.cpp" #include "tests/waveFx.cpp" #include "tests/waveManager.cpp" +#include "tests/waveReader.cpp" #include #include #include diff --git a/src/core/kernelMidi.cpp b/src/core/kernelMidi.cpp index 254cee8..fd24587 100644 --- a/src/core/kernelMidi.cpp +++ b/src/core/kernelMidi.cpp @@ -70,8 +70,13 @@ bool KernelMidi::openOutDevice(int api, int port) if (port == -1) return false; - m_midiOut = makeDevice(api, port, OUTPUT_NAME); - return m_midiOut != nullptr; + u::log::print("[KM] Opening output device '%s', port=%d\n", OUTPUT_NAME, port); + + m_midiOut = makeDevice(api, OUTPUT_NAME); + if (m_midiOut == nullptr) + return false; + + return openPort(*m_midiOut, port); } /* -------------------------------------------------------------------------- */ @@ -81,10 +86,15 @@ bool KernelMidi::openInDevice(int api, int port) if (port == -1) return false; - m_midiIn = makeDevice(api, port, INPUT_NAME); + u::log::print("[KM] Opening input device '%s', port=%d\n", INPUT_NAME, port); + + m_midiIn = makeDevice(api, INPUT_NAME); if (m_midiIn == nullptr) return false; + if (!openPort(*m_midiIn, port)) + return false; + m_midiIn->setCallback(&s_callback, this); m_midiIn->ignoreTypes(true, false, true); // Ignore all system/time msgs, for now @@ -179,23 +189,37 @@ void KernelMidi::callback(std::vector* msg) /* -------------------------------------------------------------------------- */ template -std::unique_ptr KernelMidi::makeDevice(int api, int port, std::string name) const +std::unique_ptr KernelMidi::makeDevice(int api, std::string name) const { try { - auto device = std::make_unique(static_cast(api), name); - device->openPort(port, device->getPortName(port)); - return device; + return std::make_unique(static_cast(api), name); } catch (RtMidiError& error) { - u::log::print("[KM] Device '%s' error on open: %s\n", name.c_str(), error.getMessage()); + u::log::print("[KM] Error opening device '%s': %s\n", name.c_str(), error.getMessage()); return nullptr; } } -template std::unique_ptr KernelMidi::makeDevice(int, int, std::string) const; -template std::unique_ptr KernelMidi::makeDevice(int, int, std::string) const; +template std::unique_ptr KernelMidi::makeDevice(int, std::string) const; +template std::unique_ptr KernelMidi::makeDevice(int, std::string) const; + +/* -------------------------------------------------------------------------- */ + +bool KernelMidi::openPort(RtMidi& device, int port) +{ + try + { + device.openPort(port, device.getPortName(port)); + return true; + } + catch (RtMidiError& error) + { + u::log::print("[KM] Error opening port %d: %s\n", port, error.getMessage()); + return false; + } +} /* -------------------------------------------------------------------------- */ diff --git a/src/core/kernelMidi.h b/src/core/kernelMidi.h index 5489a22..88f98ed 100644 --- a/src/core/kernelMidi.h +++ b/src/core/kernelMidi.h @@ -71,15 +71,17 @@ public: std::function onMidiReceived; private: - template - std::unique_ptr makeDevice(int api, int port, std::string name) const; - static void s_callback(double, std::vector*, void*); void callback(std::vector*); + template + std::unique_ptr makeDevice(int api, std::string name) const; + std::string getPortName(RtMidi&, int port) const; void logPorts(RtMidi&, std::string name) const; + bool openPort(RtMidi&, int port); + std::unique_ptr m_midiOut; std::unique_ptr m_midiIn; }; diff --git a/src/core/mixer.cpp b/src/core/mixer.cpp index 8c5f81d..7b8cd54 100644 --- a/src/core/mixer.cpp +++ b/src/core/mixer.cpp @@ -107,11 +107,12 @@ const mcl::AudioBuffer& Mixer::getRecBuffer() /* -------------------------------------------------------------------------- */ -void Mixer::advanceChannels(const Sequencer::EventBuffer& events, const model::Layout& rtLayout) +void Mixer::advanceChannels(const Sequencer::EventBuffer& events, + const model::Layout& rtLayout, Range block, Frame quantizerStep) { for (const Channel& c : rtLayout.channels) if (!c.isInternal()) - c.advance(events); + c.advance(events, block, quantizerStep); } /* -------------------------------------------------------------------------- */ diff --git a/src/core/mixer.h b/src/core/mixer.h index 204845c..6ed4635 100644 --- a/src/core/mixer.h +++ b/src/core/mixer.h @@ -129,7 +129,8 @@ public: events) in the current audio block. Called by the main audio thread when the sequencer is running. */ - void advanceChannels(const Sequencer::EventBuffer& events, const model::Layout&); + void advanceChannels(const Sequencer::EventBuffer&, const model::Layout&, + Range, Frame quantizerStep); /* onSignalTresholdReached Callback fired when audio has reached a certain threshold (record-on-signal diff --git a/src/core/mixerHandler.cpp b/src/core/mixerHandler.cpp index 31cf8ae..d636775 100644 --- a/src/core/mixerHandler.cpp +++ b/src/core/mixerHandler.cpp @@ -92,7 +92,7 @@ void MixerHandler::loadChannel(ID channelId, std::unique_ptr w) Wave& wave = m_model.backShared(); Wave* old = channel.samplePlayer->getWave(); - channel.samplePlayer->loadWave(channel, &wave); + loadChannel(channel, &wave); m_model.swap(model::SwapType::HARD); /* Remove old wave, if any. It is safe to do it now: the audio thread is @@ -116,7 +116,7 @@ void MixerHandler::addAndLoadChannel(ID columnId, std::unique_ptr w, int b Wave& wave = m_model.backShared(); Channel& channel = addChannel(ChannelType::SAMPLE, columnId, bufferSize, channelManager); - channel.samplePlayer->loadWave(channel, &wave); + loadChannel(channel, &wave); m_model.swap(model::SwapType::HARD); onChannelsAltered(); @@ -142,7 +142,7 @@ void MixerHandler::cloneChannel(ID channelId, int bufferSize, ChannelManager& ch { const Wave& oldWave = *oldChannel.samplePlayer->getWave(); m_model.addShared(waveManager.createFromWave(oldWave, 0, oldWave.getBuffer().countFrames())); - newChannel.samplePlayer->loadWave(newChannel, &m_model.backShared()); + loadChannel(newChannel, &m_model.backShared()); } #ifdef WITH_VST @@ -171,7 +171,7 @@ void MixerHandler::freeChannel(ID channelId) const Wave* wave = ch.samplePlayer->getWave(); - ch.samplePlayer->loadWave(ch, nullptr); + loadChannel(ch, nullptr); m_model.swap(model::SwapType::HARD); if (wave != nullptr) @@ -188,7 +188,7 @@ void MixerHandler::freeAllChannels() for (Channel& ch : m_model.get().channels) if (ch.samplePlayer) - ch.samplePlayer->loadWave(ch, nullptr); + loadChannel(ch, nullptr); m_model.swap(model::SwapType::HARD); m_model.clearShared(); @@ -216,6 +216,7 @@ void MixerHandler::deleteChannel(ID channelId) if (wave != nullptr) m_model.removeShared(*wave); + updateSoloCount(); onChannelsAltered(); } @@ -337,6 +338,14 @@ bool MixerHandler::forAnyChannel(std::function f) const /* -------------------------------------------------------------------------- */ +void MixerHandler::loadChannel(Channel& ch, Wave* w) const +{ + ch.samplePlayer->loadWave(*ch.shared, w); + ch.name = w != nullptr ? w->getBasename(/*ext=*/false) : ""; +} + +/* -------------------------------------------------------------------------- */ + std::vector MixerHandler::getChannelsIf(std::function f) { std::vector out; @@ -362,7 +371,7 @@ void MixerHandler::setupChannelPostRecording(Channel& ch, Frame currentFrame) { /* Start sample channels in loop mode right away. */ if (ch.samplePlayer->isAnyLoopMode()) - ch.samplePlayer->kickIn(ch, currentFrame); + ch.samplePlayer->kickIn(*ch.shared, currentFrame); /* Disable 'arm' button if overdub protection is on. */ if (ch.audioReceiver->overdubProtection == true) ch.armed = false; @@ -385,7 +394,7 @@ void MixerHandler::recordChannel(Channel& ch, Frame recordedFrames, Frame curren /* Update channel with the new Wave. */ m_model.addShared(std::move(wave)); - ch.samplePlayer->loadWave(ch, &m_model.backShared()); + loadChannel(ch, &m_model.backShared()); setupChannelPostRecording(ch, currentFrame); m_model.swap(model::SwapType::HARD); diff --git a/src/core/mixerHandler.h b/src/core/mixerHandler.h index 92a9de5..b951356 100644 --- a/src/core/mixerHandler.h +++ b/src/core/mixerHandler.h @@ -163,6 +163,8 @@ public: private: bool forAnyChannel(std::function f) const; + void loadChannel(Channel&, Wave*) const; + std::vector getChannelsIf(std::function f); std::vector getRecordableChannels(); std::vector getOverdubbableChannels(); diff --git a/src/core/model/model.cpp b/src/core/model/model.cpp index 3d203bb..75a30e0 100644 --- a/src/core/model/model.cpp +++ b/src/core/model/model.cpp @@ -246,15 +246,15 @@ T& Model::backShared() #endif if constexpr (std::is_same_v) return *m_shared.waves.back().get(); - if constexpr (std::is_same_v) + if constexpr (std::is_same_v) return *m_shared.channelsShared.back().get(); } #ifdef WITH_VST template Plugin& Model::backShared(); #endif -template Wave& Model::backShared(); -template Channel::Shared& Model::backShared(); +template Wave& Model::backShared(); +template ChannelShared& Model::backShared(); /* -------------------------------------------------------------------------- */ diff --git a/src/core/model/model.h b/src/core/model/model.h index 6b7fe33..12e0f3a 100644 --- a/src/core/model/model.h +++ b/src/core/model/model.h @@ -100,7 +100,7 @@ enum class SwapType using PluginPtr = std::unique_ptr; #endif using WavePtr = std::unique_ptr; -using ChannelSharedPtr = std::unique_ptr; +using ChannelSharedPtr = std::unique_ptr; #ifdef WITH_VST using PluginPtrs = std::vector; @@ -187,10 +187,10 @@ public: private: struct Shared { - Sequencer::Shared sequencerShared; - Mixer::Shared mixerShared; - Recorder::Shared recorderShared; - std::vector> channelsShared; + Sequencer::Shared sequencerShared; + Mixer::Shared mixerShared; + Recorder::Shared recorderShared; + std::vector> channelsShared; std::vector> waves; Actions::Map actions; diff --git a/src/core/quantizer.cpp b/src/core/quantizer.cpp index f21c374..c943369 100644 --- a/src/core/quantizer.cpp +++ b/src/core/quantizer.cpp @@ -33,7 +33,7 @@ void Quantizer::trigger(int id) { assert(m_callbacks.count(id) > 0); // Make sure id exists - m_performId = id; + m_performId.store(id); } /* -------------------------------------------------------------------------- */ @@ -49,10 +49,12 @@ void Quantizer::advance(Range block, Frame quantizerStep) { /* Nothing to do if there's no action to perform. */ - if (m_performId == -1) + const int pid = m_performId.load(); + + if (pid == -1) return; - assert(m_callbacks.count(m_performId) > 0); + assert(m_callbacks.count(pid) > 0); for (Frame global = block.getBegin(), local = 0; global < block.getEnd(); global++, local++) { @@ -60,8 +62,8 @@ void Quantizer::advance(Range block, Frame quantizerStep) if (global % quantizerStep != 0) // Skip if it's not on a quantization unit. continue; - m_callbacks.at(m_performId)(local); - m_performId = -1; + m_callbacks.at(pid)(local); + m_performId.store(-1); return; } } @@ -70,13 +72,13 @@ void Quantizer::advance(Range block, Frame quantizerStep) void Quantizer::clear() { - m_performId = -1; + m_performId.store(-1); } /* -------------------------------------------------------------------------- */ bool Quantizer::hasBeenTriggered() const { - return m_performId != -1; + return m_performId.load() != -1; } } // namespace giada::m \ No newline at end of file diff --git a/src/core/quantizer.h b/src/core/quantizer.h index 490e57e..1fae526 100644 --- a/src/core/quantizer.h +++ b/src/core/quantizer.h @@ -30,6 +30,7 @@ #include "core/const.h" #include "core/range.h" #include "core/types.h" +#include "core/weakAtomic.h" #include #include @@ -69,7 +70,7 @@ public: private: std::map> m_callbacks; - int m_performId = -1; + WeakAtomic m_performId = -1; }; } // namespace giada::m diff --git a/src/glue/channel.cpp b/src/glue/channel.cpp index 2928b6e..a6aa9c2 100644 --- a/src/glue/channel.cpp +++ b/src/glue/channel.cpp @@ -176,6 +176,8 @@ std::vector getChannels() int loadChannel(ID channelId, const std::string& fname) { + auto progress = g_ui.mainWindow->getScopedProgress("Loading sample..."); + m::WaveManager::Result res = g_engine.waveManager.createFromFile(fname, /*id=*/0, g_engine.kernelAudio.getSampleRate(), g_engine.conf.data.rsmpQuality); @@ -190,7 +192,6 @@ int loadChannel(ID channelId, const std::string& fname) g_engine.conf.data.samplePath = u::fs::dirname(fname); g_engine.mixerHandler.loadChannel(channelId, std::move(res.wave)); - return G_RES_OK; } @@ -203,22 +204,16 @@ void addChannel(ID columnId, ChannelType type) /* -------------------------------------------------------------------------- */ -void addAndLoadChannel(ID columnId, const std::string& fname) -{ - m::WaveManager::Result res = g_engine.waveManager.createFromFile(fname, /*id=*/0, - g_engine.kernelAudio.getSampleRate(), g_engine.conf.data.rsmpQuality); - if (res.status == G_RES_OK) - g_engine.mixerHandler.addAndLoadChannel(columnId, std::move(res.wave), g_engine.kernelAudio.getBufferSize(), - g_engine.channelManager); - else - printLoadError_(res.status); -} - void addAndLoadChannels(ID columnId, const std::vector& fnames) { + auto progress = g_ui.mainWindow->getScopedProgress("Loading samples..."); + bool errors = false; + int i = 0; for (const std::string& f : fnames) { + progress.get().setProgress(++i / static_cast(fnames.size())); + m::WaveManager::Result res = g_engine.waveManager.createFromFile(f, /*id=*/0, g_engine.kernelAudio.getSampleRate(), g_engine.conf.data.rsmpQuality); if (res.status == G_RES_OK) diff --git a/src/glue/channel.h b/src/glue/channel.h index 0a380b6..67e175d 100644 --- a/src/glue/channel.h +++ b/src/glue/channel.h @@ -133,11 +133,6 @@ Fills an existing channel with a wave. */ int loadChannel(ID columnId, const std::string& fname); -/* addAndLoadChannel -Adds a new Sample Channel and fills it with a wave right away. */ - -void addAndLoadChannel(ID columnId, const std::string& fpath); - /* addAndLoadChannels As above, with multiple audio file paths in input. */ diff --git a/src/glue/sampleEditor.cpp b/src/glue/sampleEditor.cpp index 11a1e99..7fec174 100644 --- a/src/glue/sampleEditor.cpp +++ b/src/glue/sampleEditor.cpp @@ -155,7 +155,7 @@ Data getData(ID channelId) { /* Prepare the preview channel first, then return Data object. */ m::Channel& previewChannel = getChannel_(m::Mixer::PREVIEW_CHANNEL_ID); - previewChannel.samplePlayer->loadWave(previewChannel, &getWave_(channelId)); + previewChannel.samplePlayer->loadWave(*previewChannel.shared, &getWave_(channelId)); g_engine.model.swap(m::model::SwapType::SOFT); return Data(getChannel_(channelId)); @@ -349,7 +349,7 @@ void cleanupPreview() { m::Channel& channel = getChannel_(m::Mixer::PREVIEW_CHANNEL_ID); - channel.samplePlayer->loadWave(channel, nullptr); + channel.samplePlayer->loadWave(*channel.shared, nullptr); g_engine.model.swap(m::model::SwapType::SOFT); } diff --git a/src/glue/storage.cpp b/src/glue/storage.cpp index 8e842ae..ecb2aec 100644 --- a/src/glue/storage.cpp +++ b/src/glue/storage.cpp @@ -68,9 +68,12 @@ void loadProject(void* data) const std::string projectPath = browser->getSelectedItem(); const std::string patchPath = projectPath + G_SLASH + u::fs::stripExt(u::fs::basename(projectPath)) + ".gptc"; - browser->showStatusBar(); + auto progress = g_ui.mainWindow->getScopedProgress("Loading project..."); + auto progressCb = [&p = progress.get()](float v) { + p.setProgress(v); + }; - if (int res = g_engine.load(projectPath, patchPath); res != G_PATCH_OK) + if (int res = g_engine.load(projectPath, patchPath, progressCb); res != G_PATCH_OK) { if (res == G_PATCH_UNREADABLE) v::gdAlert("This patch is unreadable."); @@ -78,7 +81,6 @@ void loadProject(void* data) v::gdAlert("This patch is not valid."); else if (res == G_PATCH_UNSUPPORTED) v::gdAlert("This patch format is no longer supported."); - browser->hideStatusBar(); return; } @@ -115,9 +117,14 @@ void saveProject(void* data) if (u::fs::dirExists(projectPath) && !v::gdConfirmWin("Warning", "Project exists: overwrite?")) return; - g_ui.store(g_engine.patch.data); + auto progress = g_ui.mainWindow->getScopedProgress("Saving project..."); + auto progressCb = [&p = progress.get()](float v) { + p.setProgress(v); + }; - if (!g_engine.store(projectName, projectPath, patchPath)) + g_ui.store(projectName, g_engine.patch.data); + + if (!g_engine.store(projectName, projectPath, patchPath, progressCb)) { v::gdAlert("Unable to save the project!"); return; @@ -136,9 +143,9 @@ void loadSample(void* data) if (fullPath.empty()) return; - int res = c::channel::loadChannel(browser->getChannelId(), fullPath); + auto progress = g_ui.mainWindow->getScopedProgress("Loading sample..."); - if (res == G_RES_OK) + if (int res = c::channel::loadChannel(browser->getChannelId(), fullPath); res == G_RES_OK) { g_engine.conf.data.samplePath = u::fs::dirname(fullPath); browser->do_callback(); diff --git a/src/gui/dialogs/actionEditor/baseActionEditor.cpp b/src/gui/dialogs/actionEditor/baseActionEditor.cpp index dd8b480..75f4e23 100644 --- a/src/gui/dialogs/actionEditor/baseActionEditor.cpp +++ b/src/gui/dialogs/actionEditor/baseActionEditor.cpp @@ -54,7 +54,6 @@ gdBaseActionEditor::gdBaseActionEditor(ID channelId, m::Conf::Data& conf, Frame , m_barTop(0, 0, Direction::HORIZONTAL) , m_splitScroll(0, 0, 0, 0) , m_conf(conf) -, m_playhead(0) , m_ratio(conf.actionEditorZoom) { end(); @@ -176,7 +175,7 @@ void gdBaseActionEditor::draw() gdWindow::draw(); const geompp::Rect splitBounds = m_splitScroll.getBoundsNoScrollbar(); - const geompp::Line playhead = splitBounds.getHeightAsLine().withX(m_playhead); + const geompp::Line playhead = splitBounds.getHeightAsLine().withX(currentFrameToPixel()); if (splitBounds.contains(playhead)) drawLine(playhead, G_COLOR_LIGHT_2); @@ -218,7 +217,6 @@ void gdBaseActionEditor::zoomAbout(std::function f) void gdBaseActionEditor::refresh() { - m_playhead = m_data.isChannelPlaying() ? currentFrameToPixel() : 0; redraw(); } diff --git a/src/gui/dialogs/actionEditor/baseActionEditor.h b/src/gui/dialogs/actionEditor/baseActionEditor.h index b25a6a3..67a325a 100644 --- a/src/gui/dialogs/actionEditor/baseActionEditor.h +++ b/src/gui/dialogs/actionEditor/baseActionEditor.h @@ -107,7 +107,6 @@ private: Pixel currentFrameToPixel() const; - Pixel m_playhead; float m_ratio; }; } // namespace giada::v diff --git a/src/gui/dialogs/actionEditor/sampleActionEditor.cpp b/src/gui/dialogs/actionEditor/sampleActionEditor.cpp index 5c75220..939b90a 100644 --- a/src/gui/dialogs/actionEditor/sampleActionEditor.cpp +++ b/src/gui/dialogs/actionEditor/sampleActionEditor.cpp @@ -54,7 +54,7 @@ gdSampleActionEditor::gdSampleActionEditor(ID channelId, m::Conf::Data& conf, Fr m_actionType.add("Key press"); m_actionType.add("Key release"); - m_actionType.add("Kill chan"); + m_actionType.add("Stop sample"); m_actionType.value(0); m_actionType.copy_tooltip("Action type to add"); if (!canChangeActionType()) diff --git a/src/gui/dialogs/browser/browserBase.cpp b/src/gui/dialogs/browser/browserBase.cpp index eb97019..7366d18 100644 --- a/src/gui/dialogs/browser/browserBase.cpp +++ b/src/gui/dialogs/browser/browserBase.cpp @@ -28,6 +28,7 @@ #include "core/conf.h" #include "core/const.h" #include "core/graphics.h" +#include "gui/elems/basics/box.h" #include "gui/elems/basics/button.h" #include "gui/elems/basics/check.h" #include "gui/elems/basics/input.h" @@ -71,11 +72,8 @@ gdBrowserBase::gdBrowserBase(const std::string& title, const std::string& path, Fl_Group* groupButtons = new Fl_Group(8, browser->y() + browser->h() + 8, w() - 16, 20); ok = new geButton(w() - 88, groupButtons->y(), 80, 20); cancel = new geButton(w() - ok->w() - 96, groupButtons->y(), 80, 20, "Cancel"); - status = new geProgress(8, groupButtons->y(), cancel->x() - 16, 20); - status->minimum(0); - status->maximum(1); - status->hide(); // show the bar only if necessary - groupButtons->resizable(status); + geBox* spacer = new geBox(8, groupButtons->y(), cancel->x() - 16, 20); + groupButtons->resizable(spacer); groupButtons->end(); end(); @@ -132,28 +130,6 @@ void gdBrowserBase::cb_toggleHiddenFiles() /* -------------------------------------------------------------------------- */ -void gdBrowserBase::setStatusBar(float v) -{ - status->value(status->value() + v); - Fl::wait(0); -} - -/* -------------------------------------------------------------------------- */ - -void gdBrowserBase::showStatusBar() -{ - status->show(); -} - -/* -------------------------------------------------------------------------- */ - -void gdBrowserBase::hideStatusBar() -{ - status->hide(); -} - -/* -------------------------------------------------------------------------- */ - std::string gdBrowserBase::getCurrentPath() const { return where->value(); diff --git a/src/gui/dialogs/browser/browserBase.h b/src/gui/dialogs/browser/browserBase.h index 6244cda..f9d9c10 100644 --- a/src/gui/dialogs/browser/browserBase.h +++ b/src/gui/dialogs/browser/browserBase.h @@ -37,7 +37,6 @@ class Fl_Group; class geCheck; class geButton; class geInput; -class geProgress; namespace giada::m { @@ -61,14 +60,6 @@ public: ID getChannelId() const; void fireCallback() const; - /* setStatusBar - Increments status bar for progress tracking. */ - - void setStatusBar(float v); - - void showStatusBar(); - void hideStatusBar(); - protected: gdBrowserBase(const std::string& title, const std::string& path, std::function f, ID channelId, m::Conf::Data&); @@ -88,14 +79,13 @@ protected: m::Conf::Data& m_conf; ID m_channelId; - Fl_Group* groupTop; - geCheck* hiddenFiles; - geBrowser* browser; - geButton* ok; - geButton* cancel; - geInput* where; - geButton* updir; - geProgress* status; + Fl_Group* groupTop; + geCheck* hiddenFiles; + geBrowser* browser; + geButton* ok; + geButton* cancel; + geInput* where; + geButton* updir; }; } // namespace giada::v diff --git a/src/gui/dialogs/mainWindow.cpp b/src/gui/dialogs/mainWindow.cpp index ef5072c..50f566c 100644 --- a/src/gui/dialogs/mainWindow.cpp +++ b/src/gui/dialogs/mainWindow.cpp @@ -42,6 +42,29 @@ namespace giada::v { +gdMainWindow::ScopedProgress::ScopedProgress(gdProgress& p, const char* msg) +: m_progress(p) +{ + m_progress.popup(msg); +} + +/* -------------------------------------------------------------------------- */ + +gdMainWindow::ScopedProgress::~ScopedProgress() +{ + m_progress.hide(); +} + +/* -------------------------------------------------------------------------- */ + +gdProgress& gdMainWindow::ScopedProgress::get() +{ + return m_progress; +} +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ + gdMainWindow::gdMainWindow(int W, int H, const char* title, int argc, char** argv, m::Conf::Data& c) : gdWindow(W, H, title) , m_conf(c) @@ -151,4 +174,12 @@ void gdMainWindow::clearKeyboard() { keyboard->init(); } + +/* -------------------------------------------------------------------------- */ + +gdMainWindow::ScopedProgress gdMainWindow::getScopedProgress(const char* msg) +{ + return {m_progress, msg}; +} + } // namespace giada::v \ No newline at end of file diff --git a/src/gui/dialogs/mainWindow.h b/src/gui/dialogs/mainWindow.h index 4cb9c26..5c9e2fb 100644 --- a/src/gui/dialogs/mainWindow.h +++ b/src/gui/dialogs/mainWindow.h @@ -27,8 +27,9 @@ #ifndef GD_MAINWINDOW_H #define GD_MAINWINDOW_H -#include "window.h" #include "core/conf.h" +#include "gui/dialogs/progress.h" +#include "window.h" namespace giada::v { @@ -40,6 +41,8 @@ class geMainTransport; class geMainTimer; class gdMainWindow : public gdWindow { + class ScopedProgress; + public: gdMainWindow(int w, int h, const char* title, int argc, char** argv, m::Conf::Data&); ~gdMainWindow(); @@ -52,6 +55,8 @@ public: void clearKeyboard(); + ScopedProgress getScopedProgress(const char* msg); + geKeyboard* keyboard; geSequencer* sequencer; geMainMenu* mainMenu; @@ -60,7 +65,21 @@ public: geMainTransport* mainTransport; private: + class ScopedProgress + { + public: + ScopedProgress(gdProgress&, const char* msg); + ~ScopedProgress(); + + gdProgress& get(); + + private: + gdProgress& m_progress; + }; + m::Conf::Data& m_conf; + + gdProgress m_progress; }; } // namespace giada::v diff --git a/src/gui/dialogs/progress.cpp b/src/gui/dialogs/progress.cpp new file mode 100644 index 0000000..ac983f3 --- /dev/null +++ b/src/gui/dialogs/progress.cpp @@ -0,0 +1,76 @@ +/* ----------------------------------------------------------------------------- + * + * Giada - Your Hardcore Loopmachine + * + * ----------------------------------------------------------------------------- + * + * Copyright (C) 2010-2021 Giovanni A. Zuliani | Monocasual + * + * This file is part of Giada - Your Hardcore Loopmachine. + * + * Giada - Your Hardcore Loopmachine 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 3 of the License, or (at your option) any later version. + * + * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/dialogs/progress.h" +#include "core/const.h" +#include "deps/geompp/src/rect.hpp" +#include "utils/gui.h" +#include + +namespace giada::v +{ +gdProgress::gdProgress() +: gdWindow(300, 58) +, m_text(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN, w() - (G_GUI_OUTER_MARGIN * 2), 30, "", FL_ALIGN_CENTER) +, m_progress(G_GUI_OUTER_MARGIN, 40, w() - (G_GUI_OUTER_MARGIN * 2), 10) +{ + end(); + add(m_text); + add(m_progress); + + m_progress.minimum(0.0f); + m_progress.maximum(1.0f); + m_progress.value(0.0f); + + hide(); + border(0); + set_modal(); +} + +/* -------------------------------------------------------------------------- */ + +void gdProgress::setProgress(float p) +{ + m_progress.value(p); + redraw(); + Fl::flush(); +} + +/* -------------------------------------------------------------------------- */ + +void gdProgress::popup(const char* s) +{ + m_text.copy_label(s); + + const int px = u::gui::centerWindowX(w()); + const int py = u::gui::centerWindowY(h()); + + position(px, py); + show(); + wait_for_expose(); // No async bullshit, show it right away + Fl::flush(); // Make sure everything is displayed +} +} // namespace giada::v diff --git a/src/gui/dialogs/progress.h b/src/gui/dialogs/progress.h new file mode 100644 index 0000000..bae1850 --- /dev/null +++ b/src/gui/dialogs/progress.h @@ -0,0 +1,50 @@ +/* ----------------------------------------------------------------------------- + * + * Giada - Your Hardcore Loopmachine + * + * ----------------------------------------------------------------------------- + * + * Copyright (C) 2010-2021 Giovanni A. Zuliani | Monocasual + * + * This file is part of Giada - Your Hardcore Loopmachine. + * + * Giada - Your Hardcore Loopmachine 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 3 of the License, or (at your option) any later version. + * + * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GD_PROGRESS_H +#define GD_PROGRESS_H + +#include "gui/dialogs/window.h" +#include "gui/elems/basics/box.h" +#include "gui/elems/basics/progress.h" + +namespace giada::v +{ +class gdProgress : public gdWindow +{ +public: + gdProgress(); + + void setProgress(float p); + void popup(const char* s); + +private: + geBox m_text; + geProgress m_progress; +}; +} // namespace giada::v + +#endif diff --git a/src/gui/elems/basics/progress.cpp b/src/gui/elems/basics/progress.cpp index ccf4350..9e17189 100644 --- a/src/gui/elems/basics/progress.cpp +++ b/src/gui/elems/basics/progress.cpp @@ -24,13 +24,16 @@ * * -------------------------------------------------------------------------- */ -#include "progress.h" -#include "../../../core/const.h" -#include "boxtypes.h" +#include "gui/elems/basics/progress.h" +#include "core/const.h" +#include "gui/elems/basics/boxtypes.h" +namespace giada::v +{ geProgress::geProgress(int x, int y, int w, int h, const char* l) : Fl_Progress(x, y, w, h, l) { color(G_COLOR_GREY_2, G_COLOR_GREY_4); box(G_CUSTOM_BORDER_BOX); } +} // namespace giada::v \ No newline at end of file diff --git a/src/gui/elems/basics/progress.h b/src/gui/elems/basics/progress.h index b1f87a1..1ec3186 100644 --- a/src/gui/elems/basics/progress.h +++ b/src/gui/elems/basics/progress.h @@ -29,10 +29,13 @@ #include +namespace giada::v +{ class geProgress : public Fl_Progress { public: geProgress(int x, int y, int w, int h, const char* l = 0); }; +} // namespace giada::v #endif diff --git a/src/gui/elems/sampleEditor/boostTool.cpp b/src/gui/elems/sampleEditor/boostTool.cpp index 8bfc636..90f628a 100644 --- a/src/gui/elems/sampleEditor/boostTool.cpp +++ b/src/gui/elems/sampleEditor/boostTool.cpp @@ -50,7 +50,7 @@ geBoostTool::geBoostTool(int X, int Y) spacing(G_GUI_INNER_MARGIN); begin(); - label = new geBox(0, 0, u::gui::getStringWidth("Boost"), G_GUI_UNIT, "Boost", FL_ALIGN_RIGHT); + label = new geBox(0, 0, u::gui::getStringRect("Boost").w, G_GUI_UNIT, "Boost", FL_ALIGN_RIGHT); dial = new geDial(0, 0, G_GUI_UNIT, G_GUI_UNIT); input = new geInput(0, 0, 70, G_GUI_UNIT); normalize = new geButton(0, 0, 70, G_GUI_UNIT, "Normalize"); diff --git a/src/gui/ui.cpp b/src/gui/ui.cpp index e7d2897..fda0bc1 100644 --- a/src/gui/ui.cpp +++ b/src/gui/ui.cpp @@ -74,13 +74,13 @@ void Ui::load(const m::Patch::Data& patch) /* -------------------------------------------------------------------------- */ -void Ui::store(m::Patch::Data& patch) +void Ui::store(const std::string patchName, m::Patch::Data& patch) { patch.columns.clear(); mainWindow->keyboard->forEachColumn([&](const geColumn& c) { patch.columns.push_back({c.id, c.w()}); }); - setMainWindowTitle(patch.name); + setMainWindowTitle(patchName); } /* -------------------------------------------------------------------------- */ diff --git a/src/gui/ui.h b/src/gui/ui.h index a23a5c1..724347e 100644 --- a/src/gui/ui.h +++ b/src/gui/ui.h @@ -61,7 +61,7 @@ public: /* store Writes UI information to a patch when a project needs to be saved. */ - void store(m::Patch::Data& patch); + void store(const std::string patchName, m::Patch::Data& patch); void init(int argc, char** argv, m::Engine&); void reset(); diff --git a/src/utils/gui.cpp b/src/utils/gui.cpp index 1d15cc5..7446db0 100644 --- a/src/utils/gui.cpp +++ b/src/utils/gui.cpp @@ -74,12 +74,12 @@ void setFavicon(v::gdWindow* w) /* -------------------------------------------------------------------------- */ -int getStringWidth(const std::string& s) +geompp::Rect getStringRect(const std::string& s) { int w = 0; int h = 0; fl_measure(s.c_str(), w, h); - return w; + return {0, 0, w, h}; } /* -------------------------------------------------------------------------- */ @@ -97,13 +97,13 @@ std::string removeFltkChars(const std::string& s) std::string truncate(const std::string& s, Pixel width) { - if (s.empty() || getStringWidth(s) <= width) + if (s.empty() || getStringRect(s).w <= width) return s; std::string tmp = s; std::size_t size = tmp.size(); - while (getStringWidth(tmp + "...") > width) + while (getStringRect(tmp + "...").w > width) { if (size == 0) return ""; diff --git a/src/utils/gui.h b/src/utils/gui.h index afe1927..537e723 100644 --- a/src/utils/gui.h +++ b/src/utils/gui.h @@ -28,6 +28,7 @@ #define G_UTILS_GUI_H #include "core/types.h" +#include "deps/geompp/src/rect.hpp" #include #include @@ -45,10 +46,10 @@ Strips special chars used by FLTK to split menus into sub-menus. */ std::string removeFltkChars(const std::string& s); -/* getStringWidth -Returns the width in pixels of a string 's'. */ +/* getStringRect +Returns the bounding box in pixels of a string 's'. */ -int getStringWidth(const std::string& s); +geompp::Rect getStringRect(const std::string& s); /* truncate Adds ellipsis to a string 's' if it longer than 'width' pixels. */ diff --git a/src/utils/vector.h b/src/utils/vector.h index b05959a..025a844 100644 --- a/src/utils/vector.h +++ b/src/utils/vector.h @@ -34,25 +34,25 @@ namespace giada::u::vector { template -std::size_t indexOf(T& v, const P& p) +std::size_t indexOf(const T& v, const P& p) { - return std::distance(v.begin(), std::find(v.begin(), v.end(), p)); + return std::distance(std::cbegin(v), std::find(std::cbegin(v), std::cend(v), p)); } /* -------------------------------------------------------------------------- */ template -auto findIf(T& v, F&& func) +auto findIf(const T& v, F&& func) { - return std::find_if(v.begin(), v.end(), func); + return std::find_if(std::cbegin(v), std::cend(v), func); } /* -------------------------------------------------------------------------- */ template -bool has(T& v, F&& func) +bool has(const T& v, F&& func) { - return findIf(v, func) != v.end(); + return findIf(v, func) != std::cend(v); } /* -------------------------------------------------------------------------- */ diff --git a/tests/samplePlayer.cpp b/tests/samplePlayer.cpp new file mode 100644 index 0000000..1c6a267 --- /dev/null +++ b/tests/samplePlayer.cpp @@ -0,0 +1,91 @@ +#include "../src/core/channels/samplePlayer.h" +#include + +TEST_CASE("SamplePlayer") +{ + using namespace giada; + + constexpr int BUFFER_SIZE = 1024; + constexpr int NUM_CHANNELS = 2; + + // Wave values: [1..BUFFERSIZE*4] + m::Wave wave(0); + wave.getBuffer().alloc(BUFFER_SIZE * 4, NUM_CHANNELS); + wave.getBuffer().forEachFrame([](float* f, int i) { + f[0] = static_cast(i + 1); + f[1] = static_cast(i + 1); + }); + + m::ChannelShared channelShared(BUFFER_SIZE); + m::Resampler resampler(m::Resampler::Quality::LINEAR, NUM_CHANNELS); + + m::SamplePlayer samplePlayer(&resampler); + samplePlayer.onLastFrame = [](bool) {}; + + SECTION("Test initialization") + { + REQUIRE(samplePlayer.hasWave() == false); + } + + SECTION("Test rendering") + { + samplePlayer.loadWave(channelShared, &wave); + + REQUIRE(samplePlayer.hasWave() == true); + REQUIRE(samplePlayer.begin == 0); + REQUIRE(samplePlayer.end == wave.getBuffer().countFrames() - 1); + + REQUIRE(channelShared.tracker.load() == 0); + REQUIRE(channelShared.playStatus.load() == ChannelStatus::OFF); + + for (const float pitch : {1.0f, 0.5f}) + { + samplePlayer.pitch = pitch; + + SECTION("Sub-range [M, N), pitch == " + std::to_string(pitch)) + { + constexpr int RANGE_BEGIN = 16; + constexpr int RANGE_END = 48; + + samplePlayer.begin = RANGE_BEGIN; + samplePlayer.end = RANGE_END; + samplePlayer.render(channelShared, {}); + + int numFramesWritten = 0; + channelShared.audioBuffer.forEachFrame([&numFramesWritten](float* f, int) { + if (f[0] != 0.0) + numFramesWritten++; + }); + + REQUIRE(numFramesWritten == (RANGE_END - RANGE_BEGIN) / pitch); + } + + SECTION("Rewind, pitch == " + std::to_string(pitch)) + { + // Point in audio buffer where the rewind takes place + const int OFFSET = 256; + + samplePlayer.render(channelShared, {m::SamplePlayer::Render::Mode::REWIND, OFFSET}); + + // Rendering should start over again at buffer[OFFSET] + REQUIRE(channelShared.audioBuffer[OFFSET][0] == 1.0f); + } + + SECTION("Stop, pitch == " + std::to_string(pitch)) + { + // Point in audio buffer where the stop takes place + const int OFFSET = 256; + + samplePlayer.render(channelShared, {m::SamplePlayer::Render::Mode::STOP, OFFSET}); + + int numFramesWritten = 0; + channelShared.audioBuffer.forEachFrame([&numFramesWritten](float* f, int) { + if (f[0] != 0.0) + numFramesWritten++; + }); + + REQUIRE(numFramesWritten == OFFSET); + } + } + } +} diff --git a/tests/waveReader.cpp b/tests/waveReader.cpp new file mode 100644 index 0000000..ef7b587 --- /dev/null +++ b/tests/waveReader.cpp @@ -0,0 +1,72 @@ +#include "../src/core/channels/waveReader.h" +#include "../src/core/resampler.h" +#include "../src/core/wave.h" +#include "../src/utils/vector.h" +#include +#include + +TEST_CASE("WaveReader") +{ + using namespace giada; + + constexpr int BUFFER_SIZE = 1024; + constexpr int NUM_CHANNELS = 2; + + m::Wave wave(0); + wave.getBuffer().alloc(BUFFER_SIZE, NUM_CHANNELS); + wave.getBuffer().forEachFrame([](float* f, int i) { + f[0] = static_cast(i + 1); + f[1] = static_cast(i + 1); + }); + m::Resampler resampler; + m::WaveReader waveReader(&resampler); + + SECTION("Test initialization") + { + REQUIRE(waveReader.wave == nullptr); + } + + waveReader.wave = &wave; + + SECTION("Test fill, pitch 1.0") + { + mcl::AudioBuffer out(BUFFER_SIZE, NUM_CHANNELS); + + SECTION("Regular fill") + { + m::WaveReader::Result res = waveReader.fill(out, + /*start=*/0, BUFFER_SIZE, /*offset=*/0, /*pitch=*/1.0f); + + bool allFilled = true; + int numFramesFilled = 0; + out.forEachFrame([&allFilled, &numFramesFilled](const float* f, int) { + if (f[0] == 0.0f) + allFilled = false; + else + numFramesFilled++; + }); + + REQUIRE(allFilled); + REQUIRE(numFramesFilled == res.used); + REQUIRE(numFramesFilled == res.generated); + } + + SECTION("Partial fill") + { + m::WaveReader::Result res = waveReader.fill(out, + /*start=*/0, BUFFER_SIZE, /*offset=*/BUFFER_SIZE / 2, /*pitch=*/1.0f); + + int numFramesFilled = 0; + out.forEachFrame([&numFramesFilled](const float* f, int) { + if (f[0] != 0.0f) + numFramesFilled++; + }); + + REQUIRE(numFramesFilled == BUFFER_SIZE / 2); + REQUIRE(out[(BUFFER_SIZE / 2) - 1][0] == 0.0f); + REQUIRE(out[BUFFER_SIZE / 2][0] != 0.0f); + REQUIRE(numFramesFilled == res.used); + REQUIRE(numFramesFilled == res.generated); + } + } +}