New upstream version 0.20.0+ds1
authorIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Mon, 31 Jan 2022 10:57:19 +0000 (11:57 +0100)
committerIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Mon, 31 Jan 2022 10:57:19 +0000 (11:57 +0100)
49 files changed:
CMakeLists.txt
ChangeLog
src/core/channels/channel.cpp
src/core/channels/channel.h
src/core/channels/channelManager.cpp
src/core/channels/channelManager.h
src/core/channels/sampleAdvancer.cpp
src/core/channels/sampleAdvancer.h
src/core/channels/samplePlayer.cpp
src/core/channels/samplePlayer.h
src/core/channels/sampleReactor.cpp
src/core/channels/sampleReactor.h
src/core/const.h
src/core/engine.cpp
src/core/engine.h
src/core/init.cpp
src/core/kernelMidi.cpp
src/core/kernelMidi.h
src/core/mixer.cpp
src/core/mixer.h
src/core/mixerHandler.cpp
src/core/mixerHandler.h
src/core/model/model.cpp
src/core/model/model.h
src/core/quantizer.cpp
src/core/quantizer.h
src/glue/channel.cpp
src/glue/channel.h
src/glue/sampleEditor.cpp
src/glue/storage.cpp
src/gui/dialogs/actionEditor/baseActionEditor.cpp
src/gui/dialogs/actionEditor/baseActionEditor.h
src/gui/dialogs/actionEditor/sampleActionEditor.cpp
src/gui/dialogs/browser/browserBase.cpp
src/gui/dialogs/browser/browserBase.h
src/gui/dialogs/mainWindow.cpp
src/gui/dialogs/mainWindow.h
src/gui/dialogs/progress.cpp [new file with mode: 0644]
src/gui/dialogs/progress.h [new file with mode: 0644]
src/gui/elems/basics/progress.cpp
src/gui/elems/basics/progress.h
src/gui/elems/sampleEditor/boostTool.cpp
src/gui/ui.cpp
src/gui/ui.h
src/utils/gui.cpp
src/utils/gui.h
src/utils/vector.h
tests/samplePlayer.cpp [new file with mode: 0644]
tests/waveReader.cpp [new file with mode: 0644]

index 0c4427eee93fdec60dc802fa8b0a7904e2680ac6..1a0ca06fec318b1553fea479c6518de9cfe735f9 100644 (file)
@@ -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()
index 815cc331d08f6718f4d4d88286c827e05bc66db7..4adddd470929b9e57068c4b68621449d7d7cbed2 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
 --------------------------------------------------------------------------------
 
 
+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
 
index 5aa00ab763607b199601f46b2ed325d24140f496..6c9cb87c2324b48a52d8dda7adbee125295fd736 100644 (file)
@@ -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<Frame> 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);
 
index 1d1d81c0efdc32b4996a3c5959aac67f937a96dc..aa91a9a5c209db514fba5276c86681c83f2746d8 100644 (file)
 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<MidiEvent, 32> midiQueue;
+       juce::MidiBuffer     midiBuffer;
+       Queue<MidiEvent, 32> midiQueue;
 #endif
 
-               WeakAtomic<Frame>         tracker     = 0;
-               WeakAtomic<ChannelStatus> playStatus  = ChannelStatus::OFF;
-               WeakAtomic<ChannelStatus> recStatus   = ChannelStatus::OFF;
-               WeakAtomic<bool>          readActions = false;
-               bool                      rewinding   = false;
-               Frame                     offset      = 0;
+       WeakAtomic<Frame>         tracker     = 0;
+       WeakAtomic<ChannelStatus> playStatus  = ChannelStatus::OFF;
+       WeakAtomic<ChannelStatus> recStatus   = ChannelStatus::OFF;
+       WeakAtomic<bool>          readActions = false;
+
+       std::optional<Quantizer> quantizer;
+
+       /* Optional render queue for sample-based channels. Used by SampleReactor
+       and SampleAdvancer to instruct SamplePlayer how to render audio. */
+
+       std::optional<Queue<SamplePlayer::Render, 2>> 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> resampler = {};
-       };
+       std::optional<Resampler> 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>, 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<Plugin*> plugins;
 #endif
