Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derive scale from current chord #16

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,20 +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|
|**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)|
| | |`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 Expand Up @@ -326,3 +332,7 @@ different live players have different preferences.
Arpligner is processing notes may cause stuck notes, please report an
issue here if this happens to you, preferably with a set of MIDI files
that will help me reproduce the problem

[^4]: This is an adaptation of the method described by Julian Bradley in
https://www.youtube.com/watch?v=Ro2dVvwzKNs. See notably this video
for the definition of "chordal tone" vs. "extension"
108 changes: 85 additions & 23 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,19 +180,81 @@ 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 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(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);
}
}

Chord curChord;
bool shouldProcess, shouldSilence;
chd->getCurrentChord(curChord, shouldProcess, shouldSilence);

if (shouldSilence)
if (mShouldSilence)
noteOns.clear();

// Process and add processable messages:

for (auto& msg : noteOffs) { // Note OFFs first
Expand All @@ -218,12 +280,12 @@ void Arp::processPatternNotes(ChordStore* chd, Array<MidiMessage>& noteOns, Arra
midibuf.addEvent(MidiMessage::noteOff(msg.getChannel(), nn), 0);
thisNoteMappings.clear();

if (shouldProcess) // The ChordStore tells us to process
if (mShouldProcess) // The ChordStore tells us to process
Mapping::mapPatternNote(referenceNote,
mappingMode,
wrapMode,
unmappedBeh,
curChord,
mNoteSetToUse,
noteCodeIn,
thisNoteMappings);
else // We map the note to itself
Expand Down
5 changes: 5 additions & 0 deletions Source/Arp.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ using Mappings = HashMap< NoteOnChan, Array<NoteNumber> >;
class Arp : public ArplignerAudioProcessor {
private:
ChordStore mLocalChordStore;
NoteSet mNoteSetToUse;
bool mShouldProcess;
bool mShouldSilence;

// On each pattern chan, to which note has been mapped each incoming
// NoteNumber, so we can send the correct NOTE OFFs afterwards
Expand All @@ -43,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
16 changes: 14 additions & 2 deletions Source/PluginProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,22 @@ 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
(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
("patternNotesMapping", "Pattern notes mapping",
Expand All @@ -66,13 +76,13 @@ ArplignerAudioProcessor::ArplignerAudioProcessor()
));

StringArray waModes = StringArray
{ "No wraparound", "[Dynamic] After all chord degrees", "[Fixed] Every 3rd pattern note" };
{ "No wraparound", "[Dynamic] After all degrees", "[Fixed] Every 3rd pattern note" };
for (int i = 3; i <= 12; i++)
waModes.add(String("[Fixed] Every ") + String(i + 1) + "th pattern note");
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" };
Expand Down Expand Up @@ -221,6 +231,7 @@ void ArplignerAudioProcessor::getStateInformation(MemoryBlock& destData)
s.writeInt(*numMillisecsOfLatency);
s.writeInt(*patternNotesWraparound);
s.writeInt(*unmappedNotesBehaviour);
s.writeInt(*preMappingChordProcessing);
}

// Reload state info
Expand All @@ -235,4 +246,5 @@ void ArplignerAudioProcessor::setStateInformation(const void* data, int sizeInBy
*numMillisecsOfLatency = s.readInt();
*patternNotesWraparound = s.readInt();
*unmappedNotesBehaviour = s.readInt();
*preMappingChordProcessing = s.readInt();
}
23 changes: 17 additions & 6 deletions Source/PluginProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ class ArplignerAudioProcessor : public AudioProcessor
AudioParameterChoice* whenSingleChordNote;
AudioParameterChoice* firstDegreeCode;
AudioParameterChoice* patternNotesMapping;
AudioParameterInt* numMillisecsOfLatency;
AudioParameterInt* numMillisecsOfLatency;
AudioParameterChoice* patternNotesWraparound;
AudioParameterChoice* unmappedNotesBehaviour;
AudioParameterChoice* preMappingChordProcessing;
AudioParameterBool* holdCurState;

private:
//==============================================================================
Expand Down Expand Up @@ -105,6 +107,15 @@ namespace WhenSingleChordNote {
};
}

namespace PreMappingChordProcessing {
enum Enum {
NONE = 0,
IGNORE_BASS_NOTE,
ADD_WHOLE_STEPS,
ADD_WHOLE_STEPS_DEF_P4_P5_MAJ7
};
}

namespace PatternNotesMapping {
enum Enum {
ALWAYS_LEAVE_UNMAPPED = 0,
Expand All @@ -116,11 +127,11 @@ namespace PatternNotesMapping {
namespace PatternNotesWraparound {
enum Enum {
NO_WRAPAROUND = 0,
AFTER_ALL_CHORD_DEGREES = 1,
AFTER_ALL_DEGREES = 1,
// Values of 2 and above indicate a specific number of notes after which to
// wrap around (effectively discarding all the chords degrees above that
// number, and leaving unmapped pattern notes that are above the last degree
// of the chord but before the wraparound value)
// wrap around. This effectively discards all the chord/scale degrees above
// that number, and leaving unmapped pattern notes that are above the last
// degree of the chord/scale but before the wraparound value.
};
}

Expand All @@ -129,6 +140,6 @@ namespace UnmappedNotesBehaviour {
SILENCE = 0,
USE_AS_IS,
TRANSPOSE_FROM_FIRST_DEGREE,
PLAY_FULL_CHORD_UP_TO_NOTE
PLAY_ALL_DEGREES_UP_TO_NOTE
};
}