diff --git a/src-ui/app/browser-ui/BrowserPane.cpp b/src-ui/app/browser-ui/BrowserPane.cpp index 56950699..1b32d747 100644 --- a/src-ui/app/browser-ui/BrowserPane.cpp +++ b/src-ui/app/browser-ui/BrowserPane.cpp @@ -31,6 +31,7 @@ #include "sst/jucegui/components/Label.h" #include "sst/jucegui/components/NamedPanelDivider.h" #include "sst/jucegui/components/TextPushButton.h" +#include "sst/jucegui/components/GlyphButton.h" #include "sst/plugininfra/strnatcmp.h" namespace scxt::ui::app::browser_ui @@ -290,9 +291,35 @@ struct DriveFSListBoxRow : public juce::Component DriveFSListBoxModel *model{nullptr}; juce::ListBox *enclosingBox() { return browserPane->devicesPane->driveFSArea->lbox.get(); } - bool isDragging; + bool isDragging{false}; + bool isMouseDownWithoutDrag{false}; + bool hasStartedPreview{false}; + void mouseDown(const juce::MouseEvent &event) override { + isMouseDownWithoutDrag = true; + if (browserPane->autoPreviewEnabled && isFile()) + { + juce::Timer::callAfterDelay(500, [w = juce::Component::SafePointer(this)]() { + if (!w) + return; + if (!w->isMouseDownWithoutDrag) + return; + + const auto &data = w->browserPane->devicesPane->driveFSArea->contents; + const auto &entry = data[w->rowNumber]; + if (browser::Browser::isLoadableSingleSample(entry.path())) + { + w->hasStartedPreview = true; + namespace cmsg = scxt::messaging::client; + scxt::messaging::client::clientSendToSerialization( + cmsg::PreviewBrowserSample({true, data[w->rowNumber].path().u8string()}), + w->browserPane->editor->msgCont); + w->repaint(); + } + }); + } + enclosingBox()->selectRowsBasedOnModifierKeys(rowNumber, event.mods, false); if (event.mods.isPopupMenu()) @@ -309,6 +336,9 @@ struct DriveFSListBoxRow : public juce::Component if (!isFile()) return; + isMouseDownWithoutDrag = false; + + stopPreview(); if (!isDragging && e.getDistanceFromDragStart() > 1.5f) { if (auto *container = juce::DragAndDropContainer::findParentDragContainerFor(this)) @@ -325,6 +355,8 @@ struct DriveFSListBoxRow : public juce::Component void mouseUp(const juce::MouseEvent &event) override { + isMouseDownWithoutDrag = false; + stopPreview(); if (isDragging) { isDragging = false; @@ -334,8 +366,23 @@ struct DriveFSListBoxRow : public juce::Component enclosingBox()->selectRowsBasedOnModifierKeys(rowNumber, event.mods, true); } } + + void stopPreview() + { + if (hasStartedPreview) + { + hasStartedPreview = false; + repaint(); + namespace cmsg = scxt::messaging::client; + + scxt::messaging::client::clientSendToSerialization( + cmsg::PreviewBrowserSample({false, ""}), browserPane->editor->msgCont); + } + } + void mouseDoubleClick(const juce::MouseEvent &event) override { + isMouseDownWithoutDrag = false; const auto &data = browserPane->devicesPane->driveFSArea->contents; if (rowNumber >= 0 && rowNumber < data.size()) { @@ -422,6 +469,13 @@ struct DriveFSListBoxRow : public juce::Component jcmp::GlyphPainter::paintGlyph(g, r, jcmp::GlyphPainter::TUNING, textColor.withAlpha(0.5f)); } + + if (hasStartedPreview) + { + auto q = r.translated(getWidth() - r.getHeight() - 2, 0); + jcmp::GlyphPainter::paintGlyph(g, q, jcmp::GlyphPainter::SPEAKER, + textColor.withAlpha(0.5f)); + } g.setColour(textColor); g.drawText(entry.path().filename().u8string(), 14, 1, width - 16, height - 2, juce::Justification::centredLeft); @@ -476,6 +530,32 @@ struct SearchPane : TempPane SearchPane(SCXTEditor *e) : TempPane(e, "Search") {} }; +struct BrowserPaneFooter : HasEditor, juce::Component +{ + BrowserPane *parent{nullptr}; + std::unique_ptr autoPreview; + std::unique_ptr preview; + std::unique_ptr previewLevel; + std::unique_ptr autoPreviewAtt; + + BrowserPaneFooter(SCXTEditor *e, BrowserPane *p) : HasEditor(e), parent(p) + { + autoPreview = std::make_unique(); + autoPreview->setDrawMode(sst::jucegui::components::ToggleButton::DrawMode::GLYPH_WITH_BG); + autoPreview->setGlyph(sst::jucegui::components::GlyphPainter::SPEAKER); + autoPreviewAtt = std::make_unique( + [](auto) {}, parent->autoPreviewEnabled); + autoPreview->setSource(autoPreviewAtt.get()); + addAndMakeVisible(*autoPreview); + } + void resized() override + { + auto r = getLocalBounds(); + autoPreview->setBounds(r.withWidth(r.getHeight())); + r = r.translated(r.getHeight() + 2, 0); + } +}; + struct sfData : sst::jucegui::data::Discrete { BrowserPane *browserPane{nullptr}; @@ -500,10 +580,10 @@ struct sfData : sst::jucegui::data::Discrete switch (i) { case 0: - return "DEVICES"; + return "LOCATIONS"; + // case 1: + // return "FAVORITES"; case 1: - return "FAVORITES"; - case 2: return "SEARCH"; } } @@ -512,7 +592,7 @@ struct sfData : sst::jucegui::data::Discrete return "-error-"; } - int getMax() const override { return 2; } + int getMax() const override { return 1; } }; BrowserPane::BrowserPane(SCXTEditor *e) @@ -528,11 +608,14 @@ BrowserPane::BrowserPane(SCXTEditor *e) devicesPane = std::make_unique(this); addChildComponent(*devicesPane); - favoritesPane = std::make_unique(e); - addChildComponent(*favoritesPane); + // favoritesPane = std::make_unique(e); + // addChildComponent(*favoritesPane); searchPane = std::make_unique(e); addChildComponent(*searchPane); + footerArea = std::make_unique(e, this); + addAndMakeVisible(*footerArea); + selectPane(selectedPane); resetRoots(); @@ -543,13 +626,15 @@ BrowserPane::~BrowserPane() { selectedFunction->setSource(nullptr); } void BrowserPane::resized() { auto r = getContentArea(); - auto sel = r.withHeight(22); - auto ct = r.withTrimmedTop(25); + auto sel = r.withHeight(18); + auto ct = r.withTrimmedTop(21).withTrimmedBottom(23); + auto ft = r.withTop(r.getBottom() - 21).withTrimmedBottom(2); selectedFunction->setBounds(sel); devicesPane->setBounds(ct); - favoritesPane->setBounds(ct); + // favoritesPane->setBounds(ct); searchPane->setBounds(ct); + footerArea->setBounds(ft); } void BrowserPane::resetRoots() @@ -563,10 +648,12 @@ void BrowserPane::resetRoots() void BrowserPane::selectPane(int i) { selectedPane = i; + if (selectedPane < 0 || selectedPane > 1) + selectedPane = 0; - devicesPane->setVisible(i == 0); - favoritesPane->setVisible(i == 1); - searchPane->setVisible(i == 2); + devicesPane->setVisible(selectedPane == 0); + // favoritesPane->setVisible(i == 1); + searchPane->setVisible(selectedPane == 1); repaint(); } } // namespace scxt::ui::app::browser_ui diff --git a/src-ui/app/browser-ui/BrowserPane.h b/src-ui/app/browser-ui/BrowserPane.h index 6a1998ec..ccc3b3d9 100644 --- a/src-ui/app/browser-ui/BrowserPane.h +++ b/src-ui/app/browser-ui/BrowserPane.h @@ -42,6 +42,7 @@ namespace scxt::ui::app::browser_ui // a bit clumsy to distinguis from scxt::bro struct DevicesPane; struct FavoritesPane; struct SearchPane; +struct BrowserPaneFooter; struct BrowserPane : public app::HasEditor, sst::jucegui::components::NamedPanel { std::unique_ptr selectedFunction; @@ -57,9 +58,12 @@ struct BrowserPane : public app::HasEditor, sst::jucegui::components::NamedPanel std::unique_ptr devicesPane; std::unique_ptr favoritesPane; std::unique_ptr searchPane; + std::unique_ptr footerArea; void selectPane(int); int selectedPane{0}; + + bool autoPreviewEnabled{true}; }; } // namespace scxt::ui::app::browser_ui diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fdd28022..4f640a70 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -52,6 +52,7 @@ add_library(${PROJECT_NAME} STATIC selection/selection_manager.cpp voice/voice.cpp + voice/preview_voice.cpp patch_io/patch_io.cpp diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index ba6e7389..7bfa78c8 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -57,6 +57,7 @@ #include "feature_enums.h" #include "missing_resolution.h" #include "sst/voicemanager/midi1_to_voicemanager.h" +#include "voice/preview_voice.h" namespace scxt::engine { @@ -133,6 +134,8 @@ Engine::Engine() { ep = std::make_unique(nullptr); } + + previewVoice = std::make_unique(); } Engine::~Engine() @@ -342,6 +345,15 @@ bool Engine::processAudio() getPatch()->process(*this); + if (previewVoice->isActive) + { + previewVoice->processBlock(); + + auto &main = getPatch()->busses.mainBus.output; + mech::accumulate_from_to(previewVoice->output[0], main[0]); + mech::accumulate_from_to(previewVoice->output[1], main[1]); + } + auto &bl = sharedUIMemoryState.busVULevels; const auto &bs = getPatch()->busses; for (int c = 0; c < 2; ++c) @@ -928,6 +940,7 @@ void Engine::loadSf2MultiSampleIntoSelectedPart(const fs::path &p) void Engine::onSampleRateChanged() { patch->setSampleRate(sampleRate); + previewVoice->setSampleRate(sampleRate); messageController->forceStatusUpdate = true; } diff --git a/src/engine/engine.h b/src/engine/engine.h index 3a79f4ad..ff29287e 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -62,7 +62,8 @@ namespace scxt::voice { struct Voice; -} +struct PreviewVoice; +} // namespace scxt::voice namespace scxt::messaging { struct MessageController; @@ -251,6 +252,8 @@ struct Engine : MoveableOnly, SampleRateSupport } monoVoiceManagerResponder{*this}; using voiceManager_t = sst::voicemanager::VoiceManager; + static_assert(sst::voicemanager::constraints::ConstraintsChecker< + VMConfig, VoiceManagerResponder, MonoVoiceManagerResponder>::satisfies()); voiceManager_t voiceManager{voiceManagerResponder, monoVoiceManagerResponder}; void onSampleRateChanged() override; @@ -270,6 +273,8 @@ struct Engine : MoveableOnly, SampleRateSupport void assertActiveVoiceCount(); std::atomic activeVoices{0}; + std::unique_ptr previewVoice; + const std::unique_ptr &getMessageController() const { return messageController; diff --git a/src/engine/group.cpp b/src/engine/group.cpp index 5772a6d7..e8c9b6af 100644 --- a/src/engine/group.cpp +++ b/src/engine/group.cpp @@ -402,16 +402,17 @@ void Group::postZoneTraversalRemoveHandler() /* * Go backwards down the weak refs removing inactive ones */ - assert(rescanWeakRefs); - assert(activeZones); assert(activeZones >= rescanWeakRefs); - for (int i = activeZones - 1; i >= 0; --i) + if (activeZones != 0) { - if (!activeZoneWeakRefs[i]->isActive()) + for (int i = activeZones - 1; i >= 0; --i) { - rescanWeakRefs--; - activeZoneWeakRefs[i] = activeZoneWeakRefs[activeZones - 1]; - activeZones--; + if (!activeZoneWeakRefs[i]->isActive()) + { + rescanWeakRefs--; + activeZoneWeakRefs[i] = activeZoneWeakRefs[activeZones - 1]; + activeZones--; + } } } assert(rescanWeakRefs == 0); diff --git a/src/messaging/client/browser_messages.h b/src/messaging/client/browser_messages.h index 4bd10f1d..d0db0986 100644 --- a/src/messaging/client/browser_messages.h +++ b/src/messaging/client/browser_messages.h @@ -35,6 +35,7 @@ #include "client_macros.h" #include "filesystem/import.h" #include "browser/browser.h" +#include "voice/preview_voice.h" namespace scxt::messaging::client { @@ -48,6 +49,38 @@ inline void doAddBrowserDeviceLocation(const fs::path &p, const engine::Engine & CLIENT_TO_SERIAL(AddBrowserDeviceLocation, c2s_add_browser_device_location, std::string, doAddBrowserDeviceLocation(fs::path(fs::u8path(payload)), engine, cont)); +using previewBrowserSamplePayload_t = std::tuple; +inline void doPreviewBrowserSample(const previewBrowserSamplePayload_t &p, + const engine::Engine &engine, MessageController &cont) +{ + auto [startstop, pathString] = p; + auto path = fs::path(fs::u8path(pathString)); + if (startstop) + { + SCLOG("Starting preview " << path.u8string()); + auto sid = engine.getSampleManager()->loadSampleByPath(path); + if (sid.has_value()) + { + auto smp = engine.getSampleManager()->getSample(*sid); + cont.scheduleAudioThreadCallback( + [smp](auto &eng) { eng.previewVoice->attachAndStart(smp); }); + } + else + { + cont.reportErrorToClient("Unable to launch preview", + "Sample file preview load failed for " + path.u8string()); + } + } + else + { + cont.scheduleAudioThreadCallback( + [](auto &eng) { eng.previewVoice->detatchAndStop(); }, + [](const auto &e) { e.getSampleManager()->purgeUnreferencedSamples(); }); + } +} +CLIENT_TO_SERIAL(PreviewBrowserSample, c2s_preview_browser_sample, previewBrowserSamplePayload_t, + doPreviewBrowserSample(payload, engine, cont)); + SERIAL_TO_CLIENT(RefreshBrowser, s2c_refresh_browser, bool, onBrowserRefresh) } // namespace scxt::messaging::client diff --git a/src/messaging/client/client_serial.h b/src/messaging/client/client_serial.h index aef7eb7f..038c7b31 100644 --- a/src/messaging/client/client_serial.h +++ b/src/messaging/client/client_serial.h @@ -141,7 +141,9 @@ enum ClientToSerializationMessagesIds c2s_set_mixer_effect_storage, c2s_set_mixer_send_storage, + // Browser functions c2s_add_browser_device_location, + c2s_preview_browser_sample, c2s_request_debug_action, diff --git a/src/voice/preview_voice.cpp b/src/voice/preview_voice.cpp new file mode 100644 index 00000000..cf95daa4 --- /dev/null +++ b/src/voice/preview_voice.cpp @@ -0,0 +1,121 @@ +/* + * Shortcircuit XT - a Surge Synth Team product + * + * A fully featured creative sampler, available as a standalone + * and plugin for multiple platforms. + * + * Copyright 2019 - 2024, Various authors, as described in the github + * transaction log. + * + * ShortcircuitXT is released under the Gnu General Public Licence + * V3 or later (GPL-3.0-or-later). The license is found in the file + * "LICENSE" in the root of this repository or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * Individual sections of code which comprises ShortcircuitXT in this + * repository may also be used under an MIT license. Please see the + * section "Licensing" in "README.md" for details. + * + * ShortcircuitXT is inspired by, and shares code with, the + * commercial product Shortcircuit 1 and 2, released by VemberTech + * in the mid 2000s. The code for Shortcircuit 2 was opensourced in + * 2020 at the outset of this project. + * + * All source for ShortcircuitXT is available at + * https://github.com/surge-synthesizer/shortcircuit-xt + */ + +#include "sst/basic-blocks/mechanics/block-ops.h" +#include "preview_voice.h" +#include "dsp/generator.h" + +namespace mech = sst::basic_blocks::mechanics; + +namespace scxt::voice +{ +struct PreviewVoice::Details +{ + dsp::GeneratorState GD; + dsp::GeneratorIO GDIO; + dsp::GeneratorFPtr Generator; + + PreviewVoice *parent{nullptr}; + Details(PreviewVoice *p) : parent(p) {} + std::shared_ptr sample; + + void initiateGD() + { + GDIO.outputL = parent->output[0]; + GDIO.outputR = parent->output[1]; + if (sample->bitDepth == sample::Sample::BD_I16) + { + GDIO.sampleDataL = sample->GetSamplePtrI16(0); + GDIO.sampleDataR = sample->GetSamplePtrI16(1); + } + else if (sample->bitDepth == sample::Sample::BD_F32) + { + GDIO.sampleDataL = sample->GetSamplePtrF32(0); + GDIO.sampleDataR = sample->GetSamplePtrF32(1); + } + else + { + assert(false); + } + GDIO.waveSize = sample->sample_length; + + GD.samplePos = 0; + GD.sampleSubPos = 0; + GD.loopLowerBound = 0; + GD.loopUpperBound = sample->sample_length; + GD.loopFade = 0; + GD.playbackLowerBound = 0; + GD.playbackUpperBound = sample->sample_length; + GD.direction = 1; + GD.isFinished = false; + GD.directionAtOutset = GD.direction; + GD.gated = true; + + GD.ratio = (int32_t)((double)(1 << 24) * sample->sample_rate * parent->samplerate_inv); + + Generator = dsp::GetFPtrGeneratorSample( + sample->channels != 1, sample->bitDepth == sample::Sample::BD_F32, false, false, false); + assert(Generator); + } +}; + +PreviewVoice::PreviewVoice() { details = std::make_unique
(this); } +PreviewVoice::~PreviewVoice() {} + +bool PreviewVoice::attachAndStart(const std::shared_ptr &s) +{ + SCLOG("Starting Preview : " << s->mFileName.u8string()); + details->sample = s; + details->initiateGD(); + isActive = true; + return true; +} +bool PreviewVoice::detatchAndStop() +{ + details->sample = nullptr; + // TODO : Fade + isActive = false; + return true; +} + +void PreviewVoice::processBlock() +{ + if (details->GD.isFinished) + { + detatchAndStop(); + return; + } + assert(details->Generator); + details->Generator(&details->GD, &details->GDIO); + if (details->sample->channels == 1) + { + mech::copy_from_to(output[0], output[1]); + } + // SCLOG(output[0][0] << " " << output[1][0]); +} + +} // namespace scxt::voice \ No newline at end of file diff --git a/src/voice/preview_voice.h b/src/voice/preview_voice.h new file mode 100644 index 00000000..4d783a9a --- /dev/null +++ b/src/voice/preview_voice.h @@ -0,0 +1,56 @@ +/* + * Shortcircuit XT - a Surge Synth Team product + * + * A fully featured creative sampler, available as a standalone + * and plugin for multiple platforms. + * + * Copyright 2019 - 2024, Various authors, as described in the github + * transaction log. + * + * ShortcircuitXT is released under the Gnu General Public Licence + * V3 or later (GPL-3.0-or-later). The license is found in the file + * "LICENSE" in the root of this repository or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * Individual sections of code which comprises ShortcircuitXT in this + * repository may also be used under an MIT license. Please see the + * section "Licensing" in "README.md" for details. + * + * ShortcircuitXT is inspired by, and shares code with, the + * commercial product Shortcircuit 1 and 2, released by VemberTech + * in the mid 2000s. The code for Shortcircuit 2 was opensourced in + * 2020 at the outset of this project. + * + * All source for ShortcircuitXT is available at + * https://github.com/surge-synthesizer/shortcircuit-xt + */ + +#ifndef SCXT_SRC_VOICE_PREVIEW_VOICE_H +#define SCXT_SRC_VOICE_PREVIEW_VOICE_H + +#include + +#include "utils.h" +#include "configuration.h" +#include "sample/sample.h" + +namespace scxt::voice +{ +struct PreviewVoice : SampleRateSupport +{ + float output alignas(16)[2][blockSize]; + + struct Details; + PreviewVoice(); + ~PreviewVoice(); + + bool attachAndStart(const std::shared_ptr &); + void processBlock(); + bool detatchAndStop(); + + bool isActive{false}; + std::unique_ptr
details; +}; +} // namespace scxt::voice + +#endif // PREVIEW_VOICE_H