index da24d726ab1625e24d480ee2dc46bb62ee9f4f61..30c5578b68a663ca3c0f39e509896c2defbf6584 100644 (file)
@@ -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<Channel::Shared> shared = std::make_unique<Channel::Shared>(bufferSize);
+       std::unique_ptr<ChannelShared> shared = std::make_unique<ChannelShared>(bufferSize);
 
        if (type == ChannelType::SAMPLE || type == ChannelType::PREVIEW)
-               shared->resampler = Resampler(static_cast<Resampler::Quality>(m_conf.rsmpQuality), G_MAX_IO_CHANS);
+       {
+               shared->quantizer.emplace();
+               shared->renderQueue.emplace();
+               shared->resampler.emplace(static_cast<Resampler::Quality>(m_conf.rsmpQuality), G_MAX_IO_CHANS);
+       }
 
        m_model.addShared(std::move(shared));
-       return m_model.backShared<Channel::Shared>();
+       return m_model.backShared<ChannelShared>();
 }
 } // namespace giada::m
index 0e32e04edc3fa69269f6c9e5eee8ce2f7637ef2c..2e1e5826c166b1db5f964e01b473cee21cbf934d 100644 (file)
@@ -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;
 
index e029c346c839729a3f4da183a58f7b12b6e4e6c3..9f0391a7f717a20b22e46c916b477effa7fed916 100644 (file)
@@ -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:
index bf5a8d4d95f57ef4d5bf0568da9e38c1c2a5e60f..e3246c0edd7f2506eed81ea5de0ee0268d3e22d4 100644 (file)
@@ -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;
index 350d34112e5888d490eabe30fac177880b026442..108a8ef1d6f13878ac32f993a5e3e6a47b23fd4e 100644 (file)
@@ -31,6 +31,8 @@
 #include <algorithm>
 #include <cassert>
 
+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
index d9bcbcff20df6332881b7b511ad690be1674fe38..2e18b1740f0e4e2e0c106c0ecd2d60c62a5b3a39 100644 (file)
 
 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<void()> 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<void(bool natural)> 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
 
index 1358a5f3b48cb5a1716380e18395e722265763e1..b009d7b130b14444243a12f8c0c47858afdde9a5 100644 (file)
@@ -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
index cfb725c6f9cfc115caee9f7e2b1d4ae63d9344be..8761f2efc1228aa5f561a0622ecaca9d7c06b20a 100644 (file)
@@ -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
index 35eccbbfb03b46ff1b2cc69cff5cdf36be576d39..a2e7356aaf391178ee3a53299fc0a7ffeb5088ce 100644 (file)
 
 /* -- 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;
index a71c86374423e7eb4d5d8c5447d792dbba836c26..31394438600e2dbe6d037d75d690b7ec7e12dc0e 100644 (file)
@@ -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<PluginManager::SortMethod>(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<PluginManager::SortMethod>(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<Frame> 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<void(float)> 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<void(float)> 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;
 }
 
index b0811623e8be908d22bc9c40dba70cb47ecb6e61..5fa6ae5ded2b5e629521fdd09c15a2633775125e 100644 (file)
@@ -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<void(float)> 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<void(float)> progress);
 
        /* updateMixerModel
        Updates some values in model::Mixer data struct needed by m::Mixer for the
index 9e1504eecd7d2441b6e5af4579cf7e116bb9cbfc..bd4b98f110b2299368aab0e413fdfc58816d8782 100644 (file)
 #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 <catch2/catch.hpp>
 #include <string>
 #include <vector>
index 254cee8a40085256bce68a650ee3b331275e3990..fd2458774c5fa3a41ec94291dfcf3b3512668770 100644 (file)
@@ -70,8 +70,13 @@ bool KernelMidi::openOutDevice(int api, int port)
        if (port == -1)
                return false;
 
-       m_midiOut = makeDevice<RtMidiOut>(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<RtMidiOut>(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<RtMidiIn>(api, port, INPUT_NAME);
+       u::log::print("[KM] Opening input device '%s', port=%d\n", INPUT_NAME, port);
+
+       m_midiIn = makeDevice<RtMidiIn>(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<unsigned char>* msg)
 /* -------------------------------------------------------------------------- */
 
 template <typename Device>
