Skip to content

Commit

Permalink
Added second chord-to-scale option that preserves P4, P5 and M7
Browse files Browse the repository at this point in the history
  • Loading branch information
Yves Parès committed Mar 2, 2023
1 parent 27a14ae commit 58a2753
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 79 deletions.
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,26 @@ different live players have different preferences.

| Parameter name | Default value | Possible values | Documentation |
|--------------------------------|---------------|-----------------|---------------|
|**Hold current chord/scale**|`Off`|`On` or `Off`|Latch onto the current chord/scale until deactivated. This parameter is never saved, and is meant to be mapped to some MIDI control, like a sustain pedal or a switch. **Pre-mapping chord processing** changes are not applied while in this state.|
|**Reference pattern note**|`60 (C3)`|A MIDI note|On pattern tracks, the note that will always be mapped to the first (lowest) degree of the currently playing chord|
|**Turn chord into scale**|`None`|Choose from:|Instead of mapping pattern notes only to degrees of the current chord, you can map them to a scale that makes sense over the current chord. It will _always_ consider that the root is the lowest degree of your chord|
| | |`None`|Do not derive a scale, map pattern notes only to chord degrees|
| | |`Add whole steps (nat7 by default)`|Between each chordal tone, unless the chord has an extension tone that fits, adds a tone following the "whole-step technique"[^4]. If your chord does not have a 7th, give the scale a ♮7|
| | |`Add whole steps (b7 by default)`|Same as above, but if your chord does not have a 7th, give the scale a ♭7|
|**Pre-mapping chord processing**|`None`|Choose from:|Instead of mapping pattern notes directly to notes of the current chord, apply first some transformation to the current chord. For instance, Arpligner can derive from the chord a scale that makes sense over it. Note that it will _always_ consider that the root is the lowest degree of your input chord. However, if your input chords are inverted, you can double the root at the bass (any octave would do) to make sure Arpligner sees the correct root|
| | |`None`|Usual mode: map pattern notes directly to chord notes|
| | |`Ignore bass note`|Mapping process always ignores the lowest chord note. Can be useful if your chord track always features a note doubled at the bass|
| | |`Turn into scale - Add whole steps`|Between each chordal tone and after the last chordal tone, unless the chord has an extension tone that allows Arpligner to guess the degree that should go there, adds a tone following the "Whole-step technique"[^4]. Prefer this if all your input chords are at least seventh chords.|
| | |`Turn into scale - P4,P5,Maj7 by default`|Same as above, but defaults to "less jazzy" scales when your input chord lacks an 11th, a 5th or a 7th, ie. when Arpligner has to guess what the 4th, 5th or 7th degree of the scale should be. Prefer this if your input chords are just triads, or even just simple two-note intervals|
|**Pattern notes mapping**|`Semitone to degree`|Choose from:|How to map midi note codes on pattern tracks to actual notes|
| | |`Always leave unmapped`|No pattern note is mapped, and therefore always use the **Unmapped notes behaviour**|
| | |`Semitone to degree`|Going up/down one _semitone_ in the pattern track means going up/down one degree in the chord|
| | |`White note to degree`|Going up/down one _white key_ in the pattern track means going up/down one degree in the chord. Black keys are not mapped|
|**Pattern octave wraparound**|`[Dynamic] After all chord degrees`|Choose from:|When should your pattern wrap around the chord, ie. play it at a higher/lower octave. _(Used depending on the mapping setting above)_|
| | |`No wraparound`|Never. Pattern notes past the last chord degree are unmapped, as well as notes below the Reference note|
| | |`[Dynamic] After all chord degrees`|Repeatedly go up one octave (and back to the first chord degree) as soon as we're past the last chord degree, or down one octave (and to the _last_ chord degree) in the other direction. No pattern note is therefore ever left unmapped.|
| | |`[Fixed] Every XXth pattern note`|Use a fixed wraparound setting. You will go up one octave (and back to the first chord degree) every time your pattern goes up `XX` notes, and down one octave (and to the _last_ chord degree) every time your pattern goes _down_ `XX` notes. Intermediary pattern notes that do not correspond to chord degrees are unmapped.|
| | |`Semitone to degree`|Going up/down one _semitone_ in the pattern track means going up/down one degree in the chord/scale|
| | |`White note to degree`|Going up/down one _white key_ in the pattern track means going up/down one degree in the chord/scale. Black keys are not mapped|
|**Pattern octave wraparound**|`[Dynamic] After all degrees`|Choose from:|When should your pattern wrap around the chord/scale, ie. play it at a higher/lower octave. _(Used depending on the mapping setting above)_|
| | |`No wraparound`|Never. Pattern notes past the last chord/scale degree are unmapped, as well as notes below the Reference note|
| | |`[Dynamic] After all degrees`|Repeatedly go up one octave (and back to the first chord/scale degree) as soon as we're past the last degree, or down one octave (and to the _last_ degree) in the other direction. No pattern note is therefore ever left unmapped.|
| | |`[Fixed] Every XXth pattern note`|Use a fixed wraparound setting. You will go up one octave (and back to the first chord/scale degree) every time your pattern goes up `XX` notes, and down one octave (and to the _last_ degree) every time your pattern goes _down_ `XX` notes. Intermediary pattern notes that do not correspond to degrees are unmapped.|
|**Unmapped notes behaviour**|`Silence`|Choose from:|What to do when the mapping or wraparound settings above have left some pattern notes unmapped|
| | |`Silence`|Do not output any note|
| | |`Use as is`|Output the pattern note as it is|
| | |`Transpose from 1st degree`|Use Arpligner as a "dynamic" transposer: ignore all chord degrees besides the first (lowest) one. Pattern notes are just transposed accordingly. This allows you to play notes that are outside the current chord, but keeping your patterns centered around the reference note|
| | |`Play all degrees up to note`|Play the full chord, using the played note as a filter (all chord degrees above will be silenced)|
|**Hold current chord**|`Off`|`On` or `Off`|Latch onto the current chord/scale until deactivated. This parameter is never saved, and is meant to be mapped to some MIDI control, like a sustain pedal or a switch.|
| | |`Transpose from 1st degree`|Use Arpligner as a "dynamic" transposer: ignore all chord/scale degrees besides the first (lowest) one. Pattern notes are just transposed accordingly. This allows you to play notes that are outside the current chord/scale, but keeping your patterns centered around the reference note|
| | |`Play all degrees up to note`|Play the full chord/scale, using the played note as a filter (all degrees above will be silenced)|

