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
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
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
set_target_properties(giada PROPERTIES
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES)
-endif()
\ No newline at end of file
+endif()
--------------------------------------------------------------------------------
+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
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
-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)
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:
/* -------------------------------------------------------------------------- */
-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)
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:
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)
{
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);
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;
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. */
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
/* -------------------------------------------------------------------------- */
-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
const Patch::Channel serializeChannel(const Channel& c);
private:
- Channel::Shared& makeShared(ChannelType type, int bufferSize);
+ ChannelShared& makeShared(ChannelType type, int bufferSize);
IdManager m_channelId;
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();
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:
break;
case Sequencer::EventType::REWIND:
- rewind(ch, e.delta);
+ if (ch.samplePlayer->isAnyLoopMode())
+ rewind(ch, e.delta);
break;
case Sequencer::EventType::ACTIONS:
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});
}
/* -------------------------------------------------------------------------- */
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});
}
/* -------------------------------------------------------------------------- */
case MidiEvent::NOTE_OFF:
case MidiEvent::NOTE_KILL:
- stop(ch, localFrame);
+ if (ch.shared->playStatus.load() == ChannelStatus::PLAY)
+ stop(ch, localFrame);
break;
default:
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;
#include <algorithm>
#include <cassert>
+using namespace mcl;
+
namespace giada::m
{
SamplePlayer::SamplePlayer(Resampler* r)
/* -------------------------------------------------------------------------- */
-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)
{
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;
}
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
-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
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);
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
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;
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
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);
});
}
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;
}
/* -------------------------------------------------------------------------- */
-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});
}
/* -------------------------------------------------------------------------- */
if (sequencer.canQuantize())
{
- sequencer.quantizer.trigger(Q_ACTION_PLAY + ch.id);
+ ch.shared->quantizer->trigger(Q_ACTION_PLAY + ch.id);
return ChannelStatus::OFF;
}
else
{
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;
/* -------------------------------------------------------------------------- */
-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. */
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();
}
/* -------------------------------------------------------------------------- */
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
namespace giada::m
{
class Channel;
+class ChannelShared;
class Sequencer;
/* SampleReactor
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
/* -- 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";
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;
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);
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
}
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. */
/* -------------------------------------------------------------------------- */
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");
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;
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. */
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. */
mixer.enable();
+ progress(1.0f);
+
return G_PATCH_OK;
}
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
#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>
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);
}
/* -------------------------------------------------------------------------- */
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
/* -------------------------------------------------------------------------- */
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;
+ }
+}
/* -------------------------------------------------------------------------- */
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;
};
/* -------------------------------------------------------------------------- */
-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);
}
/* -------------------------------------------------------------------------- */
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
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
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();
{
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
const Wave* wave = ch.samplePlayer->getWave();
- ch.samplePlayer->loadWave(ch, nullptr);
+ loadChannel(ch, nullptr);
m_model.swap(model::SwapType::HARD);
if (wave != nullptr)
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>();
if (wave != nullptr)
m_model.removeShared<Wave>(*wave);
+ updateSoloCount();
onChannelsAltered();
}
/* -------------------------------------------------------------------------- */
+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;
{
/* 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;
/* 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);
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();
#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>();
/* -------------------------------------------------------------------------- */
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>;
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;
{
assert(m_callbacks.count(id) > 0); // Make sure id exists
- m_performId = id;
+ m_performId.store(id);
}
/* -------------------------------------------------------------------------- */
{
/* 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++)
{
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;
}
}
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
#include "core/const.h"
#include "core/range.h"
#include "core/types.h"
+#include "core/weakAtomic.h"
#include <functional>
#include <map>
private:
std::map<int, std::function<void(Frame)>> m_callbacks;
- int m_performId = -1;
+ WeakAtomic<int> m_performId = -1;
};
} // namespace giada::m
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);
g_engine.conf.data.samplePath = u::fs::dirname(fname);
g_engine.mixerHandler.loadChannel(channelId, std::move(res.wave));
-
return G_RES_OK;
}
/* -------------------------------------------------------------------------- */
-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)
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. */
{
/* 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));
{
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);
}
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.");
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;
}
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;
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();
, m_barTop(0, 0, Direction::HORIZONTAL)
, m_splitScroll(0, 0, 0, 0)
, m_conf(conf)
-, m_playhead(0)
, m_ratio(conf.actionEditorZoom)
{
end();
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);
void gdBaseActionEditor::refresh()
{
- m_playhead = m_data.isChannelPlaying() ? currentFrameToPixel() : 0;
redraw();
}
Pixel currentFrameToPixel() const;
- Pixel m_playhead;
float m_ratio;
};
} // namespace giada::v
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())
#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"
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();
/* -------------------------------------------------------------------------- */
-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();
class geCheck;
class geButton;
class geInput;
-class geProgress;
namespace giada::m
{
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&);
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
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)
{
keyboard->init();
}
+
+/* -------------------------------------------------------------------------- */
+
+gdMainWindow::ScopedProgress gdMainWindow::getScopedProgress(const char* msg)
+{
+ return {m_progress, msg};
+}
+
} // namespace giada::v
\ No newline at end of file
#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
{
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();
void clearKeyboard();
+ ScopedProgress getScopedProgress(const char* msg);
+
geKeyboard* keyboard;
geSequencer* sequencer;
geMainMenu* mainMenu;
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
--- /dev/null
+/* -----------------------------------------------------------------------------
+ *
+ * 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
--- /dev/null
+/* -----------------------------------------------------------------------------
+ *
+ * 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
*
* -------------------------------------------------------------------------- */
-#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
#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
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");
/* -------------------------------------------------------------------------- */
-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);
}
/* -------------------------------------------------------------------------- */
/* 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();
/* -------------------------------------------------------------------------- */
-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};
}
/* -------------------------------------------------------------------------- */
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 "";
#define G_UTILS_GUI_H
#include "core/types.h"
+#include "deps/geompp/src/rect.hpp"
#include <FL/Fl_Menu_Item.H>
#include <string>
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. */
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);
}
/* -------------------------------------------------------------------------- */
--- /dev/null
+#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);
+ }
+ }
+ }
+}
--- /dev/null
+#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);
+ }
+ }
+}