-std::unique_ptr<Device> KernelMidi::makeDevice(int api, int port, std::string name) const
+std::unique_ptr<Device> KernelMidi::makeDevice(int api, std::string name) const
 {
        try
        {
-               auto device = std::make_unique<Device>(static_cast<RtMidi::Api>(api), name);
-               device->openPort(port, device->getPortName(port));
-               return device;
+               return std::make_unique<Device>(static_cast<RtMidi::Api>(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<RtMidiOut> KernelMidi::makeDevice(int, int, std::string) const;
-template std::unique_ptr<RtMidiIn>  KernelMidi::makeDevice(int, int, std::string) const;
+template std::unique_ptr<RtMidiOut> KernelMidi::makeDevice(int, std::string) const;
+template std::unique_ptr<RtMidiIn>  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;
+       }
+}
 
 /* -------------------------------------------------------------------------- */
 
index 5489a22e446007a179e1c7bccab39b39fe40ab31..88f98ed944bcbe678d631209aa1312d66f75b67f 100644 (file)
@@ -71,15 +71,17 @@ public:
        std::function<void(uint32_t)> onMidiReceived;
 
 private:
-       template <typename Device>
-       std::unique_ptr<Device> makeDevice(int api, int port, std::string name) const;
-
        static void s_callback(double, std::vector<unsigned char>*, void*);
        void        callback(std::vector<unsigned char>*);
 
+       template <typename Device>
+       std::unique_ptr<Device> 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<RtMidiOut> m_midiOut;
        std::unique_ptr<RtMidiIn>  m_midiIn;
 };
index 8c5f81db205ddc33957f5af1edefa8028dd49c77..7b8cd5416e5e993a7c35a0ecfcf559b91795a924 100644 (file)
@@ -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<Frame> block, Frame quantizerStep)
 {
        for (const Channel& c : rtLayout.channels)
                if (!c.isInternal())
-                       c.advance(events);
+                       c.advance(events, block, quantizerStep);
 }
 
 /* -------------------------------------------------------------------------- */
index 204845cca3996abc939cb3d4bd66442f67ba0526..6ed46358c6f57eadd61f35ef621578d203e8a7c9 100644 (file)
@@ -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>, Frame quantizerStep);
 
        /* onSignalTresholdReached
        Callback fired when audio has reached a certain threshold (record-on-signal 
index 31cf8aef4e8ac5c8f2fd6df9b5b2879f5175d9c4..d636775a006515dd0716a033eefbd048416d20b9 100644 (file)
@@ -92,7 +92,7 @@ void MixerHandler::loadChannel(ID channelId, std::unique_ptr<Wave> w)
        Wave&    wave    = m_model.backShared<Wave>();
        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<Wave> w, int b
        Wave&    wave    = m_model.backShared<Wave>();
        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<Wave>());
+               loadChannel(newChannel, &m_model.backShared<Wave>());
        }
 
 #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<model::WavePtrs>();
@@ -216,6 +216,7 @@ void MixerHandler::deleteChannel(ID channelId)
        if (wave != nullptr)
                m_model.removeShared<Wave>(*wave);
 
+       updateSoloCount();
        onChannelsAltered();
 }
 
@@ -337,6 +338,14 @@ bool MixerHandler::forAnyChannel(std::function<bool(const Channel&)> 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<Channel*> MixerHandler::getChannelsIf(std::function<bool(const Channel&)> f)
 {
        std::vector<Channel*> 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<Wave>());
+       loadChannel(ch, &m_model.backShared<Wave>());
        setupChannelPostRecording(ch, currentFrame);
 
        m_model.swap(model::SwapType::HARD);
index 92a9de58451df5d4a3cf80187d399cba9a4d00c2..b9513568b7dec71fa3fef9aa2f90d26d879ba4e3 100644 (file)
@@ -163,6 +163,8 @@ public:
 private:
        bool forAnyChannel(std::function<bool(const Channel&)> f) const;
 
+       void loadChannel(Channel&, Wave*) const;
+
        std::vector<Channel*> getChannelsIf(std::function<bool(const Channel&)> f);
        std::vector<Channel*> getRecordableChannels();
        std::vector<Channel*> getOverdubbableChannels();
index 3d203bbc845cbf2ba2cc7f948f1f6d12ae45b5d7..75a30e0778b060825769c7a3687306637abcad8f 100644 (file)
@@ -246,15 +246,15 @@ T& Model::backShared()
 #endif
        if constexpr (std::is_same_v<T, Wave>)
                return *m_shared.waves.back().get();
-       if constexpr (std::is_same_v<T, Channel::Shared>)
+       if constexpr (std::is_same_v<T, ChannelShared>)
                return *m_shared.channelsShared.back().get();
 }
 
 #ifdef WITH_VST
 template Plugin& Model::backShared<Plugin>();
 #endif
-template Wave&            Model::backShared<Wave>();
-template Channel::Shared& Model::backShared<Channel::Shared>();
+template Wave&          Model::backShared<Wave>();
+template ChannelShared& Model::backShared<ChannelShared>();
 
 /* -------------------------------------------------------------------------- */
 