## Current limitations

Expand Down
119 changes: 77 additions & 42 deletions Source/Arp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ AudioProcessor* JUCE_CALLTYPE createPluginFilter()
// Functions that compute the mappings of input pattern notes:
namespace Mapping {

void mapToChordDegree(PatternNotesWraparound::Enum wrapMode,
const Chord& curChord,
void mapToNoteSet(PatternNotesWraparound::Enum wrapMode,
const NoteSet& curNoteSet,
int degreeNum,
Array<NoteNumber>& thisNoteMappings) {
int numChordDegrees = curChord.size();
int curNoteSetSize = curNoteSet.size();

if ((degreeNum < 0 || degreeNum >= numChordDegrees) &&
if ((degreeNum < 0 || degreeNum >= curNoteSetSize) &&
wrapMode == PatternNotesWraparound::NO_WRAPAROUND ||
numChordDegrees == 0)
curNoteSetSize == 0)
return;

int numValidDegrees =
(wrapMode <= PatternNotesWraparound::AFTER_ALL_CHORD_DEGREES)
? numChordDegrees
(wrapMode <= PatternNotesWraparound::AFTER_ALL_DEGREES)
? curNoteSetSize
: wrapMode;
int wantedDegree;
if (degreeNum >= 0)
Expand All @@ -44,15 +44,15 @@ namespace Mapping {
}
int wantedOctaveShift = floor((float)degreeNum / (float)numValidDegrees);

if (wantedDegree < numChordDegrees)
thisNoteMappings.add(curChord[wantedDegree] + 12 * wantedOctaveShift);
if (wantedDegree < curNoteSetSize)
thisNoteMappings.add(curNoteSet[wantedDegree] + 12 * wantedOctaveShift);
}

void mapPatternNote(NoteNumber referenceNote,
PatternNotesMapping::Enum mappingMode,
PatternNotesWraparound::Enum wrapMode,
UnmappedNotesBehaviour::Enum unmappedBeh,
const Chord& curChord,
const NoteSet& curNoteSet,
NoteNumber noteCodeIn,
Array<NoteNumber>& thisNoteMappings) {
int offsetFromRef = noteCodeIn - referenceNote;
Expand All @@ -69,26 +69,26 @@ namespace Mapping {
for (int i = referenceNote; i != noteCodeIn; i += sign)
if (MidiMessage::isMidiNoteBlack(i))
absOffset--;
mapToChordDegree(wrapMode, curChord, sign * absOffset, thisNoteMappings);
mapToNoteSet(wrapMode, curNoteSet, sign * absOffset, thisNoteMappings);
}
break;
default:
mapToChordDegree(wrapMode, curChord, offsetFromRef, thisNoteMappings);
mapToNoteSet(wrapMode, curNoteSet, offsetFromRef, thisNoteMappings);
break;
}

if (thisNoteMappings.size() == 0) { // If the note has not been mapped yet:
switch (unmappedBeh) {
case UnmappedNotesBehaviour::SILENCE:
break;
case UnmappedNotesBehaviour::PLAY_FULL_CHORD_UP_TO_NOTE:
for (NoteNumber chdNote : curChord) {
case UnmappedNotesBehaviour::PLAY_ALL_DEGREES_UP_TO_NOTE:
for (NoteNumber chdNote : curNoteSet) {
if (chdNote <= noteCodeIn)
thisNoteMappings.add(chdNote);
}
break;
case UnmappedNotesBehaviour::TRANSPOSE_FROM_FIRST_DEGREE:
thisNoteMappings.add(curChord[0] + offsetFromRef);
thisNoteMappings.add(curNoteSet[0] + offsetFromRef);
break;
case UnmappedNotesBehaviour::USE_AS_IS:
thisNoteMappings.add(noteCodeIn);
Expand Down Expand Up @@ -180,40 +180,75 @@ void Arp::runArp(MidiBuffer& midibuf) {
processPatternNotes(chd, ptrnNoteOns, ptrnNoteOffs, midibuf);
}

void Arp::turnNoteSetToUseIntoScale(PreMappingChordProcessing::Enum chordProc) {
int numChordNotes = mNoteSetToUse.size();
NoteNumber chordRoot = mNoteSetToUse[0];

// We first compute the octave shift to apply, by computing the distance
// between the lowest note and the average of the rest of the notes
float avgNote = 0;
for (int i = 1; i<numChordNotes; i++)
avgNote += mNoteSetToUse[i];
avgNote = avgNote / (numChordNotes - 1);
NoteNumber scaleRoot = chordRoot + 12*round((avgNote - chordRoot)/12.0f);

NoteSet compactChord;
// Then we "regroup" all the chord notes so they fit into one octave,
// starting from the lowest chord note transposed to the average wanted
// octave:
for (auto note : mNoteSetToUse)
compactChord.add(scaleRoot + (note - chordRoot)%12);

NoteSet scale(compactChord);
// Then we fill in the gaps by adding 1 whole step to each degree just
// before a gap (a "gap" being any interval strictly larger than a whole
// step)...
for (int i=0; i<scale.size()-1; i++)
if (scale[i+1] - scale[i] > 2)
scale.add(scale[i] + 2);
// ...and to the last degree, until we have a full scale up to a 7th:
while (scale.getLast() < scaleRoot + 10)
scale.add(scale.getLast() + 2);

// Then if needed, we correct the 4th, 5th and 7th:
if (scale.size() == 7 &&
chordProc == PreMappingChordProcessing::ADD_WHOLE_STEPS_DEF_P4_P5_MAJ7) {
if (!compactChord.contains(scale[3])) {
scale.remove(3);
scale.add(scaleRoot + 5); // Perfect 4th
}
if (!compactChord.contains(scale[4])) {
scale.remove(4);
scale.add(scaleRoot + 7); // Perfect 5th
}
if (!compactChord.contains(scale[6])) {
scale.remove(6);
scale.add(scaleRoot + 11); // Major 7th
}
}

// Finally, we override mNoteSetToUse:
mNoteSetToUse.swapWith(scale);
}

void Arp::processPatternNotes(ChordStore* chd, Array<MidiMessage>& noteOns, Array<MidiMessage>& noteOffs, MidiBuffer& midibuf) {
auto chordToScaleMode = (ChordToScale::Enum)chordToScale->getIndex();
auto chordProc = (PreMappingChordProcessing::Enum)preMappingChordProcessing->getIndex();
auto mappingMode = (PatternNotesMapping::Enum)patternNotesMapping->getIndex();
auto wrapMode = (PatternNotesWraparound::Enum)patternNotesWraparound->getIndex();
auto unmappedBeh = (UnmappedNotesBehaviour::Enum)unmappedNotesBehaviour->getIndex();
auto referenceNote = firstDegreeCode->getIndex();
auto updateState = !holdCurState->get();

if (updateState) {
chd->getCurrentChord(mChordToUse, mShouldProcess, mShouldSilence);

// If wanted, pre-process the current chord to turn it into a scale:

if (chordToScaleMode != ChordToScale::NONE) {
Chord scale;
// We first "regroup" all the chord notes so they fit into one octave,
// starting from lowest chord note:
NoteNumber root = mChordToUse[0];
for (auto note : mChordToUse)
scale.add(root + (note - root)%12);
// If we don't have a 7th, we add it:
if (scale.getLast() < root + 10) {
if (chordToScaleMode == ChordToScale::ADD_WHOLE_STEPS_DEF_NAT7)
scale.add(root + 11);
else
scale.add(root + 10);
}
// Then we fill in the gaps by adding 1 whole step to each degree below a
// gap (a gap being any interval strictly larger than a whole step):
for (int i=0; i<scale.size()-1; i++)
if (scale[i+1] - scale[i] > 2)
scale.add(scale[i]+2);
// Then, we override curChord:
mChordToUse.swapWith(scale);
chd->getCurrentChord(mNoteSetToUse, mShouldProcess, mShouldSilence);

if (mShouldProcess && !mShouldSilence) {
// If wanted, pre-process the current chord:
if (chordProc == PreMappingChordProcessing::NONE) {}
else if (chordProc == PreMappingChordProcessing::IGNORE_BASS_NOTE)
mNoteSetToUse.remove(0);
else if (mNoteSetToUse.size() >= 2)
turnNoteSetToUseIntoScale(chordProc);
}
}

Expand Down Expand Up @@ -250,7 +285,7 @@ void Arp::processPatternNotes(ChordStore* chd, Array<MidiMessage>& noteOns, Arra
mappingMode,
wrapMode,
unmappedBeh,
mChordToUse,
mNoteSetToUse,
noteCodeIn,
thisNoteMappings);
else // We map the note to itself
Expand Down
4 changes: 3 additions & 1 deletion Source/Arp.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ using Mappings = HashMap< NoteOnChan, Array<NoteNumber> >;
class Arp : public ArplignerAudioProcessor {
private:
ChordStore mLocalChordStore;
Chord mChordToUse;
NoteSet mNoteSetToUse;
bool mShouldProcess;
bool mShouldSilence;

Expand All @@ -46,6 +46,8 @@ class Arp : public ArplignerAudioProcessor {

void processPatternNotes(ChordStore* chd, Array<MidiMessage>&, Array<MidiMessage>&, MidiBuffer&);

void turnNoteSetToUseIntoScale(PreMappingChordProcessing::Enum);

//void finalizeMappings(MidiBuffer&);

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Arp);
Expand Down
2 changes: 1 addition & 1 deletion Source/ChordStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ void ChordStore::updateCurrentChord(WhenNoChordNote::Enum whenNoChordNoteVal,
mShouldSilence = false;
mShouldProcess = true;

Chord newChord;
NoteSet newChord;
for (Counters::Iterator i(mCounters); i.next();) {
newChord.add(i.getKey());
}
Expand Down
8 changes: 4 additions & 4 deletions Source/ChordStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
using namespace juce;

using NoteNumber = int;
using Chord = SortedSet<NoteNumber>;
using NoteSet = SortedSet<NoteNumber>;
using Counters = HashMap<NoteNumber, int>;


// A thread-safe way to keep track of the currently playing chord
class ChordStore {
private:
Counters mCounters;
Chord mCurrentChord;
NoteSet mCurrentChord;
bool mShouldProcess;
bool mShouldSilence;
bool mNeedsUpdate;
Expand Down Expand Up @@ -59,7 +59,7 @@ class ChordStore {
mNeedsUpdate = false;
}

virtual void getCurrentChord(Chord& chord, bool& shouldProcess, bool& shouldSilence) {
virtual void getCurrentChord(NoteSet& chord, bool& shouldProcess, bool& shouldSilence) {
chord = mCurrentChord;
shouldProcess = mShouldProcess;
shouldSilence = mShouldSilence;
Expand All @@ -80,7 +80,7 @@ class GlobalChordStore : public ChordStore {
ChordStore::flushCurrentChord();
}

void getCurrentChord(Chord& chord, bool& shouldProcess, bool& shouldSilence) override {
void getCurrentChord(NoteSet& chord, bool& shouldProcess, bool& shouldSilence) override {
// This will prevent a Pattern instance to access the current chord if the
// Global chord instance is still updating it
const ScopedReadLock lock(globalStoreLock);
Expand Down
20 changes: 11 additions & 9 deletions Source/PluginProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,21 @@ ArplignerAudioProcessor::ArplignerAudioProcessor()
(numMillisecsOfLatency = new AudioParameterInt
("numMillisecsOfLatency", "Global chord track lookahead (ms)", 0, 50, 15));

addParameter(holdCurState = new AudioParameterBool("holdCurState", "Hold current chord/scale", false));

StringArray notes;
for (int i = 0; i <= 127; i++)
notes.add(String(i) + " (" + MidiMessage::getMidiNoteName(i, true, true, 3) + ")");
addParameter(firstDegreeCode = new AudioParameterChoice
("firstDegreeCode", "Reference pattern note", notes, 60));

addParameter
(chordToScale = new AudioParameterChoice
("chordToScale", "Turn chord into scale",
StringArray{"None", "Add whole steps (nat7 by default)", "Add whole steps (b7 by default)"},
ChordToScale::NONE));
(preMappingChordProcessing = new AudioParameterChoice
("preMappingChordProcessing", "Pre-mapping chord processing",
StringArray{ "None", "Ignore bass note",
"Turn into scale - Add whole steps",
"Turn into scale - P4,P5,Maj7 by default "},
PreMappingChordProcessing::NONE));

addParameter
(patternNotesMapping = new AudioParameterChoice
Expand All @@ -78,16 +82,14 @@ ArplignerAudioProcessor::ArplignerAudioProcessor()
addParameter
(patternNotesWraparound = new AudioParameterChoice
("patternNotesWraparound", "Pattern octave wraparound", waModes,
PatternNotesWraparound::AFTER_ALL_CHORD_DEGREES));
PatternNotesWraparound::AFTER_ALL_DEGREES));

StringArray unmappedBehs = StringArray
{ "Silence", "Use as is", "Transpose from 1st degree", "Play all degrees up to note" };
addParameter
(unmappedNotesBehaviour = new AudioParameterChoice
("unmappedNotesBehaviour", "Unmapped notes behaviour", unmappedBehs,
UnmappedNotesBehaviour::SILENCE));

addParameter(holdCurState = new AudioParameterBool("holdCurState", "Hold current chord", false));
}

ArplignerAudioProcessor::~ArplignerAudioProcessor()
Expand Down Expand Up @@ -229,7 +231,7 @@ void ArplignerAudioProcessor::getStateInformation(MemoryBlock& destData)
s.writeInt(*numMillisecsOfLatency);
s.writeInt(*patternNotesWraparound);
s.writeInt(*unmappedNotesBehaviour);
s.writeInt(*chordToScale);
s.writeInt(*preMappingChordProcessing);
}

// Reload state info
Expand All @@ -244,5 +246,5 @@ void ArplignerAudioProcessor::setStateInformation(const void* data, int sizeInBy
*numMillisecsOfLatency = s.readInt();
*patternNotesWraparound = s.readInt();
*unmappedNotesBehaviour = s.readInt();
*chordToScale = s.readInt();
*preMappingChordProcessing = s.readInt();
}
Loading

0 comments on commit 58a2753

Please sign in to comment.