From 9eacc6cdee424c53fa8f4383440cc2533decf29a Mon Sep 17 00:00:00 2001 From: John Hoar Date: Wed, 29 Nov 2017 00:02:38 +0100 Subject: [PATCH] Locks affect randomised pitches, update README --- README.md | 31 ++++++----- src/Arpeggiator.cpp | 124 ++++++++++++++++++++++++++------------------ 2 files changed, 92 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index fbf9dcc..47fdc86 100644 --- a/README.md +++ b/README.md @@ -23,37 +23,42 @@ Scared by the anarchy of unconstrained tone? Want to come back to that familiar * OUT: Quantised note (1V/OCT) * Top row of outputs: Gates triggered according to the scale degree of the note quantised -This is largely done, will probably do some internal plumbing to use PulseGenerators in the future. ## Arpeggiator Need some more predictable variety in your patches? Why not try the Arpeggiator from Amalgamated Harmonics? Guaranteed 17.5% more music per hour. How it is (supposed to) work is the following: -* The Arpeggiator plays a Sequence, composed on 1 or more Cycles, each Cycle consisting of up to 6 notes. -* At the bottom there are 6 inputs which represent up to 6 pitches (1V/OCT) which will played during a Cycle. The DIR switch controls whether the pitch are read left-to-right (up position) or right-to-down (down). Empty inputs are ignored. -* A Cycle will repeated a number of times according to the STEPS input. -* Each subsequent Cycle after the first will be shifted by the number of semi-tones controlled by the DIST input. The shift is either up or down depending on the setting of the Seq DIR switch. This should be replaced with a wider selection of patterns -* A Sequence is triggered from a gate on the TRIG input. -* The Sequence is... err... sequenced from the CLOCK input, so the CLOCK source should be running faster the TRIG source. +* The Arpeggiator plays a sequence, composed on 1 or more arpeggios, each arpeggio consisting of up to 6 notes. +* At the bottom there are 6 inputs which represent up to 6 pitches (1V/OCT) which will played in an arpeggios. +* The ARP switch controls whether the pitch are played left-to-right (L-R), right-to-left (R-L) or a random selection of the pitches and selected (RND). In the RND position pitches can be repeated. Empty inputs are ignored. +* A arpeggio will repeated a number of times according to the STEPS input. +* Each subsequent arpeggio after the first will be shifted by the number of semi-tones controlled by the DIST input. The shift is either up or down depending on the setting of the SEQ switch; ASC shifts the arpeggio up, DSC shifts the arpeggio down and RND shifts the arpeggio up or down 50/50; in this position it is a random walk +* A sequence is triggered from a gate on the TRIG input. +* The sequence is... err... sequenced from the CLOCK input, so the CLOCK source should be running faster the TRIG source. * The output pitch is on the OUT output (1V/OCT), with a gate fired on the GATE. -* When a cycle completes, a gate if fired on the EOC output, and similarly when the whole sequence completes there is gate on the EOS output -* The LOCK button locks the pitches to the value in the current cycle, both within the running sequence and after, rather like turning the probability knob on the Turing Machine to the far right. When unlocked the pitches will be scanned at the start of every cycle. +* When an arpeggio completes, a gate is fired on the EOC output, and similarly when the whole sequence completes there is gate on the EOS output +* The LOCK button locks the pitches, SEQ and ARP settings to the value in the current arpeggio, both within the running sequence and after. When unlocked the pitches will be scanned at the start of every cycle. + +A small display shows the active STEPS and DIST in the currently active sequence, with the values being read from the inputs shown in square brackets. The active values of the SEQ and ARP parameters are alos shown + +In summary: * P1 - P6: Input pitches; these will be scanned left to right and form the notes to be played in each cycle. Empty inputs are ignored * STEPS: Number of repetitions of a cycle (max 16) * DIST: Number of semi-tones used for shifting pitches between cycles (max 10) -* DIR: Scan the pitches left-to-right (up) or right-to-left (down) -* CLOCK: Input clock for the sequenced notes +* ARP: Scan the pitches left-to-right (L-R) or right-to-left (R-L), or a random selection of pitches (RND) +* SEQ: Shifts the arpeggio up (ASC), down (DSC) or randonly up or down (RND) +* CLOCK: Input clock for the arpeggio * TRIG: Input gate to trigger the sequence -* LOCK: Lock the pitches +* LOCK: Lock the pitches, SEQ and ARP setting, including the randomised notes * OUT: Output sequenced note (1V/OCT) * GATE: Output gate fired for each sequenced note. * EOC: Output gate fired when a cycle ends * EOS: Output gate fired when the whole series of cycles is finished -Almost something usable. The gaping void in the middle of the panel is where the selection mechanism for patterns will go. I should stop procrastinating and do it. +This is complete for the time being. There are a few new ideas which might materialise in the future. ### Notes diff --git a/src/Arpeggiator.cpp b/src/Arpeggiator.cpp index 990c04e..ca42d78 100644 --- a/src/Arpeggiator.cpp +++ b/src/Arpeggiator.cpp @@ -43,9 +43,6 @@ struct Arpeggiator : Module { NUM_LIGHTS }; - int PATT_UP[17] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; - int PATT_DN[17] = {0,-1,-2,-3,-4,-5,-6,-7,-8,-9,-10,-11,-12,-13,-14,-15,-16}; - Arpeggiator() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} void step() override; @@ -66,7 +63,6 @@ struct Arpeggiator : Module { int inputSDir; int sDir = 0; - int *sDirection; // FIXME eventually remove this int inputStep = 0; int nStep = 0; @@ -86,6 +82,7 @@ struct Arpeggiator : Module { int cycleI = 0; int stepsRemaining; int cycleRemaining; + int currDist = 0; bool debug = false; int stepX = 0; @@ -148,6 +145,7 @@ void Arpeggiator::step() { newCycle = true; stepI = 0; cycleI = 0; + currDist = 0; // Set flag to advance sequence advance = true; @@ -157,15 +155,15 @@ void Arpeggiator::step() { std::cout << "Advance from Trigger" << std::endl; } - // Need this to stop clock gates from immediately advancing sequence. Probably should be a pulse + // Need this to stop clock gates from immediately advancing sequence, and for resetting the stepcount. + // Probably should be a pulse wasTriggered = true; } - - // Set the pitches - // if there is a new cycle and the pitches are unlocked or we have been triggered + bool readCycleSettings = false; + // if there is a new cycle and the pitches are unlocked, or we have been triggered if (isTriggered && !locked) { if (debug) { std::cout << "Read pitches from trigger: " << isTriggered << std::endl; @@ -173,13 +171,14 @@ void Arpeggiator::step() { readCycleSettings = true; } - if (isClocked && isRunning && newCycle && !locked) { + if (isClocked && newCycle && !locked) { if (debug) { std::cout << "Read pitches from clock: " << isClocked << isRunning << newCycle << !locked << std::endl; } readCycleSettings = true; } + // Capture the settings if (readCycleSettings) { // Freeze sequence params @@ -187,16 +186,7 @@ void Arpeggiator::step() { nDist = inputDist; pDir = inputPDir; sDir = inputSDir; - - // Deal with RND setting // FIXME this will change as random will work differently - if (sDir == 1) { - if (rand() % 2 == 0) { - sDir = 0; - } else { - sDir = 2; - } - } - + cycleLength = 0; // Read out voltages @@ -219,11 +209,13 @@ void Arpeggiator::step() { } + // Slightly hacky but of code to catch case when there are no steps, or we have been triggered after being set to 0 steps if (nStep == 0) { return; // No steps, abort } else { // If we had reachd the end of the sequence or just transitioned from 0 step length, reset the counter - if (stepsRemaining == 0) { + if (stepsRemaining == 0 || wasTriggered) { + if (debug) { std::cout << "Hard reset stepsRemaining" << std::endl; } stepsRemaining = nStep; } } @@ -235,6 +227,7 @@ void Arpeggiator::step() { advance = true; } + // Advance the sequence if (advance) { if (debug) { std::cout << "Advance S: " << stepI << @@ -243,38 +236,54 @@ void Arpeggiator::step() { " cRemain: " << cycleRemaining << std::endl; } + // Starting a new cycle if (newCycle) { if (debug) { std::cout << "New Cycle: " << newCycle << std::endl; } - // Read the pattern - // Calculate the subsequent pitches, need direction, number of steps and step size - switch (sDir) { - case 0: sDirection = PATT_DN; break; - case 1: // This might happen on reset, if there is not valid input - case 2: sDirection = PATT_UP; break; - default: sDirection = PATT_UP; + // Deal with RND setting, when sDir == 1, force it up or down + if (sDir == 1) { + if (rand() % 2 == 0) { + sDir = 0; + } else { + sDir = 2; + } } - - for (int i = 0; i < cycleLength; i++) { - - int target; - // Read the pitches according to direction, but we should do this for the sequence? - switch (pDir) { - case 0: target = cycleLength - i - 1; break; // DOWN - case 2: target = i; break; // UP - default: target = i; break; // For random case, read randomly from array, so order does not matter + + // Only starting moving after the first cycle + if (stepI) { + switch (sDir) { + case 0: currDist--; break; + case 2: currDist++; break; + default: ; } - - float dV = semiTone * nDist * sDirection[stepI]; - pitches[i] = clampf(inputPitches[target] + dV, -10.0, 10.0); + } + + if (!locked) {// Pitches are locked, and so is the order. This keeps randomly generated arps fixed when locked + for (int i = 0; i < cycleLength; i++) { + + int target; + + // Read the pitches according to direction, but we should do this for the sequence? + switch (pDir) { + case 0: target = cycleLength - i - 1; break; // DOWN + case 1: target = rand() % cycleLength; break; // RANDOM + case 2: target = i; break; // UP + default: target = i; break; // For random case, read randomly from array, so order does not matter + } + + // How many semi-tones do we need to shift + float dV = semiTone * nDist * currDist; + pitches[i] = clampf(inputPitches[target] + dV, -10.0, 10.0); - if (debug) { - std::cout << i << " stepI: " << - " dV:" << dV << - " in: " << inputPitches[target] << - " out: " << pitches[target] << std::endl; - } + if (debug) { + std::cout << i << " stepI: " << stepI << + " dV:" << dV << + " target: " << target << + " in: " << inputPitches[target] << + " out: " << pitches[target] << std::endl; + } + } } if (debug) { @@ -285,29 +294,38 @@ void Arpeggiator::step() { std::cout << std::endl; } + // Done, reset flag newCycle = false; } - if (pDir == 1) { - outVolts = pitches[rand() % cycleLength]; - } else { - outVolts = pitches[cycleI]; - } + // Finally set the out voltage + outVolts = pitches[cycleI]; if (debug) { std::cout << "V = " << outVolts << std::endl; } + // Update counters cycleI++; cycleRemaining--; + // Pulse the output gate gatePulse.trigger(5e-3); + // Reached the end of the cycle if (cycleRemaining == 0) { + + // Completed 1 step stepI++; stepsRemaining--; + + /// Reset the cycle counters cycleRemaining = cycleLength; cycleI = 0; + + // Need to start a new cycle newCycle = true; + + // Pulse the EOC gate eocPulse.trigger(5e-3); if (debug) { std::cout << "Cycle Finished S: " << stepI << @@ -318,6 +336,7 @@ void Arpeggiator::step() { } } + // Reached the end of the sequence if (stepsRemaining == 0) { if (debug) { std::cout << "Sequence Finished S: " << stepI << @@ -326,12 +345,17 @@ void Arpeggiator::step() { " cRemain: " << cycleRemaining << " flag:" << isRunning << std::endl; } + // Update the flag isRunning = false; + + // Pulse the EOS gate eosPulse.trigger(5e-3); } } + + // Set the outputs float delta = 1.0 / engineGetSampleRate(); bool sPulse = eosPulse.process(delta);