index 6b7fe33a30236550678102c7c1548fe0c9b0cdbd..12e0f3ad8956375dfa1eb001977799e9da272f50 100644 (file)
@@ -100,7 +100,7 @@ enum class SwapType
 using PluginPtr = std::unique_ptr<Plugin>;
 #endif
 using WavePtr          = std::unique_ptr<Wave>;
-using ChannelSharedPtr = std::unique_ptr<Channel::Shared>;
+using ChannelSharedPtr = std::unique_ptr<ChannelShared>;
 
 #ifdef WITH_VST
 using PluginPtrs = std::vector<PluginPtr>;
@@ -187,10 +187,10 @@ public:
 private:
        struct Shared
        {
-               Sequencer::Shared                             sequencerShared;
-               Mixer::Shared                                 mixerShared;
-               Recorder::Shared                              recorderShared;
-               std::vector<std::unique_ptr<Channel::Shared>> channelsShared;
+               Sequencer::Shared                           sequencerShared;
+               Mixer::Shared                               mixerShared;
+               Recorder::Shared                            recorderShared;
+               std::vector<std::unique_ptr<ChannelShared>> channelsShared;
 
                std::vector<std::unique_ptr<Wave>> waves;
                Actions::Map                       actions;
index f21c37467072a0581128d1d93e05a9bb3d92bb25..c9433697ca4f681ae1b884690d72b10d531b10b9 100644 (file)
@@ -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<Frame> 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<Frame> 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<Frame> 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
index 490e57e6bc7552682a13e74ea84d95824eff76c7..1fae5269e8f9aa8d9544d7e8f7234dd3c9d618ba 100644 (file)
@@ -30,6 +30,7 @@
 #include "core/const.h"
 #include "core/range.h"
 #include "core/types.h"
+#include "core/weakAtomic.h"
 #include <functional>
 #include <map>
 
@@ -69,7 +70,7 @@ public:
 
 private:
        std::map<int, std::function<void(Frame)>> m_callbacks;
-       int                                       m_performId = -1;
+       WeakAtomic<int>                           m_performId = -1;
 };
 } // namespace giada::m
 
index 2928b6e93435d9c0fb7c3bf7472036999b2404cd..a6aa9c2e579e3d115d09321ba8c6fcf2f6752625 100644 (file)
@@ -176,6 +176,8 @@ std::vector<Data> 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<std::string>& 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<float>(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)
index 0a380b683aa868fb9634b9b1132b40f2200175d0..67e175d56fcf0ed0f965bb7292286e2d90c31271 100644 (file)
@@ -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. */
 
index 11a1e99203ee75522d1ec10d46c70b792599f292..7fec1745d48eec823c4310efecb41bdd53abdb93 100644 (file)
@@ -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);
 }
 
index 8e842ae9069228386dbb14eb399c228c9cc0b914..ecb2aec4f6973c9c476fecbc25b6a45d02450981 100644 (file)
@@ -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();
index dd8b480aaae8e9c91a6fcace8368afb343d6b6a4..75f4e23280e42085fa9f719d89e2499f139459ef 100644 (file)
@@ -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<float()> f)
 
 void gdBaseActionEditor::refresh()
 {
-       m_playhead = m_data.isChannelPlaying() ? currentFrameToPixel() : 0;
        redraw();
 }
 
index b25a6a32acb21d92007120aa4ef5cafc73ae75a2..67a325af0b27766d7a6923b5bbfcd77a69642aa8 100644 (file)
@@ -107,7 +107,6 @@ private:
 
        Pixel currentFrameToPixel() const;
 
