diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9dd96fe..ee5698e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,11 +11,12 @@ jobs: fail-fast: false matrix: os: [windows-latest, macos-latest] + edition: [full, lite] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Configure - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" "-DKSP_ENABLE_VST_HOST=${{ matrix.edition == 'full' && 'ON' || 'OFF' }}" - name: Build run: cmake --build build --config Release --parallel - name: Tests diff --git a/App/AppFolders.h b/App/AppFolders.h new file mode 100644 index 0000000..8fb6e13 --- /dev/null +++ b/App/AppFolders.h @@ -0,0 +1,18 @@ +/* + AppFolders.h + Where the app keeps its own files: Documents/KeyStepPro Studio (the library, backups and the like). +*/ + +#pragma once + +#include + +namespace ksp +{ + +inline juce::File dataFolder() +{ + return juce::File::getSpecialLocation (juce::File::userDocumentsDirectory).getChildFile ("KeyStepPro Studio"); +} + +} // namespace ksp diff --git a/App/LibraryWindow.h b/App/LibraryWindow.h index 68111bc..31d8f80 100644 --- a/App/LibraryWindow.h +++ b/App/LibraryWindow.h @@ -33,7 +33,9 @@ class LibraryPanel : public juce::Component, private juce::TableListBoxModel h.addColumn ("Name", 1, 240, 100, 600); h.addColumn ("Changed", 2, 140, 80, 220); h.addColumn ("KeyStep project", 3, 270, 100, 500); +#if KSP_ENABLE_VST_HOST h.addColumn ("Instruments", 4, 200, 80, 500); +#endif h.setStretchToFitActive (true); openButton.setButtonText ("Open"); @@ -140,17 +142,20 @@ class LibraryPanel : public juce::Component, private juce::TableListBoxModel text = juce::String (e.modified).replaceCharacter ('T', ' ').upToFirstOccurrenceOf ("Z", false, false); else if (column == 3) text = e.summary; +#if KSP_ENABLE_VST_HOST else { for (const auto& n : e.instruments) text += (text.isEmpty() ? "" : ", ") + juce::String (n); } +#endif g.setColour (ksp::ui::palette::ink); g.setFont (14.0f); g.drawText (text, 4, 0, w - 8, h, juce::Justification::centredLeft, true); } void selectedRowsChanged (int) override { updateButtons(); } + void returnKeyPressed (int) override { openSelected(); } void cellDoubleClicked (int row, int, const juce::MouseEvent&) override { table.selectRow (row); diff --git a/App/Main.cpp b/App/Main.cpp index 0fc2576..73ff1d7 100644 --- a/App/Main.cpp +++ b/App/Main.cpp @@ -8,21 +8,25 @@ #include #include +#include "AppFolders.h" +#include "Mix/Transport.h" +#include +#if KSP_ENABLE_VST_HOST #include "Host/PluginList.h" #include #include "Host/Engine.h" -#include "KspStyle.h" #include "FxWindow.h" +#include "PluginTable.h" +#endif +#include "KspStyle.h" #include "LibraryWindow.h" #include "GeneratorWindow.h" #include "DrumGrid.h" #include "ProjectMapWindow.h" #include "Gen/Arrangement.h" #include "MidiMonitor.h" -#include "PluginTable.h" #include "Studio/Library.h" #include "Ksp/Device.h" -#include "Model/Overdub.h" #include "Model/Pattern.h" #include "Model/NoteMove.h" #include "Model/PatternOps.h" @@ -32,6 +36,7 @@ namespace { // The KeyStep's memory is a volatile working copy of the library: all 16 slots can be loaded (with a backup and a warning first) +constexpr int kTracks = 4; constexpr int kFirstWritableSlot = 1, kLastWritableSlot = 16; constexpr int kRows = 24, kCellW = 18, kCellH = 14, kKeyW = 44, kRulerH = 18; @@ -1036,9 +1041,9 @@ class MainComponent : public juce::Component, private juce::Timer public: MainComponent() { - for (auto* b : { &openButton, &readButton, &syncButton, &octDown, &octUp, ©Button, &clearButton, &opsButton, &recButton, &undoTakeButton, &toolsButton, &libraryButton, &saveButton, &saveAsButton }) + for (auto* b : { &openButton, &readButton, &syncButton, &octDown, &octUp, ©Button, &clearButton, &opsButton, &toolsButton, &libraryButton, &saveButton, &saveAsButton }) addAndMakeVisible (*b); - for (auto* c : { &projectBox, &patternBox, &lengthBox, &rootBox, &scaleBox, ©TrackBox, ©PatBox, &recChannelBox, &stepBox }) + for (auto* c : { &projectBox, &patternBox, &lengthBox, &rootBox, &scaleBox, ©TrackBox, ©PatBox }) addAndMakeVisible (*c); for (auto* l : { &status, &octLabel, §ionLabel }) addAndMakeVisible (*l); @@ -1069,8 +1074,6 @@ class MainComponent : public juce::Component, private juce::Timer libraryButton.setButtonText ("Library..."); saveButton.setButtonText ("Save"); saveAsButton.setButtonText ("Save as..."); - recButton.setButtonText ("Rec"); - undoTakeButton.setButtonText ("Undo take"); sectionLabel.setText ("Project / Track / Pattern / Length / Root / Scale", juce::dontSendNotification); sectionLabel.setFont (juce::FontOptions (11.0f)); @@ -1081,11 +1084,6 @@ class MainComponent : public juce::Component, private juce::Timer for (int sc : { 0, 1, 2, 3, 4, 5, 6, 8, 9 }) scaleBox.addItem (ksp::scaleName (sc), sc + 1); for (int t = 1; t <= 4; ++t) copyTrackBox.addItem ("Track " + juce::String (t), t); for (int n = 1; n <= 16; ++n) copyPatBox.addItem ("Pattern " + juce::String (n), n); - for (int c = 1; c <= 16; ++c) recChannelBox.addItem ("Ch " + juce::String (c), c); - for (auto [name, clocks] : { std::pair { "1/4", 24 }, { "1/8", 12 }, { "1/8T", 8 }, { "1/16", 6 }, { "1/16T", 4 }, { "1/32", 3 } }) - stepBox.addItem (name, clocks); - recChannelBox.setSelectedId (1, juce::dontSendNotification); - stepBox.setSelectedId (6, juce::dontSendNotification); copyTrackBox.setSelectedId (1, juce::dontSendNotification); copyPatBox.setSelectedId (2, juce::dontSendNotification); projectBox.setSelectedId (1, juce::dontSendNotification); @@ -1139,13 +1137,11 @@ class MainComponent : public juce::Component, private juce::Timer libraryButton.onClick = [this] { showLibrary(); }; saveButton.onClick = [this] { saveToLibrary (false); }; saveAsButton.onClick = [this] { saveToLibrary (true); }; - recButton.onClick = [this] { if (overdub) stopRecording(); else startRecording(); }; - undoTakeButton.onClick = [this] { undoTake(); }; octDown.onClick = [this] { lowPitch = std::max (0, lowPitch - 12); showPattern(); }; octUp.onClick = [this] { lowPitch = std::min (96, lowPitch + 12); showPattern(); }; projectBox.onChange = [this] { selectProject(); }; trackTabs.onSelect = [this] (int t) { trackBox.setSelectedId (t, juce::sendNotification); }; - trackBox.onChange = [this] { trackTabs.setSelected (trackBox.getSelectedId(), false); grid.setTrack (track() - 1); refreshCards(); loadPattern(); recChannelBox.setSelectedId (engine.channel (track() - 1), juce::dontSendNotification); }; + trackBox.onChange = [this] { trackTabs.setSelected (trackBox.getSelectedId(), false); grid.setTrack (track() - 1); refreshCards(); loadPattern(); }; patternBox.onChange = [this] { loadPattern(); }; lengthBox.onChange = [this] { const int n = lengthBox.getSelectedId(); @@ -1166,13 +1162,21 @@ class MainComponent : public juce::Component, private juce::Timer rootBox.onChange = [this] { pattern.root = rootBox.getSelectedId() - 1; markEdited(); }; scaleBox.onChange = [this] { pattern.scale = scaleBox.getSelectedId() - 1; markEdited(); }; grid.onEdited = [this] { markEdited(); }; +#if KSP_ENABLE_VST_HOST grid.onNoteAdded = [this] (int pitch, int velocity) { auditionNote (pitch, velocity); }; +#endif grid.onLastStep = [this] (int n) { lengthBox.setSelectedId (n, juce::sendNotification); }; drumGrid.onEdited = [this] { markEdited(); }; +#if KSP_ENABLE_VST_HOST drumGrid.onHit = [this] (int note, int velocity) { auditionNote (note, velocity); }; +#endif drumGrid.onLastStep = [this] (int n) { lengthBox.setSelectedId (n, juce::sendNotification); }; drumGrid.onMessage = [this] (const juce::String& m) { say (m); }; +#if KSP_ENABLE_VST_HOST buildInstrumentRow(); +#else + buildCardRow(); +#endif setSize (1220, 756); buildRecordRow(); setSize (1220, 748); @@ -1182,25 +1186,33 @@ class MainComponent : public juce::Component, private juce::Timer ~MainComponent() override { stopTimer(); +#if KSP_ENABLE_VST_HOST stopScan->store (true); +#endif if (device != nullptr) device->setListener (nullptr); - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) +#if KSP_ENABLE_VST_HOST + for (int t = 0; t < kTracks; ++t) closeEditor (t); +#endif mapWindow.reset(); generatorWindow.reset(); monitorWindow.reset(); +#if KSP_ENABLE_VST_HOST pluginWindow.reset(); +#endif libraryWindow.reset(); +#if KSP_ENABLE_VST_HOST for (int p = 0; p < ksp::host::Engine::kFxPositions; ++p) fxEditorWindow[static_cast (p)].reset(); fxWindow.reset(); +#endif } /** The track cards: a body in the full track colour with a border; the selected track is bright, the others dimmed. */ void paint (juce::Graphics& g) override { - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) { const auto i = static_cast (t); const auto c = ksp::ui::palette::trackColour (t); @@ -1248,10 +1260,11 @@ class MainComponent : public juce::Component, private juce::Timer arpSwingSlider[i].setEnabled (a.valid && a.on); } arpUpdating = false; - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) { const auto i = static_cast (t); const float a = t == track() - 1 ? 1.0f : 0.75f; +#if KSP_ENABLE_VST_HOST for (juce::Component* c : { static_cast (&instLabel[i]), static_cast (&instBox[i]), static_cast (&editButton[i]), static_cast (&muteToggle[i]), static_cast (&soloToggle[i]), static_cast (&volSlider[i]), static_cast (&dbLabel[i]), @@ -1261,6 +1274,11 @@ class MainComponent : public juce::Component, private juce::Timer const double v = volSlider[i].getValue(); dbLabel[i].setText (v <= 0.001 ? juce::String ("-inf") : juce::String (20.0 * std::log10 (v), 1) + " dB", juce::dontSendNotification); volSlider[i].setTooltip ("Track volume: " + dbLabel[i].getText()); +#else + for (juce::Component* c : { static_cast (&instLabel[i]), static_cast (&seqToggle[i]), + static_cast (&modeToggle[i]) }) + c->setAlpha (a); +#endif } repaint(); } @@ -1268,7 +1286,7 @@ class MainComponent : public juce::Component, private juce::Timer int cardAt (const juce::MouseEvent& e) { const auto pos = e.getEventRelativeTo (this).getPosition(); - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) if (cardRect[static_cast (t)].contains (pos)) return t + 1; return 0; @@ -1317,7 +1335,9 @@ class MainComponent : public juce::Component, private juce::Timer top.removeFromLeft (6); mapButton.setBounds (top.removeFromLeft (110)); top.removeFromLeft (6); +#if KSP_ENABLE_VST_HOST fxButton.setBounds (top.removeFromLeft (60)); +#endif r.removeFromTop (6); auto row = r.removeFromTop (28); for (auto* c : { &patternBox, &lengthBox, &rootBox, &scaleBox }) @@ -1343,26 +1363,19 @@ class MainComponent : public juce::Component, private juce::Timer lenDisplay.setBounds (row.removeFromRight (100)); r.removeFromTop (6); auto bottom = r.removeFromBottom (30); - recButton.setBounds (bottom.removeFromLeft (60)); - bottom.removeFromLeft (4); - recChannelBox.setBounds (bottom.removeFromLeft (76)); - bottom.removeFromLeft (4); - stepBox.setBounds (bottom.removeFromLeft (76)); - bottom.removeFromLeft (4); - undoTakeButton.setBounds (bottom.removeFromLeft (90)); - bottom.removeFromLeft (10); status.setBounds (bottom); r.removeFromBottom (6); // four cards in a row, one per track, in the track's colour; the audio readout at the right auto instArea = cardsRow; const int w = instArea.getWidth() / 4; - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) { const auto i = static_cast (t); cardRect[i] = instArea.removeFromLeft (w).withTrimmedRight (8); auto cell = cardRect[i].reduced (8, 6); auto line1 = cell.removeFromTop (26); cell.removeFromTop (4); +#if KSP_ENABLE_VST_HOST instLabel[i].setBounds (line1.removeFromLeft (26)); line1.removeFromLeft (6); instBox[i].setBounds (line1); @@ -1382,6 +1395,14 @@ class MainComponent : public juce::Component, private juce::Timer modeToggle[i].setBounds (colB.removeFromTop (30)); muteToggle[i].setBounds (colA.removeFromBottom (30)); soloToggle[i].setBounds (colB.removeFromBottom (30)); +#else + instLabel[i].setBounds (line1.removeFromLeft (26)); + line1.removeFromLeft (6); + const int kw = (line1.getWidth() - 4) / 2; + seqToggle[i].setBounds (line1.removeFromLeft (kw)); + line1.removeFromLeft (4); + modeToggle[i].setBounds (line1); +#endif if (t > 0) { cell.removeFromTop (6); @@ -1399,8 +1420,10 @@ class MainComponent : public juce::Component, private juce::Timer r.removeFromBottom (6); auto recRow = r.removeFromBottom (28); transportLabel.setBounds (recRow.removeFromLeft (330)); +#if KSP_ENABLE_VST_HOST meterLabel.setBounds (recRow.removeFromLeft (260)); recStatusLabel.setBounds (recRow); +#endif r.removeFromBottom (6); auto knobRow = r.removeFromBottom (86); r.removeFromBottom (4); @@ -1424,6 +1447,7 @@ class MainComponent : public juce::Component, private juce::Timer std::unique_ptr device; std::optional backup; std::atomic busy { false }; +#if KSP_ENABLE_VST_HOST std::shared_ptr plugins = std::make_shared(); std::shared_ptr> stopScan = std::make_shared> (false); std::atomic scanning { false }; @@ -1431,21 +1455,47 @@ class MainComponent : public juce::Component, private juce::Timer // ---- instruments: one hosted VST3 per track of the KeyStep, fed by the KeyStep's MIDI ksp::host::Engine engine; +#if KSP_ENABLE_VST_HOST std::vector instChoices; // what the boxes offer: ids 2.. are indexes + 2 +#endif +#else + // ---- the KeyStep's clock (Start / Stop / tempo) + mix::Transport transport; +#endif + std::array trackChannel { 1, 2, 3, 4 }; // the MIDI channel each track sends on std::array instLabel; +#if KSP_ENABLE_VST_HOST std::array instBox; +#endif +#if KSP_ENABLE_VST_HOST std::array editButton; +#endif +#if KSP_ENABLE_VST_HOST std::array muteToggle, soloToggle; +#endif +#if KSP_ENABLE_VST_HOST std::array volSlider; +#endif // arpeggiator of tracks 2-4 (index track - 2): on/off, mode and octave of the pattern shown, division and swing of the track std::array seqToggle, modeToggle; // Seq and Arp (Drum on track 1): the track mode, lit when on std::array arpModeBox, arpOctBox, arpDivBox; std::array arpSwingSlider; +#if KSP_ENABLE_VST_HOST std::array dbLabel; +#endif std::array, 4> cardRect; +#if KSP_ENABLE_VST_HOST static constexpr int cardHeight = 140; +#else + static constexpr int cardHeight = 80; // no instrument rows: track number, Seq / Arp keys and the arp settings +#endif +#if KSP_ENABLE_VST_HOST std::array missingName; // a plugin of the project that is not on this machine: kept untouched - juce::TextButton toolsButton, mapButton, fxButton; // FX is also in Tools; on the bar so the effects are easy to find +#endif + juce::TextButton toolsButton, mapButton; +#if KSP_ENABLE_VST_HOST + juce::TextButton fxButton; // FX is also in Tools; on the bar so the effects are easy to find +#endif ksp::ui::WandButton generateButton; // a magic wand on it: the way to new music std::unique_ptr generatorWindow; std::unique_ptr mapWindow; @@ -1453,33 +1503,33 @@ class MainComponent : public juce::Component, private juce::Timer std::optional beforeGenerate; // the project as it was before the last generation, for Undo std::unique_ptr monitorWindow; std::shared_ptr> monitorOn = std::make_shared> (false); +#if KSP_ENABLE_VST_HOST int recMode = 1, recTail = 3; // WAV recording (Tools menu): 1 mix, 2 stems, 3 both; the tail is an index + 1 into the tails bool recFollow = true; // start at the KeyStep's Start and end after its Stop plus the tail - juce::Label transportLabel, recStatusLabel; std::unique_ptr fxWindow; std::array, ksp::host::Engine::kFxPositions> fxEditorWindow; std::array, ksp::host::Engine::kFxPositions> missingFx; // plugins of the project not on this machine std::array fxBypass {}; - juce::Label meterLabel; - std::shared_ptr> recordingOn = std::make_shared> (false); + juce::Label recStatusLabel, meterLabel; +#endif + juce::Label transportLabel; +#if KSP_ENABLE_VST_HOST std::array, 4> editorWindow; +#endif std::shared_ptr library = std::make_shared ( - std::filesystem::path (ksp::host::dataFolder().getChildFile ("Library").getFullPathName().toStdString())); + std::filesystem::path (ksp::dataFolder().getChildFile ("Library").getFullPathName().toStdString())); std::unique_ptr libraryWindow; ksp::studio::StudioProject current; // the library project being edited (its plugin setup and name); current.ksp is not kept up to date bool haveStudio = false; // false: what is shown came from a backup or the KeyStep and is not in the library yet std::unique_ptr nameDialog, closeDialog; - std::optional overdub; // while Rec is on - ksp::model::Pattern takeBefore; // the pattern before the last take, for Undo take - bool haveTake = false; juce::TextButton clearButton, opsButton; - juce::TextButton libraryButton, saveButton, saveAsButton, copyButton, openButton, readButton, syncButton, octDown, octUp, recButton, undoTakeButton; + juce::TextButton libraryButton, saveButton, saveAsButton, copyButton, openButton, readButton, syncButton, octDown, octUp; ksp::ui::TrackTabs trackTabs; ksp::ui::SevenSegDisplay lenDisplay { 2 }, bpmDisplay { 5 }; BusyOverlay busyOverlay; juce::String fwReadOnly; // firmware version of a KeyStep that is connected but not tested: empty when writing is allowed - juce::ComboBox projectBox, trackBox, patternBox, lengthBox, rootBox, scaleBox, copyTrackBox, copyPatBox, recChannelBox, stepBox; + juce::ComboBox projectBox, trackBox, patternBox, lengthBox, rootBox, scaleBox, copyTrackBox, copyPatBox; juce::Label status, octLabel, sectionLabel; juce::Viewport viewport; PatternGrid grid; @@ -1506,8 +1556,6 @@ class MainComponent : public juce::Component, private juce::Timer for (juce::Component* c : { static_cast (&openButton), static_cast (&readButton) }) c->setEnabled (! b); copyButton.setEnabled (haveProject); - recButton.setEnabled (! b && haveProject); - undoTakeButton.setEnabled (haveTake && ! overdub); } void say (const juce::String& s) { status.setText (s, juce::dontSendNotification); } @@ -1554,21 +1602,25 @@ class MainComponent : public juce::Component, private juce::Timer { const bool drums = haveProject && ksp::model::isDrumMode (project); for (int t = 0; t < 4; ++t) - engine.setChannel (t, haveProject ? ksp::model::effectiveChannel (project, t, drums) : (t == 0 && drums ? 10 : t + 1)); + { + const int ch = haveProject ? ksp::model::effectiveChannel (project, t, drums) : (t == 0 && drums ? 10 : t + 1); + trackChannel[static_cast (t)] = ch; +#if KSP_ENABLE_VST_HOST + engine.setChannel (t, ch); +#endif + } } void loadPattern() { applyChannels(); - stopRecording(); - haveTake = false; - undoTakeButton.setEnabled (false); commitPattern(); refreshMap(); loadedTrack = track(); loadedPattern = patNo(); fillPatternBox(); setDrumView (haveProject && loadedTrack == 1 && ksp::model::isDrumMode (project)); + refreshCards(); // the Seq / Arp / Drum keys follow the project (the drum view returns below before the other refresh) if (! haveProject) return; if (drumView) @@ -1635,6 +1687,7 @@ class MainComponent : public juce::Component, private juce::Timer + " an octave (not written to the KeyStep yet)."); } +#if KSP_ENABLE_VST_HOST /** Scans the VST3 folders (one file at a time in KspVstHelper.exe) and keeps the list: instruments for the tracks, effects for their chains. Takes a few minutes the first time. */ void scanPlugins() @@ -1679,6 +1732,7 @@ class MainComponent : public juce::Component, private juce::Timer }); } +#endif // ---- library: the truth of the app. The KeyStep is only a volatile working copy of it. void showLibrary() { @@ -1691,7 +1745,12 @@ class MainComponent : public juce::Component, private juce::Timer juce::Component::SafePointer self (this); libraryWindow = std::make_unique ( library, - [self] (const std::string& id) { if (self != nullptr) self->openFromLibrary (id); }, + [self] (const std::string& id) { + if (self == nullptr) + return; + self->openFromLibrary (id); + juce::MessageManager::callAsync ([self] { if (self != nullptr) self->libraryWindow.reset(); }); // the project is in front: the window gets out of the way + }, [self] (const std::string& id) { if (self != nullptr && self->haveStudio && self->current.id == id) self->haveStudio = false; }, [self] { juce::MessageManager::callAsync ([self] { if (self != nullptr) self->libraryWindow.reset(); }); }); } @@ -1710,7 +1769,9 @@ class MainComponent : public juce::Component, private juce::Timer backup.reset(); setProject (current.ksp, "Library: " + juce::String (current.name)); haveStudio = true; // after setProject, which clears it +#if KSP_ENABLE_VST_HOST restoreTracks(); +#endif } /** Opens the most recently changed library project, if any (at start, so the app is not empty without a KeyStep). */ @@ -1816,7 +1877,9 @@ class MainComponent : public juce::Component, private juce::Timer { current.ksp = project; current.ksp.project = 1; +#if KSP_ENABLE_VST_HOST captureTracks(); +#endif const auto error = library->save (current); haveStudio = error.empty(); if (error.empty()) @@ -1827,10 +1890,11 @@ class MainComponent : public juce::Component, private juce::Timer return error.empty(); } +#if KSP_ENABLE_VST_HOST // ---- instruments void buildInstrumentRow() { - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) { const auto i = static_cast (t); for (juce::Component* c : { static_cast (&instLabel[i]), static_cast (&instBox[i]), @@ -1895,7 +1959,7 @@ class MainComponent : public juce::Component, private juce::Timer instChoices = plugins->instruments(); std::sort (instChoices.begin(), instChoices.end(), [] (const auto& a, const auto& b) { return a.name.compareIgnoreCase (b.name) < 0; }); - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) rebuildInstBox (t); } @@ -2011,9 +2075,47 @@ class MainComponent : public juce::Component, private juce::Timer }); } +#else + // ---- the four track cards: track number, Seq / Arp (Drum) keys and the arpeggiator settings of tracks 2-4 + void buildCardRow() + { + for (int t = 0; t < kTracks; ++t) + { + const auto i = static_cast (t); + for (juce::Component* c : { static_cast (&instLabel[i]), static_cast (&seqToggle[i]), + static_cast (&modeToggle[i]) }) + addAndMakeVisible (*c); + seqToggle[i].setButtonText ("Seq"); + modeToggle[i].setButtonText (t == 0 ? "Drum" : "Arp"); + seqToggle[i].setTooltip (t == 0 ? "Track 1 plays its sequencer" : "Track " + juce::String (t + 1) + " plays its sequencer"); + if (t == 0) + { + modeToggle[0].setTooltip ("Track 1 is a drum track (lit)"); + seqToggle[0].onClick = [this] { toggleDrumMode (0); refreshCards(); }; + modeToggle[0].onClick = [this] { toggleDrumMode (1); refreshCards(); }; + } + instLabel[i].setText (juce::String (t + 1), juce::dontSendNotification); + instLabel[i].setJustificationType (juce::Justification::centred); + instLabel[i].setColour (juce::Label::textColourId, juce::Colours::white); + instLabel[i].setFont (juce::FontOptions (15.0f, juce::Font::bold)); + instLabel[i].setInterceptsMouseClicks (false, false); + // a click anywhere on a card, its controls included, selects that track (mouseDown below) + for (juce::Component* c : { static_cast (&seqToggle[i]), static_cast (&modeToggle[i]) }) + c->addMouseListener (this, true); + if (t > 0) + for (juce::Component* c : { static_cast (&arpModeBox[i - 1]), static_cast (&arpOctBox[i - 1]), + static_cast (&arpDivBox[i - 1]), static_cast (&arpSwingSlider[i - 1]) }) + c->addMouseListener (this, true); + } + refreshCards(); + } + +#endif // ---- the KeyStep's transport and WAV recording - static juce::File recordingsFolder() { return ksp::host::dataFolder().getChildFile ("Recordings"); } +#if KSP_ENABLE_VST_HOST + static juce::File recordingsFolder() { return ksp::dataFolder().getChildFile ("Recordings"); } +#endif static juce::String formatTime (double seconds) { const int total = static_cast (seconds); @@ -2022,22 +2124,29 @@ class MainComponent : public juce::Component, private juce::Timer void buildRecordRow() { - for (juce::Component* c : { static_cast (&transportLabel), static_cast (&recStatusLabel) }) - addAndMakeVisible (*c); + addAndMakeVisible (transportLabel); +#if KSP_ENABLE_VST_HOST + addAndMakeVisible (recStatusLabel); +#endif addAndMakeVisible (generateButton); addAndMakeVisible (mapButton); mapButton.setButtonText ("Project Map"); mapButton.onClick = [this] { showMap(); }; +#if KSP_ENABLE_VST_HOST addAndMakeVisible (fxButton); fxButton.setButtonText ("FX..."); fxButton.onClick = [this] { showFx(); }; +#endif generateButton.setButtonText ("Generate..."); generateButton.onClick = [this] { showGenerator(); }; transportLabel.setText ("STOP", juce::dontSendNotification); transportLabel.setFont (juce::FontOptions (13.0f, juce::Font::bold)); +#if KSP_ENABLE_VST_HOST recStatusLabel.setFont (juce::FontOptions (12.0f)); +#endif } +#if KSP_ENABLE_VST_HOST /** The Tools menu: the extras that are not the KeyStep's own (plugins, audio, WAV recording, the MIDI monitor). */ void showToolsMenu() { @@ -2090,6 +2199,31 @@ class MainComponent : public juce::Component, private juce::Timer }); } +#else + /** The Tools menu: the MIDI monitor, the octave shift of a track and the manual. */ + void showToolsMenu() + { + enum { monitor = 1, down, up, manual }; + juce::PopupMenu menu; + menu.addItem (monitor, "MIDI monitor"); + menu.addSeparator(); + menu.addItem (down, "Track -8va", haveProject); + menu.addItem (up, "Track +8va", haveProject); + menu.addSeparator(); + menu.addItem (manual, "User manual (PDF)"); + menu.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (toolsButton), + [safe = juce::Component::SafePointer (this)] (int id) { + if (safe == nullptr || id == 0) + return; + auto& m = *safe; + if (id == monitor) m.showMonitor(); + else if (id == down) m.transposeCurrentTrack (-12); + else if (id == up) m.transposeCurrentTrack (12); + else if (id == manual) m.openManual(); + }); + } + +#endif /** Opens KeyStepPro-Studio-Manual.pdf (next to the exe, put there by the installer / portable zip) with the system viewer. */ void openManual() { @@ -2106,6 +2240,7 @@ class MainComponent : public juce::Component, private juce::Timer say ("Could not open the manual. Open it by hand: " + pdf.getFullPathName()); } +#if KSP_ENABLE_VST_HOST /** Arms the recording: with "Follow the KeyStep" it starts at the KeyStep's Start and ends after its Stop plus the tail. */ void startWavRecording() { @@ -2159,6 +2294,18 @@ class MainComponent : public juce::Component, private juce::Timer juce::dontSendNotification); } +#else + /** The KeyStep's clock: PLAY / STOP, position since Start, time and tempo. */ + void updateTransportUi() + { + const auto pos = mix::Transport::positionOf (transport.ticks()); + transportLabel.setText ((transport.playing() ? "PLAY " : "STOP ") + juce::String (pos.bar) + "." + juce::String (pos.beat) + "." + + juce::String (pos.sixteenth) + " " + formatTime (mix::Transport::secondsOf (transport.ticks(), transport.bpm())) + + " " + juce::String (transport.bpm(), 1) + " BPM", + juce::dontSendNotification); + } + +#endif /** The MIDI monitor: what the KeyStep sends while its controls are used. Needs the KeyStep's MIDI port open. */ void showMonitor() { @@ -2225,7 +2372,6 @@ class MainComponent : public juce::Component, private juce::Timer { if (! haveProject) return; - stopRecording(); commitPattern(); const auto before = project; const auto error = n != nullptr ? ksp::model::writeScene (project, sc, *n) : ksp::model::clearScene (project, sc); @@ -2248,7 +2394,6 @@ class MainComponent : public juce::Component, private juce::Timer { if (! haveProject) return; - stopRecording(); commitPattern(); const auto before = project; if (! ksp::model::writeSettings (project, bpm, swing)) @@ -2269,7 +2414,6 @@ class MainComponent : public juce::Component, private juce::Timer { if (! haveProject) return; - stopRecording(); commitPattern(); const auto before = project; if (! ksp::model::writeChannel (project, track, channel)) @@ -2333,7 +2477,6 @@ class MainComponent : public juce::Component, private juce::Timer say ("Open a project, a backup or read one from the KeyStep first: the generator writes into it."); return; } - stopRecording(); commitPattern(); edited = false; const auto before = project; @@ -2403,6 +2546,7 @@ class MainComponent : public juce::Component, private juce::Timer say ("Generation undone: the project is as it was."); } +#if KSP_ENABLE_VST_HOST void showAudioSettings() { if (const auto e = engine.start(); e.isNotEmpty()) @@ -2427,7 +2571,7 @@ class MainComponent : public juce::Component, private juce::Timer void testNotes() { juce::Component::SafePointer self (this); - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) if (engine.instrument (t) != nullptr) { engine.midiFromUi (juce::MidiMessage::noteOn (t + 1, 60, 0.8f)); @@ -2447,6 +2591,7 @@ class MainComponent : public juce::Component, private juce::Timer juce::Timer::callAfterDelay (300, [self, ch, pitch] { if (self != nullptr) self->engine.midiFromUi (juce::MidiMessage::noteOff (ch, pitch)); }); } +#endif /** Slow glow on the sync button while the project has edits not yet written to the KeyStep (rides the existing 10 Hz timer). */ void updateSyncUi() { @@ -2470,6 +2615,7 @@ class MainComponent : public juce::Component, private juce::Timer syncButton.repaint(); } +#if KSP_ENABLE_VST_HOST void timerCallback() override { updatePendingGlow(); @@ -2484,10 +2630,21 @@ class MainComponent : public juce::Component, private juce::Timer meterLabel.setText (text, juce::dontSendNotification); } - /** The KeyStep's MIDI goes to the instruments (lock-free) and, while Rec is on, to the overdub on the message thread. */ +#else + void timerCallback() override + { + updatePendingGlow(); + lenDisplay.setText (juce::String (drumView ? drum.length : pattern.length), ksp::ui::palette::trackColour (track() - 1).brighter (0.5f)); + bpmDisplay.setText (transport.hasTempo() ? juce::String (transport.bpm(), 1) : juce::String ("---")); + updateTransportUi(); + } + +#endif + /** The KeyStep's MIDI goes to the instruments (lock-free) and, while the MIDI monitor is open, to it on the message thread. */ +#if KSP_ENABLE_VST_HOST void installMidiListener() { - device->setListener ([eng = &engine, rec = recordingOn, mon = monitorOn, safe = juce::Component::SafePointer (this)] (const juce::MidiMessage& m) { + device->setListener ([eng = &engine, mon = monitorOn, safe = juce::Component::SafePointer (this)] (const juce::MidiMessage& m) { eng->midiFromDevice (m); if (mon->load()) { @@ -2497,22 +2654,32 @@ class MainComponent : public juce::Component, private juce::Timer safe->monitorWindow->content().add (bytes); }); } - if (rec->load()) + }); + } +#else + void installMidiListener() + { + device->setListener ([tr = &transport, mon = monitorOn, safe = juce::Component::SafePointer (this)] (const juce::MidiMessage& m) { + if (m.getRawDataSize() == 1 && m.getRawData()[0] >= 0xF8) + tr->message (m.getRawData()[0], juce::Time::getMillisecondCounterHiRes() * 0.001); + if (mon->load()) { const auto* raw = m.getRawData(); juce::MessageManager::callAsync ([safe, bytes = std::vector (raw, raw + m.getRawDataSize())] { - if (safe != nullptr) - safe->recordMidi (bytes); + if (safe != nullptr && safe->monitorWindow != nullptr) + safe->monitorWindow->content().add (bytes); }); } }); } +#endif +#if KSP_ENABLE_VST_HOST /** What the tracks hold now (instrument, its state, volume, mute) into the project being saved. A plugin that is missing on this machine keeps what the file had. */ void captureTracks() { - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) { const auto i = static_cast (t); auto& ts = current.tracks[i]; @@ -2531,7 +2698,7 @@ class MainComponent : public juce::Component, private juce::Timer /** Loads the instruments of the project that was just opened (a plugin that cannot be loaded stays as "Missing"). */ void restoreTracks() { - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) { const auto i = static_cast (t); const auto& ts = current.tracks[i]; @@ -2585,7 +2752,7 @@ class MainComponent : public juce::Component, private juce::Timer v.pop_back(); return v; }; - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) current.tracks[static_cast (t)].effects = chain (t * ksp::host::Engine::kInserts, ksp::host::Engine::kInserts); current.masterFx = chain (ksp::host::Engine::kMasterFxBase, ksp::host::Engine::kMasterFx); } @@ -2615,7 +2782,7 @@ class MainComponent : public juce::Component, private juce::Timer missingFx[i] = *ps; } }; - for (int t = 0; t < ksp::host::Engine::kTracks; ++t) + for (int t = 0; t < kTracks; ++t) for (int k = 0; k < ksp::host::Engine::kInserts; ++k) { const auto& v = current.tracks[static_cast (t)].effects; @@ -2731,6 +2898,7 @@ class MainComponent : public juce::Component, private juce::Timer }); } +#endif /** Empties the pattern shown: its notes, or its drum hits in drum view. Length and settings stay; it is a normal edit (written with the project, Undo of the generator does not cover it). */ void clearPattern() @@ -2850,7 +3018,7 @@ class MainComponent : public juce::Component, private juce::Timer viewport.setViewedComponent (on ? static_cast (&drumGrid) : static_cast (&grid), false); for (juce::Component* c : { static_cast (&rootBox), static_cast (&scaleBox), static_cast (&octDown), static_cast (&octUp), - static_cast (&octLabel), static_cast (&recButton), + static_cast (&octLabel), static_cast (©Button), static_cast (©TrackBox), static_cast (©PatBox) }) c->setEnabled (! on); @@ -2873,7 +3041,6 @@ class MainComponent : public juce::Component, private juce::Timer void setProject (ksp::ProjectData p, const juce::String& what) { - stopRecording(); beforeMapEdit.reset(); beforeGenerate.reset(); // Undo belongs to the project it was made on if (generatorWindow != nullptr) @@ -2963,7 +3130,11 @@ class MainComponent : public juce::Component, private juce::Timer post ([ro, fw] (MainComponent& m) { m.fwReadOnly = ro ? fw : juce::String(); m.say (ro ? "KeyStep Pro firmware " + fw + " is not tested: read only until it is (tested: 2.5.20)." +#if KSP_ENABLE_VST_HOST : juce::String ("KeyStep Pro connected: its notes play the instruments of the tracks.")); +#else + : juce::String ("KeyStep Pro connected: its clock and notes are monitored.")); +#endif }); } return device != nullptr; @@ -2983,7 +3154,6 @@ class MainComponent : public juce::Component, private juce::Timer void readFromDevice (int slot = 0) { const int p = slot > 0 ? slot : projectBox.getSelectedId(); - stopRecording(); work ([this, p] { if (! connect()) return; @@ -3007,7 +3177,6 @@ class MainComponent : public juce::Component, private juce::Timer void writeToSlot (int slot) { - stopRecording(); commitPattern(); edited = false; auto written = project; @@ -3049,66 +3218,6 @@ class MainComponent : public juce::Component, private juce::Timer }); } - // ---- overdub (MIDI from the KeyStep; handled on the message thread in arrival order) - void startRecording() - { - if (! haveProject || busy || ! connect()) - return; - takeBefore = pattern; - overdub.emplace (stepBox.getSelectedId(), recChannelBox.getSelectedId() - 1); - recordingOn->store (true); - recButton.setButtonText ("Stop rec"); - recButton.setColour (juce::TextButton::buttonColourId, ksp::ui::palette::rec); - recButton.setColour (juce::TextButton::textColourOffId, juce::Colours::white); - undoTakeButton.setEnabled (false); - say ("Recording on channel " + juce::String (recChannelBox.getSelectedId()) + ": press Play on the KeyStep and play over the pattern."); - } - - void recordMidi (const std::vector& m) - { - if (! overdub || m.empty()) - return; - const bool wasRunning = overdub->isRunning(); - if (overdub->handle (pattern, m.data(), m.size())) - { - edited = true; - noteChanged(); - grid.repaint(); - } - else if (overdub->isRunning() == wasRunning) - return; - say (juce::String (overdub->isRunning() ? "Recording: " : "Recording (KeyStep stopped): ") - + juce::String (overdub->added) + " note(s) added" - + (overdub->dropped > 0 ? ", " + juce::String (overdub->dropped) + " did not fit" : juce::String())); - } - - void stopRecording() - { - if (! overdub) - return; - recordingOn->store (false); - const int added = overdub->added, dropped = overdub->dropped; - overdub.reset(); - haveTake = added > 0; - recButton.setButtonText ("Rec"); - recButton.removeColour (juce::TextButton::buttonColourId); - recButton.removeColour (juce::TextButton::textColourOffId); - undoTakeButton.setEnabled (haveTake); - say ("Take: " + juce::String (added) + " note(s) added" + (dropped > 0 ? ", " + juce::String (dropped) + " did not fit" : juce::String()) - + (added > 0 ? " (not written yet)." : ".")); - } - - void undoTake() - { - if (! haveTake || overdub) - return; - pattern = takeBefore; - haveTake = false; - undoTakeButton.setEnabled (false); - showPattern(); - say ("Last take removed."); - } - int loadedTrack = 1, loadedPattern = 1; }; diff --git a/CLAUDE.md b/CLAUDE.md index 0c0ecfe..b48ba9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,9 @@ App JUCE 8 (Standalone, Windows y Mac) para el Arturia KeyStep Pro: editor de pr - No mencionar otros proyectos de Leo en el código ni en los docs (Leo, 2026-10-30): el repo es público y trata solo del KSP. - Testear lo justo (app personal): `protocol_test` y los tests sin JUCE de lo que se toca. +## Ediciones (Leo, 2026-11-04) +- Un solo repo, dos builds, las dos mantenidas: Lite (solo editor) es la edición PRINCIPAL y recomendada (Leo: el host de VST aleja la app de su propósito, editar proyectos visualmente); Full agrega VST3, audio y WAV. Opción CMake `KSP_ENABLE_VST_HOST` (OFF por defecto = Lite) y macro `KSP_ENABLE_VST_HOST` en App/Main.cpp, App/LibraryWindow.h. Lite no enlaza juce_audio_processors / audio_utils ni compila Source/Host. Todo lo que sea del editor, la sincronización y el MIDI va en las dos; el código de VST solo dentro de `#if KSP_ENABLE_VST_HOST`. Source/Mix/Transport.h (reloj del KSP) y App/AppFolders.h son comunes. Instaladores: `build-installer.ps1` (Lite, sin sufijo: KeyStepProStudio-Setup-x.y.z.exe) y `-Full` (KeyStepProStudio-Full-Setup-x.y.z.exe). CI compila las dos ediciones en Windows y Mac. + ## Build - `cmake -S . -B build -DCMAKE_BUILD_TYPE=Release` y `cmake --build build --config Release`. JUCE 8.0.4 se descarga solo (FetchContent) o se usa uno local con `-DJUCE_DIR=...`. - Windows: Visual Studio 2022/2026 con CMake (o `cmake -G "Visual Studio 17 2022"`). Mac: Xcode o Ninja. @@ -39,7 +42,6 @@ App JUCE 8 (Standalone, Windows y Mac) para el Arturia KeyStep Pro: editor de pr - Tools/KspRestore/Main.cpp: fase 1b, `--file X --project N [--to M] [--write]`: backup previo del slot (ksp-pre-restore-P-*.json), sin `--write` es un dry run; con `--write` abre sesión, manda, commit (3 s) y relee. Solo manda tramas al slot M (`ksp::isWriteFor`). - App/KspStyle.h: look del KeyStep Pro (paleta chasis blanco / panel negro / colores de track verde-naranja-amarillo-rojo, botones grises con bisel, brillo en lo encendido, KspLookAndFeel, TrackTabs, SevenSegDisplay de 7 segmentos). Los colores van en ksp::ui::palette; no hardcodear colores en las ventanas. - App/Main.cpp: KeyStepProStudio (fase 2): editor de patterns, abre un backup o lee del equipo, escribe a los 16 slots con backup previo y relectura. Source/Ksp/Device.h (JUCE): open, readSlot, backupSlot, writeSlot; lo usan la app y KspRestore. -- Source/Model/Overdub.h: grabación en tiempo real (overdub) sobre un Pattern desde MIDI crudo: clock desde Start, paso más cercano módulo el largo, solo suma notas y salta las que ya están en el paso (el KSP tocando lo guardado). Lo usa el botón Rec de la app con Device::setListener. - Source/Model/Pattern.h: un pattern de secuenciador (largo, root, escala, pasos con notas) leído de y escrito en un ProjectData, sin tocar el equipo; después `writePlan` lo manda. Portado de edReadPattern / edWritePattern de KSCON. No toca drums ni arpegiador. - Source/Model/NoteMove.h: mover / copiar un grupo de notas de un Pattern (arrastre del grid), todo o nada, y sameSteps para el Undo del grid; tests/notemove_test.cpp. - Source/Model/PatternOps.h: nudge / reverse / invert de un Pattern entero (botón Ops de la app); tests/patternops_test.cpp. @@ -57,7 +59,7 @@ App JUCE 8 (Standalone, Windows y Mac) para el Arturia KeyStep Pro: editor de pr - App/GeneratorWindow.h: la ventana Generate... (estilo, mood, semilla, cadenas, velocidad) que escribe patterns y scenes en el proyecto abierto, con deshacer. - Source/Gen/Theory.h (Chord, Params, escalas, RNG) + Roles.h (motor de roles: RoleSpec = drone/pad/melody/bells/bass/stab con densidad, registro, largo, pulsos y gate; euclid; makeDrumPattern con DrumSpec) + Ambient.h (el preset ambient = 4 RoleSpec, misma salida que antes: hash congelado en tests/gen_test.cpp) + Styles.h (presets techno/house/dnb: drums en el track 1 en drum mode, bajo, acordes, lead; `applyStyle`) + Arrangement.h (arreglo musical: 8 scenes con curva de energía; patterns 1-5 drums, 1-12 y 16 vacío en T2-T4; `applyArrangement`). En drum mode el instrumento del track 1 escucha el canal 10. - Source/Gen/Ambient.h: generador ambient (4 tracks, largos 16/13/17/19, semilla, scenes, cadenas, moods ethereal/dark/meditative). Tools/KspGen/Main.cpp: lo escribe a un slot 10-16 con backup y relectura. tests/gen_test.cpp. -- tests/protocol_test.cpp: tramas contra las capturas de KSCON en hardware. tests/backup_test.cpp: plan, lectura con un equipo falso y JSON. tests/restore_test.cpp: JSON ida y vuelta, orden de envío, arreglo de drums, relectura. tests/pattern_test.cpp: leer / editar / escribir un pattern, límites. tests/overdub_test.cpp: cuantizado al paso, dedupe, canal, Start/Stop/Continue, límites. +- tests/protocol_test.cpp: tramas contra las capturas de KSCON en hardware. tests/backup_test.cpp: plan, lectura con un equipo falso y JSON. tests/restore_test.cpp: JSON ida y vuelta, orden de envío, arreglo de drums, relectura. tests/pattern_test.cpp: leer / editar / escribir un pattern, límites. ## Manual - docs/manual/ (inglés): README con el estado por fase, protocolo y reglas de seguridad, herramientas, modelo, app y bitácora de hardware. Cada PR que cambie comportamiento, herramientas o resultados lo actualiza; es el contexto para tareas futuras. Conciso. diff --git a/CMakeLists.txt b/CMakeLists.txt index 56e872d..0cbe678 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,9 +42,6 @@ add_test(NAME mix_test COMMAND mix_test) add_executable(gen_test tests/gen_test.cpp) target_link_libraries(gen_test PRIVATE ksp_core) add_test(NAME gen_test COMMAND gen_test) -add_executable(overdub_test tests/overdub_test.cpp) -target_link_libraries(overdub_test PRIVATE ksp_core) -add_test(NAME overdub_test COMMAND overdub_test) add_executable(notemove_test tests/notemove_test.cpp) target_link_libraries(notemove_test PRIVATE ksp_core) add_test(NAME notemove_test COMMAND notemove_test) @@ -62,6 +59,9 @@ target_link_libraries(KspCompare PRIVATE ksp_core) # JUCE: a local checkout with -DJUCE_DIR=..., otherwise downloaded once into the build folder. # KSP_TESTS_ONLY=ON skips JUCE entirely (fast checks of the no-JUCE code). option(KSP_TESTS_ONLY "Build only the tests without JUCE" OFF) +# Lite (OFF, the default and the recommended edition) is the editor alone, without the VST3 SDK. +# Full (ON) adds VST3 instruments and effects, audio output and WAV recording. +option(KSP_ENABLE_VST_HOST "Build the Full edition with VST3 hosting (OFF = Lite, the default)" OFF) if(NOT KSP_TESTS_ONLY) set(JUCE_DIR "" CACHE PATH "Local JUCE checkout (empty = download)") if(JUCE_DIR) @@ -115,25 +115,27 @@ if(NOT KSP_TESTS_ONLY) juce::juce_recommended_config_flags juce::juce_recommended_warning_flags) - # Phase 3: out-of-process VST3 scanner (one plugin file per run; a crash kills only this process). - juce_add_console_app(KspVstHelper PRODUCT_NAME "KspVstHelper") - target_sources(KspVstHelper PRIVATE Tools/KspVstHelper/Main.cpp) - target_compile_definitions(KspVstHelper PRIVATE JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_AU=0) - target_link_libraries(KspVstHelper PRIVATE - juce::juce_audio_processors - juce::juce_recommended_config_flags - juce::juce_recommended_warning_flags) + if(KSP_ENABLE_VST_HOST) + # Phase 3: out-of-process VST3 scanner (one plugin file per run; a crash kills only this process). + juce_add_console_app(KspVstHelper PRODUCT_NAME "KspVstHelper") + target_sources(KspVstHelper PRIVATE Tools/KspVstHelper/Main.cpp) + target_compile_definitions(KspVstHelper PRIVATE JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_AU=0) + target_link_libraries(KspVstHelper PRIVATE + juce::juce_audio_processors + juce::juce_recommended_config_flags + juce::juce_recommended_warning_flags) - # Phase 3: checks that a scanned VST3 instrument loads, sounds and restores its state (no audio device, no window). - juce_add_console_app(KspHostCheck PRODUCT_NAME "KspHostCheck") - target_sources(KspHostCheck PRIVATE Tools/KspHostCheck/Main.cpp) - target_compile_definitions(KspHostCheck PRIVATE JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_AU=0) - target_link_libraries(KspHostCheck PRIVATE - ksp_core - juce::juce_audio_processors - juce::juce_audio_formats - juce::juce_recommended_config_flags - juce::juce_recommended_warning_flags) + # Phase 3: checks that a scanned VST3 instrument loads, sounds and restores its state (no audio device, no window). + juce_add_console_app(KspHostCheck PRODUCT_NAME "KspHostCheck") + target_sources(KspHostCheck PRIVATE Tools/KspHostCheck/Main.cpp) + target_compile_definitions(KspHostCheck PRIVATE JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_AU=0) + target_link_libraries(KspHostCheck PRIVATE + ksp_core + juce::juce_audio_processors + juce::juce_audio_formats + juce::juce_recommended_config_flags + juce::juce_recommended_warning_flags) + endif() # Phase 2: KeyStepPro Studio, the editor app. # Icon: App/Resources/*.png from Tools/make_icon.py (the four track colours); JUCE makes the .ico / .icns. @@ -141,16 +143,18 @@ if(NOT KSP_TESTS_ONLY) BUNDLE_ID io.github.lgarraza.keystepprostudio ICON_BIG App/Resources/icon.png ICON_SMALL App/Resources/icon_small.png) target_sources(KeyStepProStudio PRIVATE App/Main.cpp) - target_compile_definitions(KeyStepProStudio PRIVATE JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_AU=0 JUCE_APPLICATION_NAME_STRING="KeyStepPro Studio" JUCE_APPLICATION_VERSION_STRING="${PROJECT_VERSION}") + target_compile_definitions(KeyStepProStudio PRIVATE JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 KSP_ENABLE_VST_HOST=$ JUCE_APPLICATION_NAME_STRING="KeyStepPro Studio" JUCE_APPLICATION_VERSION_STRING="${PROJECT_VERSION}") target_link_libraries(KeyStepProStudio PRIVATE ksp_core juce::juce_gui_basics juce::juce_audio_devices - juce::juce_audio_processors - juce::juce_audio_utils juce::juce_recommended_config_flags juce::juce_recommended_warning_flags) - add_dependencies(KeyStepProStudio KspVstHelper) - add_custom_command(TARGET KeyStepProStudio POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $) + if(KSP_ENABLE_VST_HOST) + target_compile_definitions(KeyStepProStudio PRIVATE JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_AU=0) + target_link_libraries(KeyStepProStudio PRIVATE juce::juce_audio_processors juce::juce_audio_utils) + add_dependencies(KeyStepProStudio KspVstHelper) + add_custom_command(TARGET KeyStepProStudio POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $) + endif() endif() diff --git a/README.md b/README.md index b236e1b..84efd36 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,40 @@ # KeyStepPro Studio A free desktop companion for the **Arturia KeyStep Pro**: edit its sequencer patterns on a big screen, keep a library of -projects on your computer, and play the KeyStep through your own VST3 instruments without opening a DAW. +projects on your computer, and generate new music for it. An optional Full edition also plays the KeyStep through your own +VST3 instruments. ![KeyStepPro Studio main window](docs/images/main-window.png) Windows (tested) and macOS (builds from the same code, not tested lately). Not affiliated with Arturia. +## Editions + +**We recommend Lite.** The app was made to edit KeyStep Pro projects visually, and Lite does exactly that. Full adds +VST3 hosting, which takes the app a bit away from that purpose. Both editions are maintained from one source tree +(CMake option `KSP_ENABLE_VST_HOST`, OFF by default = Lite): + +| | **Lite** (recommended) | Full | +| --- | --- | --- | +| Pattern editor, library, Project Map, generator, backups, sync, MIDI monitor | yes | yes | +| KeyStep clock readout (PLAY / STOP, position, BPM) | yes | yes | +| VST3 instruments, effects, audio output, WAV recording | no | yes | + +Lite has no VST3 SDK and no audio engine, so it is smaller and builds faster. Each release has both installers +(`KeyStepProStudio-Setup-x.y.z.exe` for Lite and `KeyStepProStudio-Full-Setup-x.y.z.exe`); installing one replaces the other. +Build Full with `cmake -S . -B build-full -DKSP_ENABLE_VST_HOST=ON`. + ## What it does - **Pattern editor.** Read a project from the KeyStep over USB (or open a backup), see all 64 steps of every pattern, add, delete and edit notes (pitch, gate, velocity, time shift, randomness), change length, root and scale, copy patterns, shift a track by an octave, and write the result back to the KeyStep. -- **Overdub.** Press Rec, hit Play on the KeyStep and play over the loop; notes land on the nearest step. - **Library.** Your projects live on the computer (`Documents/KeyStepPro Studio/Library`), with names, notes and the instruments of each track. The KeyStep's 16 slots are treated as a working area: open and edit projects without the KeyStep connected, load one into a slot when you want to play it. -- **VST3 host.** One VST3 instrument per KeyStep track (T1 to T4 on MIDI channels 1 to 4), up to four effect inserts per +- **VST3 host (Full edition).** One VST3 instrument per KeyStep track (T1 to T4 on MIDI channels 1 to 4), up to four effect inserts per track and a master effect chain with a limiter. The KeyStep is the clock: its Play and Stop drive the app. -- **WAV recording** of the mix and/or one stem per track, started and stopped by the KeyStep's transport. +- **WAV recording (Full edition)** of the mix and/or one stem per track, started and stopped by the KeyStep's transport. - **Generator** (Generate...): writes patterns and scenes into the open project in a style (Ambient with four moods, Techno, House deep / chill / melodic, Drum & bass) with a seed, pattern chains and undo. The styles add drums on track 1 and set the project tempo. @@ -52,7 +68,7 @@ start writing to your KeyStep. ## Install (Windows) -1. Download `KeyStepProStudio-Setup-x.y.z.exe` from [Releases](../../releases) and run it. It installs the app, the +1. Download `KeyStepProStudio-Setup-x.y.z.exe` (Lite, recommended; `KeyStepProStudio-Full-Setup-x.y.z.exe` adds VST3 hosting) from [Releases](../../releases) and run it. It installs the app, the VST3 helper and the command-line tools, and adds a Start menu entry. Prefer no installer? Download `KeyStepProStudio-x.y.z-portable.zip` instead, unzip it anywhere and run `KeyStepPro Studio.exe`. 2. The installer and the app are **not code-signed** (see [docs/signing.md](docs/signing.md)). Windows SmartScreen will @@ -69,7 +85,7 @@ start writing to your KeyStep. selected note. Switch track and pattern at the top. 3. **Save** keeps the project in your library. **Write to slot** sends it to a slot from 10 to 16 (backed up and verified as described above). -4. To play it through VST3 instruments: **Scan plugins** once, pick an instrument per track in the rows under the grid, +4. Full edition only: to play it through VST3 instruments: **Scan plugins** once, pick an instrument per track in the rows under the grid, choose your audio device with **Audio...**, and press Play on the KeyStep. **New to the app?** Start with the [User guide](docs/manual/user/README.md). Developer notes: [docs/manual](docs/manual/README.md). @@ -84,7 +100,7 @@ Installed next to the app; useful for backups and troubleshooting. | `KspBackup` | no | Backup of all projects and the global settings to JSON (about 10 minutes). | | `KspRestore` | only with `--write` | Restore a project from a backup to a slot (backup first, read back after). | | `KspGen` | only with `--write` | Generate an ambient project, or a style with `--style`, into a slot. | -| `KspHostCheck` | no | Check that a VST3 instrument or effect loads, sounds and restores its state. | +| `KspHostCheck` (Full only) | no | Check that a VST3 instrument or effect loads, sounds and restores its state. | Details in [docs/manual/02-tools.md](docs/manual/02-tools.md). diff --git a/Source/Mix/Transport.h b/Source/Mix/Transport.h index 06a0196..ed1ff85 100644 --- a/Source/Mix/Transport.h +++ b/Source/Mix/Transport.h @@ -56,6 +56,8 @@ class Transport long ticks() const noexcept { return tickCount.load(); } /** The tempo in BPM from the clock (120 until a clock arrives). */ double bpm() const noexcept { return tempo.load(); } + /** Has a tempo been measured from the clock yet? */ + bool hasTempo() const noexcept { return measured.load(); } /** How many Start / Continue and Stop messages arrived: a watcher compares them with what it saw last. */ std::uint32_t starts() const noexcept { return startCounter.load(); } std::uint32_t stops() const noexcept { return stopCounter.load(); } @@ -84,6 +86,7 @@ class Transport { average = average == 0.0 ? dt : 0.95 * average + 0.05 * dt; tempo.store (60.0 / (kClocksPerQuarter * average)); + measured.store (true); } } lastClock = now; @@ -92,6 +95,7 @@ class Transport std::atomic playingNow { false }; std::atomic tickCount { 0 }; std::atomic tempo { 120.0 }; + std::atomic measured { false }; std::atomic startCounter { 0 }, stopCounter { 0 }; double lastClock = 0.0, average = 0.0; // MIDI thread only }; diff --git a/Source/Model/Overdub.h b/Source/Model/Overdub.h deleted file mode 100644 index 4ef3103..0000000 --- a/Source/Model/Overdub.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - Overdub.h - Real-time recording onto a Pattern (Pattern.h) from the KeyStep's MIDI out: notes are added, nothing is removed. - C++17 + std, no JUCE; fed one raw MIDI message at a time, in arrival order, on one thread. - - Position comes from MIDI clock: Start (FA) resets it, each clock (F8) is one tick, 24 per quarter note - (`clocksPerStep` 6 = 1/16 steps). A note-on goes to the nearest step, modulo the pattern length, with its velocity; - gate, shift and randomness come from the pattern's defaults (the gate encoding is not mapped yet). A pitch that is - already on that step is skipped: that is the KeyStep's own sequencer playing the stored notes back, or the same - note played again on a later pass. -*/ - -#pragma once - -#include -#include -#include -#include "Pattern.h" - -namespace ksp::model -{ - -class Overdub -{ -public: - /** `channel` 0..15: only note-ons on that channel are recorded. */ - Overdub (int clocksPerStepIn, int channelIn) : clocksPerStep (std::max (1, clocksPerStepIn)), channel (channelIn) {} - - /** One MIDI message, status byte first. Returns true when a note was added to `pt`. */ - bool handle (Pattern& pt, const std::uint8_t* m, std::size_t size) - { - if (size == 0) - return false; - switch (m[0]) - { - case 0xFA: running = true; clocks = -1; return false; // the first clock after Start is tick 0 - case 0xFB: running = true; return false; - case 0xFC: running = false; return false; - case 0xF8: if (running) ++clocks; return false; - default: break; - } - if (! running || size < 3 || (m[0] & 0xF0) != 0x90 || m[2] == 0 || (m[0] & 0x0F) != channel) - return false; - - auto& step = pt.steps[static_cast (stepAt (pt.length))]; - const int pitch = m[1]; - if (std::any_of (step.notes.begin(), step.notes.end(), [=] (const Note& n) { return n.pitch == pitch; })) - return false; - int total = 0; - for (const auto& s : pt.steps) - total += static_cast (s.notes.size()); - if (static_cast (step.notes.size()) >= kMaxStepNotes || total >= kSlots) - { - ++dropped; - return false; - } - Note n = pt.defaults; - n.pitch = pitch; - n.velocity = m[2]; - if (n.gate == 0) n.gate = 64; - step.notes.push_back (n); - step.on = true; - ++added; - return true; - } - - /** The step a note played now would land on. */ - int stepAt (int length) const - { - const long c = std::max (0L, clocks); - return static_cast ((c + clocksPerStep / 2) / clocksPerStep % std::max (1, length)); - } - - bool isRunning() const noexcept { return running; } - - int added = 0, dropped = 0; // notes recorded / refused because the step (16) or the pattern (192) was full - -private: - int clocksPerStep, channel; - bool running = false; - long clocks = -1; -}; - -} // namespace ksp::model diff --git a/docs/announcements/arturia-forum.md b/docs/announcements/arturia-forum.md index 8743441..080b073 100644 --- a/docs/announcements/arturia-forum.md +++ b/docs/announcements/arturia-forum.md @@ -25,7 +25,7 @@ I made a free, open-source desktop app for the KeyStep Pro and I'd like to share [list] [li]Read and write projects over USB (SysEx), with the KeyStep's 16 slots used as working memory.[/li] [li]Full backups of all projects and global settings to JSON.[/li] -[li]Grid editor: all 64 steps of every pattern, notes with pitch, gate, velocity, time shift and randomness, length, root and scale, copy patterns, overdub recording.[/li] +[li]Grid editor: all 64 steps of every pattern, notes with pitch, gate, velocity, time shift and randomness, length, root and scale, copy patterns.[/li] [li]Drum grid for track 1 in drum mode.[/li] [li]Project Map: scenes x tracks with a mini piano roll in each cell. Pick the pattern a track plays in a scene, copy / paste / clear scenes, Undo.[/li] [li]Generate: writes patterns and scenes into the open project in a style (Ambient, Techno, House, Drum & bass), with a seed and undo.[/li] diff --git a/docs/images/main-window.png b/docs/images/main-window.png index 20f5bce..15ce06d 100644 Binary files a/docs/images/main-window.png and b/docs/images/main-window.png differ diff --git a/docs/manual/03-model.md b/docs/manual/03-model.md index 71367db..7ed2307 100644 --- a/docs/manual/03-model.md +++ b/docs/manual/03-model.md @@ -10,9 +10,6 @@ pattern): `readPattern`, `writePattern`, `check`. Drum lanes and arpeggio are not touched. Time shift is stored 0-99 (49 = on the step, -49%..+50%) and randomness 0-100 (% chance to play, 100 = always); `readPattern` clamps values beyond that (earlier versions let the knobs write up to 127) and `check` refuses them. -- `Source/Model/Overdub.h`: real-time recording onto a `Pattern` from raw MIDI (clock + note-ons of one channel): - nearest step by clock ticks modulo the length, velocity from the note, the rest from the pattern defaults; pitches - already on the step are skipped (the KeyStep playing its stored notes back); counts notes that did not fit. - `Source/Model/NoteMove.h`: `moveNotes` moves / copies a group of notes by steps and semitones, all or nothing (range, 16 notes per step; same pitch replaced); `sameSteps` compares two patterns' steps (the grid's Undo). `tests/notemove_test.cpp`. - `Source/Ksp/MidiLink.h` (JUCE): the USB port pair. `Source/Ksp/Device.h` (JUCE): `open`, `readSlot`, `backupSlot`, `writeSlot`, `setListener` (non-SysEx MIDI: notes, clock, Start/Stop, on the MIDI thread); used by `KspRestore` and the app. @@ -20,7 +17,7 @@ time shift / randomness / pass mask per hit): `readDrumPattern`, `writeDrumPattern`, `check`, `isDrumMode`, `setDrumMode`. Ported from KSCON (layout in the file header); the lane masks are rebuilt and freed slots emptied (127). Drum swing and flags are left as they are. NOT yet verified on hardware: do a slot 10-16 round trip (backup first) before relying on it. -- Tests (`tests/`): `protocol_test`, `backup_test`, `restore_test`, `pattern_test`, `drum_test`, `overdub_test`; all with a fake device or none. +- Tests (`tests/`): `protocol_test`, `backup_test`, `restore_test`, `pattern_test`, `drum_test`; all with a fake device or none. CI (GitHub Actions, Windows only since 2026-10-29) builds everything and runs them. - `Source/Gen/Theory.h` + `Roles.h`: the role engine. A `RoleSpec` (role drone / pad / melody / bells / bass / stab, length, density, register, RNG salt; for bass and stab euclidean pulses, rotation and gate) goes through `makeRolePattern`; `euclid` and `makeDrumPattern(DrumSpec)` build drum grooves (kick, backbeat snare, hats, diff --git a/docs/manual/04-studio-app.md b/docs/manual/04-studio-app.md index 54d9072..31cf6fc 100644 --- a/docs/manual/04-studio-app.md +++ b/docs/manual/04-studio-app.md @@ -21,17 +21,10 @@ Icon: the white chassis, black display strip and the four track colours, drawn b would leave 0-127); it reaches the KeyStep with **Write to slot**. - **Send to slot N** (single button above the track cards, replaces Read from KeyStep / Write to slot / Slot box): shows "Read slot N from KeyStep" when the project matches the KeyStep and "Send to slot N" (blinking) when it has unsent edits; N is the slot the project came from, any of 1-16. Confirms, backs the slot up, writes, reads back and reports "written and verified" or the differing values. Edits are kept in memory when you switch pattern or track. -- **Rec** (overdub, bottom row): with the KeyStep connected, press Rec, then Play on the KeyStep and play over the - loop; notes on the chosen channel (default = track number) are added to the pattern shown at the nearest step of the - chosen size (1/4 .. 1/32, default 1/16; it must match the track's time division on the KeyStep). Velocity comes from - the key, gate from the pattern default (not mapped yet). Notes already on that step are skipped, so the sequencer's - own playback is not recorded twice. **Stop rec** ends the take; **Undo take** removes it. Switching pattern, track or - project, reading or writing stops recording. The KeyStep keeps playing the stored pattern until you write the slot, - and pattern chains are not followed (the position is counted from Start, modulo this pattern's length). - **Drum grid**: when track 1 is in drum mode (the KeyStep's drum track, or after a generated style) the grid becomes 24 lanes x 64 steps (`App/DrumGrid.h`, lane 1 = kick at the bottom, names and notes of the default GM drum map). Click a cell to add / remove a hit, drag to paint more of the same, mouse wheel on a hit for its velocity (its shade), click a lane name to hear it, ruler or length box for the - last step (lanes that followed the length follow it). Root, scale, octave, knobs, Rec and Copy are disabled there. Up to 192 hits. + last step (lanes that followed the length follow it). Root, scale, octave, knobs and Copy are disabled there. Up to 192 hits. - **Drums** (card 1, lit when on): switches track 1 between drum and sequencer mode in the project (both kinds of patterns stay stored; the grid follows). **Clear** (left of Copy to) empties the pattern shown, notes or drum hits; length and settings stay. Both are ordinary edits: they reach the KeyStep with Write. @@ -111,3 +104,5 @@ Icon: the white chassis, black display strip and the four track colours, drawn b channel of the track when it sets one (`effectiveChannel`: item 0x78 p123, else 1-4). Then play it through the instruments with the KeyStep, hear it, edit it, **Save** it to the library and load it into a slot with **Write to slot**. **Undo** brings the project back as it was before the last generation. The settings used are saved with the project (the library file keeps the style, the mood and the seed). It needs a project open (a library project, a backup or one read from the KeyStep). + +- **Editions (2026-11-04).** Lite (recommended, `KSP_ENABLE_VST_HOST=OFF`, the CMake default) is the primary edition; Full (`ON`) is optional and both are maintained. Lite: no `Source/Host`, no plugin / FX windows, instrument boxes, audio, test note or WAV recording; the track cards keep the number, Seq / Arp keys and the arp settings, and the clock label and BPM come from `Source/Mix/Transport.h` fed by the MIDI listener. Library files keep the plugin fields in both. diff --git a/docs/manual/README.md b/docs/manual/README.md index dbb991d..5a22fb5 100644 --- a/docs/manual/README.md +++ b/docs/manual/README.md @@ -22,6 +22,6 @@ Protocol details (frames, quirks) live in [`../protocolo.md`](../protocolo.md), | 0 | KspProbe: read-only connection test | done, verified | | 1a | KspBackup: all projects and globals to JSON | done, verified (16 projects, 9.6 min) | | 1b | KspRestore: one project to a slot, backup first, read-back | done, verified (project 1 to slot 16) | -| 2 | Pattern model + Studio app (edit notes, overdub from the KeyStep, write to slots 10-16) | model + app done; edit and write to slot 15 verified by read-back (device display not yet checked); overdub built, not yet tried with the KeyStep | -| 3 | VSTi host per track: one VST3 instrument per track, inserts and master chain, KeyStep as clock, WAV recording | done, used live | +| 2 | Pattern model + Studio app (edit notes, write to slots 10-16) | model + app done; edit and write to slot 15 verified by read-back (device display not yet checked) | +| 3 | VSTi host per track (Full edition only, `KSP_ENABLE_VST_HOST`): one VST3 instrument per track, inserts and master chain, KeyStep as clock, WAV recording | done, used live | | 4 | Generator: ambient (moods) and style presets techno / house / drum & bass (drums on track 1) | in progress | diff --git a/docs/manual/user/01-install.md b/docs/manual/user/01-install.md index 052128d..25492e7 100644 --- a/docs/manual/user/01-install.md +++ b/docs/manual/user/01-install.md @@ -14,7 +14,7 @@ `SHA256SUMS.txt` on the release page. The release notes also link the VirusTotal scans. 4. Start **KeyStepPro Studio** from the Start menu (installer) or run the exe (zip). -The installer also adds `KspVstHelper.exe` (VST3 helper) and the command-line tools. +The installer also adds `KspVstHelper.exe` (VST3 helper, Full edition) and the command-line tools. That helper only comes with the **Full** edition (`KeyStepProStudio-Full-Setup-x.y.z.exe`). We recommend the **Lite** edition (`KeyStepProStudio-Setup-x.y.z.exe`), which has no VST3 parts; installing one edition replaces the other. ## First run diff --git a/docs/manual/user/05-grid.md b/docs/manual/user/05-grid.md index c1196f4..acc3f1e 100644 --- a/docs/manual/user/05-grid.md +++ b/docs/manual/user/05-grid.md @@ -16,7 +16,7 @@ ## Add, select, delete - **Left click** an empty cell: adds a note (up to 16 per step). New notes take the pattern's default gate and - velocity, and the track's instrument plays it briefly so you hear what you put. + velocity, and (Full edition) the track's instrument plays it briefly so you hear what you put. - **Left click** a note: selects it. - **Right click** a note: deletes it. - **Note names:** every key in the left column is named (the Cs in bold, middle C = C3 as on the KeyStep Pro), and @@ -51,7 +51,7 @@ When track 1 is in drum mode (set on the KeyStep, or by a generated style) its g notes, kick at the bottom, each named after the KeyStep's default drum map (kick 36, snare 38, clap 39, closed hat 42, open hat 46 ...). Click a cell to add or remove a hit and drag to paint more; the mouse wheel on a hit changes its velocity (darker = louder); click a lane name to hear it. END and the length box work as for notes. Root, scale, -octave, the knobs, Rec and Copy do not apply to drums. The KeyStep holds up to 192 hits per pattern. +octave, the knobs and Copy do not apply to drums. The KeyStep holds up to 192 hits per pattern. The **Drum** key on card 1 (and **Seq** for the sequencer) switches track 1 between drum and sequencer mode (lit = drums). Both kinds of patterns stay stored, so switching back loses nothing. **Clear** (left of *Copy to*) empties the pattern you are looking at, diff --git a/docs/manual/user/06-track-cards.md b/docs/manual/user/06-track-cards.md new file mode 100644 index 0000000..1ad657a --- /dev/null +++ b/docs/manual/user/06-track-cards.md @@ -0,0 +1,34 @@ +# 6. Track cards and the clock + +**You will:** pick a track, switch its mode and read the KeyStep's clock. + +## Per track + +This chapter is what the **Lite** edition shows. The Full edition adds the instrument, fader, mute and solo to each card ([chapter 6b](06b-instruments.md)). + +Above the grid there are four cards, one per track, each in the track's own color (T1 green, T2 orange, T3 yellow, +T4 red) so you can tell tracks apart at a glance. The card of the selected track is full color with a thick white border; the +others are dimmed. Click anywhere on a card (its controls too) to select its track; the pointer turns into a hand over the cards you can select. Each card has: + +- a dot with the track number, +- **Seq** and **Arp** keys (**Seq** and **Drum** on track 1), lit for the mode the track is in; see the grid chapter, +- on tracks 2-4, the arpeggiator settings: mode, octave, division and swing. + +![Track cards in the Lite edition](img/shot-track-cards.png) + +Each track sends on its own MIDI channel (T1 = 1 ... T4 = 4, the KeyStep defaults; drums on 10, or the channel the project sets in +the **Project Map**). + +## The clock + +The label under the grid shows what the KeyStep's MIDI clock says: **PLAY** or **STOP**, the position since the last Start +(bar.beat.sixteenth), the time and the tempo. The tempo also shows next to the pattern length (`---` until a clock arrives). +They need the KeyStep's MIDI port open: **Read** (or the **MIDI monitor** in **Tools**) opens it. + +```mermaid +flowchart LR + K[KeyStep Pro] -- "USB MIDI: clock, Start, Stop" --> A[App: clock label and tempo] + K -- "every control" --> M[MIDI monitor] +``` + +**Watch out:** in the Lite edition the app does not make sound. Play the KeyStep through its own outputs or your DAW; the app edits what the KeyStep plays. diff --git a/docs/manual/user/06-instruments.md b/docs/manual/user/06b-instruments.md similarity index 92% rename from docs/manual/user/06-instruments.md rename to docs/manual/user/06b-instruments.md index 0fb631c..196880e 100644 --- a/docs/manual/user/06-instruments.md +++ b/docs/manual/user/06b-instruments.md @@ -1,4 +1,6 @@ -# 6. Tracks and instruments (VST3) +# 6b. Instruments and effects (VST3, Full edition only) + +> The Lite edition has none of this: no plugins, no audio output. The track cards then show only the number and the Seq / Arp keys ([chapter 6](06-track-cards.md)). **You will:** give each KeyStep track a VST3 instrument and hear the sequencer through your computer. diff --git a/docs/manual/user/README.md b/docs/manual/user/README.md index 705dfba..b7896ba 100644 --- a/docs/manual/user/README.md +++ b/docs/manual/user/README.md @@ -12,13 +12,18 @@ Short, task-oriented chapters. Each one says what you will do, how, and what to | 3 | [Read and write slots](03-slots.md) | ready | | 4 | [Backups](04-backups.md) | ready | | 5 | [The pattern grid](05-grid.md) | ready | -| 6 | [Tracks and instruments (VST3)](06-instruments.md) | ready | +| 6 | [Track cards and the clock](06-track-cards.md) | ready | +| 6b | [Instruments and effects (VST3, Full edition only)](06b-instruments.md) | ready | | 7 | [Generate](07-generate.md) | ready | | 8 | [Save, Library and closing](08-library.md) | ready | | 9 | [Project Map](09-project-map.md) | ready | | 9b | Tools menu | coming (**Tools > User manual (PDF)** opens this guide; see below) | | 10 | Dragging steps in the grid | coming | -| 11 | Transport and WAV recording; MIDI monitor | next batch | +| 11 | WAV recording (Full edition); MIDI monitor | next batch | + +## Editions + +**Lite** (recommended) is the editor alone: it edits and generates KeyStep Pro projects visually. **Full** adds VST3 instruments and effects and WAV recording. Both are maintained. Parts marked *Full edition* do not exist in Lite. ## This guide as a PDF diff --git a/docs/manual/user/SCREENSHOTS.md b/docs/manual/user/SCREENSHOTS.md index ee9eb4a..24abb9d 100644 --- a/docs/manual/user/SCREENSHOTS.md +++ b/docs/manual/user/SCREENSHOTS.md @@ -15,6 +15,7 @@ Save as PNG in `img/`, named as below. Add numbered red callouts (1, 2, 3...) ma | `shot-end-drag.png` | 5 | END grip on the line, hovered (lit, white outline) with the dimmed area after it | | `shot-instruments.png` | 6 (done, 0.6.0) | Four track cards (color per track, one selected, others dimmed) with plugins loaded | | `shot-plugins.png` | 6 (done) | Plugins... window (Hide column) | +| `shot-track-cards.png` | 6 (done, Lite edition) | Four track cards without instrument rows: number, Seq / Arp keys, arp settings | | `shot-generate.png` | 7 (done) | Generate... window | | `shot-library.png` | 8 (done) | Library window with a few projects | | `shot-close-warning.png` | 8 (done) | The Save / Discard / Cancel dialog | diff --git a/docs/manual/user/img/shot-track-cards.png b/docs/manual/user/img/shot-track-cards.png new file mode 100644 index 0000000..d50523b Binary files /dev/null and b/docs/manual/user/img/shot-track-cards.png differ diff --git a/installer/KeyStepProStudio.iss b/installer/KeyStepProStudio.iss index dbdbd66..2120658 100644 --- a/installer/KeyStepProStudio.iss +++ b/installer/KeyStepProStudio.iss @@ -9,6 +9,14 @@ #define BuildDir "..\build" #endif +; Default = Lite edition (editor only, recommended): output KeyStepProStudio-Setup-x.y.z.exe. +; Full edition (adds VST3 hosting): ISCC /DFull=1; output KeyStepProStudio-Full-Setup-x.y.z.exe. Same AppId, so they replace each other. +#ifdef Full + #define Edition "-Full" +#else + #define Edition "" +#endif + #define AppName "KeyStepPro Studio" #define AppExe "KeyStepPro Studio.exe" @@ -31,7 +39,7 @@ LicenseFile=..\LICENSE SetupIconFile=..\App\Resources\icon.ico UninstallDisplayIcon={app}\{#AppExe} OutputDir=..\dist -OutputBaseFilename=KeyStepProStudio-Setup-{#AppVersion} +OutputBaseFilename=KeyStepProStudio{#Edition}-Setup-{#AppVersion} Compression=lzma2 SolidCompression=yes WizardStyle=modern @@ -42,17 +50,26 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [InstallDelete] ; the helper was called KspScanner.exe up to v0.4.1 Type: files; Name: "{app}\KspScanner.exe" +#ifndef Full +; going from Full to Lite +Type: files; Name: "{app}\KspVstHelper.exe" +Type: files; Name: "{app}\tools\KspHostCheck.exe" +#endif [Files] ; the app and its VST3 helper (the app looks for KspVstHelper.exe next to itself) Source: "{#BuildDir}\KeyStepProStudio_artefacts\Release\{#AppExe}"; DestDir: "{app}"; Flags: ignoreversion +#ifdef Full Source: "{#BuildDir}\KspVstHelper_artefacts\Release\KspVstHelper.exe"; DestDir: "{app}"; Flags: ignoreversion +#endif ; command-line tools Source: "{#BuildDir}\KspProbe_artefacts\Release\KspProbe.exe"; DestDir: "{app}\tools"; Flags: ignoreversion Source: "{#BuildDir}\KspBackup_artefacts\Release\KspBackup.exe"; DestDir: "{app}\tools"; Flags: ignoreversion Source: "{#BuildDir}\KspRestore_artefacts\Release\KspRestore.exe"; DestDir: "{app}\tools"; Flags: ignoreversion Source: "{#BuildDir}\KspGen_artefacts\Release\KspGen.exe"; DestDir: "{app}\tools"; Flags: ignoreversion +#ifdef Full Source: "{#BuildDir}\KspHostCheck_artefacts\Release\KspHostCheck.exe"; DestDir: "{app}\tools"; Flags: ignoreversion +#endif ; documents Source: "..\README.md"; DestDir: "{app}"; Flags: ignoreversion Source: "..\dist\KeyStepPro-Studio-Manual.pdf"; DestDir: "{app}"; Flags: ignoreversion diff --git a/installer/README.md b/installer/README.md index 70073b9..abce754 100644 --- a/installer/README.md +++ b/installer/README.md @@ -15,7 +15,7 @@ powershell -ExecutionPolicy Bypass -File installer\build-installer.ps1 It builds Release into `build\`, compiles the installer into `dist\KeyStepProStudio-Setup-.exe`, zips the same files as `dist\KeyStepProStudio--portable.zip` (no installer: unzip anywhere and run) and writes -`dist\SHA256SUMS.txt` with the SHA-256 of both. Options: `-JuceDir ` to use a local JUCE, `-SkipBuild` to package what is already built. +`dist\SHA256SUMS.txt` with the SHA-256 of both. Options: `-JuceDir ` to use a local JUCE, `-SkipBuild` to package what is already built, `-Full` for the edition with VST3 hosting (builds into `build-full\`, writes `KeyStepProStudio-Full-Setup-.exe`, `KeyStepProStudio-Full--portable.zip` and `SHA256SUMS-Full.txt`). Without `-Full` it builds Lite, the recommended edition. The version comes from `project(... VERSION x.y.z)` in `CMakeLists.txt`. ## Release diff --git a/installer/build-installer.ps1 b/installer/build-installer.ps1 index f3a5d88..d5072fc 100644 --- a/installer/build-installer.ps1 +++ b/installer/build-installer.ps1 @@ -1,10 +1,13 @@ # Builds KeyStepPro Studio (Release), its Windows installer and a portable zip, and writes SHA256SUMS.txt (both files) next to it in dist\. -# powershell -ExecutionPolicy Bypass -File installer\build-installer.ps1 [-JuceDir F:\path\to\JUCE] [-SkipBuild] +# powershell -ExecutionPolicy Bypass -File installer\build-installer.ps1 [-JuceDir F:\path\to\JUCE] [-SkipBuild] [-Full] +# Default: the Lite edition (editor only, recommended; build\, KeyStepProStudio-*). +# -Full builds the edition with VST3 hosting (build-full\, KeyStepProStudio-Full-*). # Needs CMake, Visual Studio 2022 or newer (C++ workload) and Inno Setup 6 (winget install JRSoftware.InnoSetup) # and pandoc (winget install JohnMacFarlane.Pandoc) for the manual PDF; Edge or Chrome renders it. param( [string]$JuceDir = "", - [switch]$SkipBuild + [switch]$SkipBuild, + [switch]$Full ) $ErrorActionPreference = "Stop" $root = Split-Path -Parent $PSScriptRoot @@ -14,13 +17,16 @@ Set-Location $root $match = Select-String -Path CMakeLists.txt -Pattern 'project\(KeyStepProStudio VERSION ([0-9.]+)' if (-not $match) { throw "No version in CMakeLists.txt" } $version = $match.Matches[0].Groups[1].Value +$buildName = if ($Full) { "build-full" } else { "build" } +$edition = if ($Full) { "-Full" } else { "" } if (-not $SkipBuild) { - $configure = @("-S", ".", "-B", "build", "-DCMAKE_BUILD_TYPE=Release") + $configure = @("-S", ".", "-B", $buildName, "-DCMAKE_BUILD_TYPE=Release") + $configure += "-DKSP_ENABLE_VST_HOST=$(if ($Full) { 'ON' } else { 'OFF' })" if ($JuceDir) { $configure += "-DJUCE_DIR=$JuceDir" } cmake @configure if ($LASTEXITCODE -ne 0) { throw "CMake configure failed" } - cmake --build build --config Release --parallel + cmake --build $buildName --config Release --parallel if ($LASTEXITCODE -ne 0) { throw "Build failed" } } @@ -35,18 +41,22 @@ $iscc = @( ) | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $iscc) { throw "Inno Setup 6 not found (winget install JRSoftware.InnoSetup)" } -& $iscc "/DAppVersion=$version" "/DBuildDir=$root\build" "installer\KeyStepProStudio.iss" +$isccArgs = @("/DAppVersion=$version", "/DBuildDir=$root\$buildName") +if ($Full) { $isccArgs += "/DFull=1" } +& $iscc @isccArgs "installer\KeyStepProStudio.iss" if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed" } # portable zip: the same files the installer copies, no installer needed (unzip anywhere and run) -$stageName = "KeyStepProStudio-$version-portable" +$stageName = "KeyStepProStudio$edition-$version-portable" $stage = Join-Path $root "dist\$stageName" if (Test-Path $stage) { Remove-Item $stage -Recurse -Force } New-Item -ItemType Directory -Path "$stage\tools" | Out-Null -$bd = Join-Path $root "build" +$bd = Join-Path $root $buildName Copy-Item "$bd\KeyStepProStudio_artefacts\Release\KeyStepPro Studio.exe" $stage -Copy-Item "$bd\KspVstHelper_artefacts\Release\KspVstHelper.exe" $stage -foreach ($t in "KspProbe", "KspBackup", "KspRestore", "KspGen", "KspHostCheck") { +if ($Full) { Copy-Item "$bd\KspVstHelper_artefacts\Release\KspVstHelper.exe" $stage } +$tools = @("KspProbe", "KspBackup", "KspRestore", "KspGen") +if ($Full) { $tools += "KspHostCheck" } +foreach ($t in $tools) { Copy-Item "$bd\${t}_artefacts\Release\$t.exe" "$stage\tools" } Copy-Item README.md, THIRD_PARTY_NOTICES.md $stage @@ -58,11 +68,11 @@ Compress-Archive -Path $stage -DestinationPath $zip Remove-Item $stage -Recurse -Force # checksums for the release page (installer and portable zip, in the format `sha256sum -c` and Get-FileHash users expect) -$setup = Join-Path $root "dist\KeyStepProStudio-Setup-$version.exe" +$setup = Join-Path $root "dist\KeyStepProStudio$edition-Setup-$version.exe" $lines = foreach ($f in $setup, $zip) { "$((Get-FileHash $f -Algorithm SHA256).Hash.ToLower()) $(Split-Path -Leaf $f)" } -$lines | Set-Content -Encoding ascii (Join-Path $root "dist\SHA256SUMS.txt") +$lines | Set-Content -Encoding ascii (Join-Path $root "dist\SHA256SUMS$edition.txt") Write-Host "Installer: $setup" Write-Host "Portable: $zip" $lines | ForEach-Object { Write-Host "SHA-256: $_" } diff --git a/tests/mix_test.cpp b/tests/mix_test.cpp index f6c82e0..2a9bbfe 100644 --- a/tests/mix_test.cpp +++ b/tests/mix_test.cpp @@ -78,12 +78,12 @@ int main() // 2. Transport: the tempo from the clock, Start / Continue / Stop, the position from the ticks { mix::Transport tr; - CHECK (2, ! tr.playing() && tr.ticks() == 0 && tr.bpm() == 120.0); + CHECK (2, ! tr.playing() && tr.ticks() == 0 && tr.bpm() == 120.0 && ! tr.hasTempo()); const double dt = 60.0 / (90.0 * 24.0); // 90 BPM double t = 1.0; for (int i = 0; i < 200; ++i, t += dt) tr.message (0xF8, t); - CHECK (2, std::abs (tr.bpm() - 90.0) < 0.1 && ! tr.playing() && tr.ticks() == 0); // the clock runs, nothing plays: no position + CHECK (2, tr.hasTempo() && std::abs (tr.bpm() - 90.0) < 0.1 && ! tr.playing() && tr.ticks() == 0); // the clock runs, nothing plays: no position tr.message (0xFA, t); // Start CHECK (2, tr.playing() && tr.ticks() == 0 && tr.starts() == 1); diff --git a/tests/overdub_test.cpp b/tests/overdub_test.cpp deleted file mode 100644 index e666a8f..0000000 --- a/tests/overdub_test.cpp +++ /dev/null @@ -1,92 +0,0 @@ -// overdub_test: Source/Model/Overdub.h (recording notes from MIDI clock + note-ons onto a pattern). No JUCE. -#include -#include "Model/Overdub.h" - -static int failures = 0; - -#define CHECK(n, cond) \ - do { \ - if (! (cond)) { std::printf ("FAIL %d: %s\n", n, #cond); ++failures; } \ - } while (0) - -using namespace ksp::model; - -static bool send (Overdub& o, Pattern& p, std::uint8_t a, std::uint8_t b = 0, std::uint8_t c = 0) -{ - const std::uint8_t m[3] { a, b, c }; - return o.handle (p, m, a >= 0xF8 ? 1 : 3); -} - -static void clocks (Overdub& o, Pattern& p, int n) -{ - for (int i = 0; i < n; ++i) - send (o, p, 0xF8); -} - -static bool has (const Pattern& p, int step, int pitch) -{ - for (const auto& n : p.steps[static_cast (step)].notes) - if (n.pitch == pitch) - return true; - return false; -} - -int main() -{ - Pattern p; - p.length = 16; - p.defaults.gate = 50; - p.steps[0].notes.push_back ({ 60, 64, 100, 0, 0 }); - p.steps[0].on = true; - Overdub o (6, 0); - - // 1. Before Start nothing is recorded - CHECK (1, ! send (o, p, 0x90, 64, 90)); - - // 2. After Start: the stored note on step 1 is skipped (the sequencer playing it), a new pitch is added - send (o, p, 0xFA); - clocks (o, p, 1); - CHECK (2, ! send (o, p, 0x90, 60, 100)); - CHECK (2, send (o, p, 0x90, 67, 80) && has (p, 0, 67)); - const auto& n = p.steps[0].notes.back(); - CHECK (2, n.velocity == 80 && n.gate == 50); - - // 3. Nearest step: tick 8 -> step 1 (index 1), tick 9 -> step 2 (index 2) - clocks (o, p, 7); // tick 7 - send (o, p, 0xF8); // tick 8 - CHECK (3, send (o, p, 0x90, 62, 90) && has (p, 1, 62)); - send (o, p, 0xF8); // tick 9 - CHECK (3, send (o, p, 0x90, 62, 90) && has (p, 2, 62)); - CHECK (3, p.steps[2].on); - - // 4. Wrong channel, velocity 0 (note-off) and note-off messages are ignored - CHECK (4, ! send (o, p, 0x91, 70, 90)); - CHECK (4, ! send (o, p, 0x90, 71, 0)); - CHECK (4, ! send (o, p, 0x80, 72, 0)); - - // 5. Just before the loop ends it rounds to the first step (tick 94 of 96) - clocks (o, p, 85); // tick 94 - CHECK (5, o.stepAt (16) == 0 && send (o, p, 0x90, 48, 90) && has (p, 0, 48)); - - // 6. Stop pauses, Continue resumes from the same tick, Start goes back to 0 - send (o, p, 0xFC); - CHECK (6, ! send (o, p, 0x90, 50, 90)); - send (o, p, 0xFB); - CHECK (6, o.stepAt (16) == 0); - send (o, p, 0xFA); - clocks (o, p, 13); // tick 12 -> index 2 - CHECK (6, o.stepAt (16) == 2); - - // 7. A full step refuses more notes and counts them - Pattern q; - Overdub r (6, 0); - send (r, q, 0xFA); - for (int k = 0; k < kMaxStepNotes; ++k) - send (r, q, 0x90, static_cast (40 + k), 90); - CHECK (7, static_cast (q.steps[0].notes.size()) == kMaxStepNotes && r.added == kMaxStepNotes); - CHECK (7, ! send (r, q, 0x90, 100, 90) && r.dropped == 1); - CHECK (7, check (q).empty()); - - std::printf (failures == 0 ? "overdub_test: OK\n" : "overdub_test: %d FAIL\n", failures); - return failures == 0 ? 0 : 1; -}