diff --git a/App/GeneratorWindow.h b/App/GeneratorWindow.h index 62e7622..96ce804 100644 --- a/App/GeneratorWindow.h +++ b/App/GeneratorWindow.h @@ -232,14 +232,15 @@ class GeneratorPanel : public juce::Component note.setText (musical ? "Puts track 1 in drum mode and replaces its drum patterns 1-5 and 16, patterns 1-12 and 16 of tracks 2-4 (energy levels by " "chord; 16 is empty) and scenes 1-8: intro, groove, drop, drop variation, breakdown, build, peak, outro. Each scene chains four " - "patterns per track. The KeyStep does not change scenes by itself: you play them live. Drums use the default drum map " + "patterns per track. Tracks 2-4 go to the sequencer (Seq) and the scene mutes are cleared. The KeyStep does not change scenes by itself: " + "you play them live. Drums use the default drum map " "and reach the track 1 instrument on the drum channel (10). Nothing reaches the KeyStep until you load it (Write to slot)." : st != nullptr ? "Puts track 1 in drum mode and replaces its drum patterns 1-4, patterns 1-4 of tracks 2-4 (bass, chords, lead; their time " - "division and Mono/Poly too) and scenes 1-4. Drums use the default drum map (kick 36, snare 38, clap 39, hats 42 and 46) and " + "division and Mono/Poly too) and scenes 1-4; tracks 2-4 go to the sequencer (Seq) and those scenes lose their mutes. Drums use the default drum map (kick 36, snare 38, clap 39, hats 42 and 46) and " "reach the track 1 instrument on the drum channel (10). Nothing reaches the KeyStep until you load it (Write to slot)." - : "Replaces patterns 1-4 of the four tracks and scenes 1-8 of the project shown (and the playback settings of those patterns). It does " - "not touch drums, the arpeggiator or other patterns. Nothing reaches the KeyStep until you load it (Write to slot).", + : "Replaces patterns 1-4 of the four tracks and scenes 1-8 of the project shown (and the playback settings of those patterns), puts the " + "tracks in the sequencer (Seq, not Arp or Drum) and clears the mutes of those scenes. Other patterns are left alone. Nothing reaches the KeyStep until you load it (Write to slot).", juce::dontSendNotification); } diff --git a/App/Main.cpp b/App/Main.cpp index baa18de..1a63510 100644 --- a/App/Main.cpp +++ b/App/Main.cpp @@ -1145,7 +1145,7 @@ class MainComponent : public juce::Component, private juce::Timer 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(); recChannelBox.setSelectedId (track(), juce::dontSendNotification); loadPattern(); }; + trackBox.onChange = [this] { trackTabs.setSelected (trackBox.getSelectedId(), false); grid.setTrack (track() - 1); refreshCards(); loadPattern(); recChannelBox.setSelectedId (engine.channel (track() - 1), juce::dontSendNotification); }; patternBox.onChange = [this] { loadPattern(); }; lengthBox.onChange = [this] { const int n = lengthBox.getSelectedId(); @@ -1548,10 +1548,18 @@ class MainComponent : public juce::Component, private juce::Timer } } + /** Each instrument listens on the channel its KeyStep track sends on: the project's channel when it sets one, else the default + (1-4; in drum mode track 1 plays on the drum channel, 10). */ + void applyChannels() + { + 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)); + } + void loadPattern() { - // in drum mode the KeyStep plays track 1 on the drum channel (Drum output, 10 by default), not on the track's own - engine.setChannel (0, haveProject && ksp::model::isDrumMode (project) ? 10 : 1); + applyChannels(); stopRecording(); haveTake = false; undoTakeButton.setEnabled (false); @@ -2273,6 +2281,7 @@ class MainComponent : public juce::Component, private juce::Timer beforeMapEdit = before; noteChanged(); refreshMap(); + applyChannels(); if (mapWindow != nullptr) mapWindow->content().setCanUndo (true); say ("Track " + juce::String (track + 1) + (channel == 0 ? " MIDI channel: Auto" : " MIDI channel " + juce::String (channel)) @@ -2286,6 +2295,7 @@ class MainComponent : public juce::Component, private juce::Timer beforeMapEdit.reset(); noteChanged(); refreshMap(); + applyChannels(); if (mapWindow != nullptr) mapWindow->content().setCanUndo (false); say ("Map edit undone."); diff --git a/Source/Gen/Ambient.h b/Source/Gen/Ambient.h index f4c4fb7..efb699d 100644 --- a/Source/Gen/Ambient.h +++ b/Source/Gen/Ambient.h @@ -21,6 +21,7 @@ #include #include #include "Roles.h" +#include "../Model/ArpSettings.h" #include "../Model/Scene.h" namespace ksp::gen @@ -78,6 +79,8 @@ inline std::string apply (ProjectData& project, const Params& p, int firstTrack return "variations must be 1-16"; if (firstTrack <= 1 && lastTrack >= 1) model::setDrumMode (project, false); // track 1 plays notes here: a project left in drum mode by a style would hide them + for (int t = std::max (2, firstTrack); t <= lastTrack; ++t) + model::setArpOn (project, t, false); // a track left on its arpeggiator would arpeggiate the patterns instead of playing them for (int t = firstTrack; t <= lastTrack; ++t) for (int v = 0; v < p.variations; ++v) { diff --git a/Source/Gen/Arrangement.h b/Source/Gen/Arrangement.h index c7ea865..2e1dc34 100644 --- a/Source/Gen/Arrangement.h +++ b/Source/Gen/Arrangement.h @@ -152,6 +152,7 @@ inline std::string applyArrangement (ProjectData& project, const Style& s, const return "track 1 pattern " + std::to_string (level + 1) + ": " + e; for (int t = 2; t <= 4; ++t) { + model::setArpOn (project, t, false); // the patterns play on the sequencer const auto& ts = s.tracks[t - 2]; for (int level = 0; level < kLevels; ++level) for (int chord = 0; chord < kChords; ++chord) diff --git a/Source/Gen/Styles.h b/Source/Gen/Styles.h index 27366c4..774372c 100644 --- a/Source/Gen/Styles.h +++ b/Source/Gen/Styles.h @@ -231,6 +231,8 @@ inline std::string applyStyle (ProjectData& project, const Style& s, const Param if (t == 1) if (auto e = clearTrack1Sequencer (project, p); ! e.empty()) return e; + if (t >= 2) + model::setArpOn (project, t, false); // the style's patterns play on the sequencer for (int v = 0; v < p.variations; ++v) { std::string e; diff --git a/Source/Model/ArpSettings.h b/Source/Model/ArpSettings.h index aeadc9d..49046f8 100644 --- a/Source/Model/ArpSettings.h +++ b/Source/Model/ArpSettings.h @@ -71,6 +71,18 @@ inline ArpSettings readArp (ProjectData& p, int track, int pat) return a; } +/** Turns the arpeggiator of track 2..4 on or off (off = the track plays its sequencer patterns). False when the project lacks the value. */ +inline bool setArpOn (ProjectData& p, int track, bool on) +{ + if (track < 2 || track > 4) + return false; + int* m = cell (p, item::track (track), ap::mode, { 1 }); + if (m == nullptr || *m == kUnreadable) + return false; + *m = on ? (*m | ap::kArpOnBit) : (*m & ~ap::kArpOnBit); + return true; +} + /** Writes mode and octave of the pattern and the swing, division and arp on/off of the track. Returns false when the project lacks those values. */ inline bool writeArp (ProjectData& p, int track, int pat, const ArpSettings& a) { diff --git a/Source/Model/ProjectSettings.h b/Source/Model/ProjectSettings.h index 0a3cfe0..afb8984 100644 --- a/Source/Model/ProjectSettings.h +++ b/Source/Model/ProjectSettings.h @@ -65,6 +65,16 @@ inline bool writeSettings (ProjectData& p, double tempo, int swing) return true; } +/** The MIDI channel (1..16) the KeyStep plays track 0..3 on: the project's override when set, else the default global channel + (track + 1). Track 1 in drum mode plays on the drum channel (10; a per-project drum channel is not decoded yet). */ +inline int effectiveChannel (ProjectData& p, int track, bool drumMode) +{ + if (track == 0 && drumMode) + return 10; + const int* c = cell (p, item::project, prs::channel, { static_cast (track + 1) }); + return c != nullptr && *c != kUnreadable && *c >= 1 && *c <= 16 ? *c : track + 1; +} + /** Sets the MIDI channel override of a track (track 0..3; channel 0 = global, 1..16). Item 0x78 param 123 [track 1..4]: seen on the device as 7 for a track set to channel 7, 1 for channel 1. Returns false when the project lacks the value. */ inline bool writeChannel (ProjectData& p, int track, int channel) diff --git a/docs/manual/04-studio-app.md b/docs/manual/04-studio-app.md index 4595b3f..54d9072 100644 --- a/docs/manual/04-studio-app.md +++ b/docs/manual/04-studio-app.md @@ -105,8 +105,9 @@ Icon: the white chassis, black display strip and the four track colours, drawn b **mood** (Ambient: Classic, Ethereal, Dark, Meditative; House: Deep, Chill, Melodic; one each for Techno and Drum & bass), a **seed** (New seed rolls a number; the same seed gives the same music), whether the scenes **chain** the variations and whether the time divisions are **slow**, and **Generate into this project**: patterns 1-4 of the four tracks and scenes 1-8 of the project that is open are replaced (drums, the arpeggiator - and other patterns are left alone). A style instead puts track 1 in drum mode, writes drum patterns 1-4 there (default drum map, GM notes), + settings and other patterns are left alone; tracks 2-4 are switched to the sequencer, `setArpOn (t, false)`, and the scenes written lose their mutes). A style instead puts track 1 in drum mode, writes drum patterns 1-4 there (default drum map, GM notes), patterns 1-4 of tracks 2-4 (bass, chords, lead, with their division and Mono/Poly) and scenes 1-4; and the project's tempo (techno 130, house 118-128, dnb 174 BPM). While - track 1 is in drum mode its instrument listens on channel 10 (the KeyStep's default Drum output). Then play it through the instruments with the KeyStep, hear it, edit it, **Save** it to the library and load it into + track 1 is in drum mode its instrument listens on channel 10 (the KeyStep's default Drum output); the other instruments listen on the project's + 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). diff --git a/docs/manual/user/06-instruments.md b/docs/manual/user/06-instruments.md index 828957e..0a80c94 100644 --- a/docs/manual/user/06-instruments.md +++ b/docs/manual/user/06-instruments.md @@ -36,7 +36,8 @@ others are dimmed. Click anywhere on a card (its controls too) to select its tra - **M**, which mutes (lights red), and **S**, which solos (lights blue): while any track is soloed only the soloed tracks sound (you can solo several). -Notes arrive on the track's MIDI channel (T1 = 1 ... T4 = 4, the KeyStep defaults). **Audio...** picks the output +Notes arrive on the track's MIDI channel (T1 = 1 ... T4 = 4, the KeyStep defaults; drums on 10). When the project sets its own +channel for a track (**Project Map**, the T1-T4 channel boxes), the instrument listens on that one. **Audio...** picks the output device; **Test note** plays a C3 on every loaded instrument without the KeyStep. **FX...** opens four insert slots per track and a master chain of four; each has **Edit** and **Byp** (bypass). A master diff --git a/docs/manual/user/07-generate.md b/docs/manual/user/07-generate.md index 6f7866d..fb6d4a6 100644 --- a/docs/manual/user/07-generate.md +++ b/docs/manual/user/07-generate.md @@ -18,12 +18,13 @@ 6. Press **Generate into this project**. **Ambient** replaces patterns 1-4 of the four tracks and scenes 1-8 of the open project, and puts track 1 back in -sequencer mode (a style had put it in drum mode). The arpeggiator, the drum patterns and the other patterns are left alone. +sequencer mode (a style had put it in drum mode). Tracks 2-4 are switched to **Seq** too: a track left on **Arp** would +arpeggiate the generated notes instead of playing them. The drum patterns, the arpeggiator settings and the other patterns are left alone. **Techno, House and Drum & bass** put track 1 in drum mode and write its drum patterns 1-4 (the last one ends with a fill), plus patterns 1-4 of track 2 (bass), track 3 (chords) and track 4 (lead), and scenes 1-4. The drums use the KeyStep's default drum map (kick 36, snare 38, clap 39, closed hat 42, open hat 46), so load a General MIDI style kit on -track 1; the app sends track 1 to its instrument on the drum channel (10). The project also gets the style's tempo +track 1; the app sends track 1 to its instrument on the drum channel (10). Tracks 2-4 are switched to **Seq** (not **Arp**). The project also gets the style's tempo (techno 130; house deep 128, chill 118, melodic 124; drum & bass 174 BPM); change it on the KeyStep if you like. Track 1 then shows the drum grid (see *The grid*). **Musical arrangement** (Techno, House, Drum & bass; on by default) writes eight scenes with an energy curve instead of the @@ -34,6 +35,9 @@ and pattern 16 of every track is empty, to silence a track. Each scene chains fo between levels, and all four tracks play the same chord in a scene. The KeyStep does not change scenes by itself: you play them live, so think of the eight scenes as a map. The Mood and seed still change the notes. +Every generator clears the track mutes of the scenes it writes, so no generated track starts silenced by a mute left from +the project's old scenes. Mute tracks again in the **Project Map** if you want. + The window and the message after generating say which track plays what, so you know which instrument to load on each: styles put the **drums on track 1**, then bass (track 2), chords (track 3) and a lead melody or bells (track 4); Ambient puts a drone on track 1, pad chords on 2, a lead melody on 3 and bells on 4. diff --git a/tests/gen_test.cpp b/tests/gen_test.cpp index b45cfb7..43f1aab 100644 --- a/tests/gen_test.cpp +++ b/tests/gen_test.cpp @@ -345,6 +345,39 @@ int main() CHECK (10, notes > 0 && readPattern (pr, 2, 1).root == 7 && readPattern (pr, 2, 1).scale == 1); } + // 11. Generated tracks 2-4 play on the sequencer: a track left on its arpeggiator goes back to Seq (its arp settings stay), + // and the scenes written lose their mutes + { + auto armed = [] (ProjectData& pr) { + for (int t = 2; t <= 4; ++t) + { + setArpOn (pr, t, true); + *cell (pr, item::track (t), ap::config, { 1 }) = 20; // Exclu, octave 0 + } + *cell (pr, item::scene, sp::mute, { 1 }) = 6; + }; + auto seq = [] (ProjectData& pr) { + bool ok = true; + for (int t = 2; t <= 4; ++t) + ok = ok && ! readArp (pr, t, 1).on && readArp (pr, t, 1).mode == 2; + return ok; + }; + const auto& house = *gen::findStyle ("house"); + auto a1 = blank(); + armed (a1); + CHECK (11, readArp (a1, 3, 1).on); + CHECK (11, gen::apply (a1, params).empty() && gen::applyScenes (a1, params).empty() && seq (a1) && readScene (a1, 1).muteRaw == 0); + auto a2 = blank(); + armed (a2); + CHECK (11, gen::applyStyle (a2, house, gen::styleParams (house, 4)).empty() && seq (a2)); + auto a3 = blank(); + armed (a3); + CHECK (11, gen::applyArrangement (a3, house, gen::styleParams (house, 4)).empty() && seq (a3) && readScene (a3, 1).muteRaw == 0); + auto a4 = blank(); // only track 1: the other tracks keep their arpeggiator + armed (a4); + CHECK (11, gen::applyStyle (a4, house, gen::styleParams (house, 4), 1, 1).empty() && readArp (a4, 2, 1).on); + } + std::printf ("track 3 pattern 1: %s\n", gen::preview (a).c_str()); std::printf (failures == 0 ? "gen_test: OK\n" : "gen_test: %d FAIL\n", failures); return failures == 0 ? 0 : 1; diff --git a/tests/projectmap_test.cpp b/tests/projectmap_test.cpp index 385a06b..4cff7bd 100644 --- a/tests/projectmap_test.cpp +++ b/tests/projectmap_test.cpp @@ -123,6 +123,9 @@ int main() CHECK (10, writeChannel (q, 3, 12) && readSettings (q).channel[3] == 12); CHECK (10, writeChannel (q, 0, 99) && readSettings (q).channel[0] == 16); CHECK (10, ! writeChannel (q, 4, 1)); + // the channel an instrument listens on: the override, else the default (track + 1); drums on 10 + CHECK (10, effectiveChannel (q, 0, false) == 16 && effectiveChannel (q, 0, true) == 10 && effectiveChannel (q, 2, false) == 7); + CHECK (10, writeChannel (q, 3, 0) && effectiveChannel (q, 3, false) == 4); } std::printf (failures == 0 ? "projectmap_test OK\n" : "projectmap_test FAILED\n");