-       Pixel m_playhead;
        float m_ratio;
 };
 } // namespace giada::v
index 5c75220fc5c10e97730d448f0e220f63eccc779b..939b90af46471f4b2f4ac3cbc43cb6fea2b711a1 100644 (file)
@@ -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())
index eb9701935ae5730e5d6935f7990f2d52471483ef..7366d18cf4720e5b92b4f28c2d6d3f42aa1fb6f5 100644 (file)
@@ -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();
index 6244cda6ee591a10f14dbed9190088244b6aa98e..f9d9c10e0063fecf2968d380d72a2c0c56435ad3 100644 (file)
@@ -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<void(void*)> 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
 
index ef5072c69ed332cfd205f9f327e66cbaa19b67e9..50f566ca1349a2bf06f8da1643637ed3f80fad22 100644 (file)
 
 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
index 4cb9c26ff60e6cd0dbde7cbce59315f838de363b..5c9e2fba6a8386d05045c9a814a4e91d99bc2cde 100644 (file)
@@ -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 (file)
index 0000000..ac983f3
--- /dev/null
@@ -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
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/progress.h"
+#include "core/const.h"
+#include "deps/geompp/src/rect.hpp"
+#include "utils/gui.h"
+#include <FL/Fl.H>
+
+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 (file)
index 0000000..bae1850
--- /dev/null
@@ -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
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#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
index ccf4350fd6e22e1a1d96c9c346a5adf97dd124f6..9e171890486c9e5d3dc127f007f187c9d5ef4c05 100644 (file)
  *
  * -------------------------------------------------------------------------- */
 
-#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
index b1f87a140bea74d7f6af6266eaa82c57771b0206..1ec31863c44789152ab0c599760f99aca8b2ba80 100644 (file)
 
 #include <FL/Fl_Progress.H>
 
+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
index 8bfc636f964a94ce9ccba430ea45277c9ec4415c..90f628ab5bfdde463add3467569b215f54ba5aac 100644 (file)
@@ -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");
index e7d28979b991403415088d27d260a499680b59cc..fda0bc17d4ebf0220a350583e4829f30ed6a18e3 100644 (file)
@@ -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);
 }
 
 /* -------------------------------------------------------------------------- */
index a23a5c10f5d9c54ce2745b6ffb94f8f1c17216e6..724347e5d20ae526b3f580b555d3d8d73f31b1b6 100644 (file)
@@ -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();
index 1d15cc59c0575a3a1db4f0f1757e780ac1b2039f..7446db04ce9223ef85593472c0f130dd27dab528 100644 (file)
@@ -74,12 +74,12 @@ void setFavicon(v::gdWindow* w)
 
 /* -------------------------------------------------------------------------- */
 
-int getStringWidth(const std::string& s)
+geompp::Rect<int> 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 "";
index afe1927cc04eae4b9752845d2f0d9fbd5a898468..537e7235ce572cdd90998f18627935f717672527 100644 (file)
@@ -28,6 +28,7 @@
 #define G_UTILS_GUI_H
 
 #include "core/types.h"
+#include "deps/geompp/src/rect.hpp"
 #include <FL/Fl_Menu_Item.H>
 #include <string>
 
@@ -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<int> getStringRect(const std::string& s);
 
 /* truncate
 Adds ellipsis to a string 's' if it longer than 'width' pixels. */
index b05959ae56e75c7cd91fe523b2a14987169ec53e..025a844c4f880fb2a7e764aaedd59fc914ac6b05 100644 (file)
 namespace giada::u::vector
 {
 template <typename T, typename P>
-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 <typename T, typename F>
-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 <typename T, typename F>
-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 (file)
index 0000000..1c6a267
--- /dev/null
@@ -0,0 +1,91 @@
+#include "../src/core/channels/samplePlayer.h"
+#include <catch2/catch.hpp>
+
+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<float>(i + 1);
+               f[1] = static_cast<float>(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 (file)
index 0000000..ef7b587
--- /dev/null
@@ -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 <catch2/catch.hpp>
+#include <memory>
+
+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<float>(i + 1);
+               f[1] = static_cast<float>(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);
+               }
+       }
+}