Skip to content

Commit

Permalink
Fixed arpeggiator resetting state when a playing arp note is released…
Browse files Browse the repository at this point in the history
… before moving to the next note.
  • Loading branch information
gbevin committed Oct 3, 2023
1 parent b102768 commit 9d9f528
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 44 deletions.
6 changes: 4 additions & 2 deletions linnstrument-firmware.ino
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ struct NoteEntry {
inline void setColRow(byte, byte);
inline byte getCol();
inline byte getRow();
inline boolean hasTouch();

inline byte getNextNote();
inline byte getNextChannel();
Expand All @@ -418,8 +419,8 @@ struct NoteEntry {
inline void setPreviousChannel(byte);
};
struct NoteTouchMapping {
void initialize(); // initialize the mapping data
void releaseLatched(byte split); // release all the note mappings that are latched and have no real active touch
void initialize(byte mappedSplit); // initialize the mapping data
void releaseLatched(); // release all the note mappings that are latched and have no real active touch
void noteOn(signed char, signed char, byte, byte); // register the cell for which a note was turned on
void noteOff(signed char, signed char); // turn off a note
void changeCell(signed char, signed char, byte, byte); // changes the cell of an active note
Expand All @@ -429,6 +430,7 @@ struct NoteTouchMapping {

void debugNoteChain();

unsigned char split;
unsigned short noteCount;
byte musicalTouchCount[16];
signed char firstNote;
Expand Down
86 changes: 52 additions & 34 deletions ls_arpeggiator.ino
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ the actual cell that was pressed, allowing velocity to be continuously varied du
sequence.
***************************************************************************************************/

signed char lastArpNote[NUMSPLITS]; // the last note played by the arpeggiator or -1 if it should be starting from scratch
signed char lastArpChannel[NUMSPLITS]; // the last channel played by the arpeggiator or -1 if it should be starting from scratch
signed char playingArpNote[NUMSPLITS]; // the last note played by the arpeggiator or -1 if no note is still playing
signed char playingArpChannel[NUMSPLITS]; // the last channel played by the arpeggiator or -1 if no note is still playing
signed char stepArpNote[NUMSPLITS]; // the current step note of the arpeggiator or -1 if it should be starting from scratch
signed char stepArpChannel[NUMSPLITS]; // the current step channel of the arpeggiator or -1 if it should be starting from scratch
boolean lastArpStepOdd[NUMSPLITS]; // indicates whether the last arpeggiator step was odd (1-based : 1, 3, 5)
ArpeggiatorDirection arpUpDownState[NUMSPLITS]; // the state in the alternating up/down pattern
signed char arpOctaveState[NUMSPLITS]; // the octave that is used while playing the arpeggiator sequence
Expand All @@ -23,14 +25,16 @@ void initializeArpeggiator() {
randomSeed(analogRead(0));

for (byte split = 0; split < NUMSPLITS; ++split) {
noteTouchMapping[split].initialize();
noteTouchMapping[split].initialize(split);
resetArpeggiatorState(split);
}
}

void resetArpeggiatorState(byte split) {
lastArpNote[split] = -1;
lastArpChannel[split] = -1;
playingArpNote[split] = -1;
playingArpChannel[split] = -1;
stepArpNote[split] = -1;
stepArpChannel[split] = -1;
lastArpStepOdd[split] = false;
arpUpDownState[split] = ArpUp;
arpOctaveState[split] = 0;
Expand All @@ -57,7 +61,7 @@ void disableTemporaryArpeggiator() {
void handleArpeggiatorNoteOff(byte split, byte notenum, byte channel) {
// handle replay all differently since it plays multiple notes simultaneously
if (Global.arpDirection == ArpReplayAll) {
if (lastArpNote[split] != -1) {
if (playingArpNote[split] != -1) {
for (byte octave = 0; octave <= Global.arpOctave; ++octave) {
midiSendNoteOff(split, getOctaveNote(octave, notenum), channel);
}
Expand All @@ -70,9 +74,8 @@ void handleArpeggiatorNoteOff(byte split, byte notenum, byte channel) {
}
}
// handle single note sequences, send the note off and reset the arpeggiator state if the note off was the last played
else if (lastArpNote[split] == notenum && lastArpChannel[split] == channel) {
else if (playingArpNote[split] == notenum && playingArpChannel[split] == channel) {
midiSendNoteOff(split, getArpeggiatorNote(split, notenum), channel);
resetArpeggiatorState(split);
}

// reset state when no notes are played at all anymore
Expand All @@ -87,7 +90,7 @@ void turnArpeggiatorOff(byte split) {
}

void sendArpeggiatorStepMidiOff(byte split) {
if (lastArpNote[split] != -1) {
if (playingArpNote[split] != -1) {
if (Global.arpDirection == ArpReplayAll) {
if (noteTouchMapping[split].noteCount > 0) {
signed char arpNote = noteTouchMapping[split].firstNote;
Expand All @@ -110,7 +113,7 @@ void sendArpeggiatorStepMidiOff(byte split) {
}
}
else {
midiSendNoteOff(split, getArpeggiatorNote(split, lastArpNote[split]), lastArpChannel[split]);
midiSendNoteOff(split, getArpeggiatorNote(split, playingArpNote[split]), playingArpChannel[split]);
}
}
}
Expand Down Expand Up @@ -181,13 +184,17 @@ void advanceArpeggiatorForSplit(byte split) {
}
}

lastArpNote[split] = noteTouchMapping[split].firstNote;
lastArpChannel[split] = noteTouchMapping[split].firstChannel;
playingArpNote[split] = noteTouchMapping[split].firstNote;
playingArpChannel[split] = noteTouchMapping[split].firstChannel;
stepArpNote[split] = playingArpNote[split];
stepArpChannel[split] = playingArpChannel[split];
lastArpStepOdd[split] = !lastArpStepOdd[split];
}
else {
lastArpNote[split] = -1;
lastArpChannel[split] = -1;
playingArpNote[split] = -1;
playingArpChannel[split] = -1;
stepArpNote[split] = -1;
stepArpChannel[split] = -1;
}
}
// handle single note sequences
Expand All @@ -199,18 +206,18 @@ void advanceArpeggiatorForSplit(byte split) {
// sequence steps upwards
case ArpUp:
{
if (lastArpNote[split] == -1) {
if (stepArpNote[split] == -1) {
arpNote = noteTouchMapping[split].firstNote;
arpChannel = noteTouchMapping[split].firstChannel;
}
else {
NoteEntry* lastEntry = noteTouchMapping[split].getNoteEntry(lastArpNote[split], lastArpChannel[split]);
if (lastEntry == NULL) {
NoteEntry* stepEntry = noteTouchMapping[split].getNoteEntry(stepArpNote[split], stepArpChannel[split]);
if (stepEntry == NULL) {
arpNote = -1;
}
else {
arpNote = lastEntry->getNextNote();
arpChannel = lastEntry->getNextChannel();
arpNote = stepEntry->getNextNote();
arpChannel = stepEntry->getNextChannel();
}

// start again from the beginning
Expand All @@ -228,18 +235,18 @@ void advanceArpeggiatorForSplit(byte split) {
// sequence steps downwards
case ArpDown:
{
if (lastArpNote[split] == -1) {
if (stepArpNote[split] == -1) {
arpNote = noteTouchMapping[split].lastNote;
arpChannel = noteTouchMapping[split].lastChannel;
}
else {
NoteEntry* lastEntry = noteTouchMapping[split].getNoteEntry(lastArpNote[split], lastArpChannel[split]);
if (lastEntry == NULL) {
NoteEntry* stepEntry = noteTouchMapping[split].getNoteEntry(stepArpNote[split], stepArpChannel[split]);
if (stepEntry == NULL) {
arpNote = -1;
}
else {
arpNote = lastEntry->getPreviousNote();
arpChannel = lastEntry->getPreviousChannel();
arpNote = stepEntry->getPreviousNote();
arpChannel = stepEntry->getPreviousChannel();
}

// start again from the end
Expand All @@ -257,24 +264,24 @@ void advanceArpeggiatorForSplit(byte split) {
// sequence steps alternativing upwards and downwards
case ArpUpDown:
{
if (lastArpNote[split] == -1) {
if (stepArpNote[split] == -1) {
arpUpDownState[split] = ArpUp;
arpNote = noteTouchMapping[split].firstNote;
arpChannel = noteTouchMapping[split].firstChannel;
}
else {
NoteEntry* lastEntry = noteTouchMapping[split].getNoteEntry(lastArpNote[split], lastArpChannel[split]);
if (lastEntry == NULL) {
NoteEntry* stepEntry = noteTouchMapping[split].getNoteEntry(stepArpNote[split], stepArpChannel[split]);
if (stepEntry == NULL) {
arpNote = -1;
}
else {
if (arpUpDownState[split] == ArpDown) {
arpNote = lastEntry->getPreviousNote();
arpChannel = lastEntry->getPreviousChannel();
arpNote = stepEntry->getPreviousNote();
arpChannel = stepEntry->getPreviousChannel();
}
else {
arpNote = lastEntry->getNextNote();
arpChannel = lastEntry->getNextChannel();
arpNote = stepEntry->getNextNote();
arpChannel = stepEntry->getNextChannel();
}
}

Expand Down Expand Up @@ -326,6 +333,10 @@ void advanceArpeggiatorForSplit(byte split) {
if (entry == NULL) {
arpNote = -1;
}
else if (stepEntry == NULL || !stepEntry->hasTouch()) {
arpNote = noteTouchMapping[split].lastNote;
arpChannel = noteTouchMapping[split].lastChannel;
}
else {
arpNote = entry->getPreviousNote();
arpChannel = entry->getPreviousChannel();
Expand All @@ -345,6 +356,10 @@ void advanceArpeggiatorForSplit(byte split) {
if (entry == NULL) {
arpNote = -1;
}
else if (stepEntry == NULL || !stepEntry->hasTouch()) {
arpNote = noteTouchMapping[split].firstNote;
arpChannel = noteTouchMapping[split].firstChannel;
}
else {
arpNote = entry->getNextNote();
arpChannel = entry->getNextChannel();
Expand Down Expand Up @@ -389,7 +404,7 @@ void advanceArpeggiatorForSplit(byte split) {

if (arpNote != -1) {
// if this is the first step in a new sequence, this will be an odd step (starting at one)
if (lastArpNote[split] == -1) {
if (stepArpNote[split] == -1) {
lastArpStepOdd[split] = true;
}
else {
Expand All @@ -400,6 +415,7 @@ void advanceArpeggiatorForSplit(byte split) {
NoteEntry* entry = noteTouchMapping[split].getNoteEntry(arpNote, arpChannel);
if (entry == NULL) {
arpNote = -1;
arpChannel = -1;
}
else {
// after the initial velocity, new velocity values are continuously being calculated simply based
Expand All @@ -412,8 +428,10 @@ void advanceArpeggiatorForSplit(byte split) {
}
}

lastArpNote[split] = arpNote;
lastArpChannel[split] = arpChannel;
playingArpNote[split] = arpNote;
playingArpChannel[split] = arpChannel;
stepArpNote[split] = arpNote;
stepArpChannel[split] = arpChannel;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ls_handleTouches.ino
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,7 @@ boolean handleXYZupdate() {
// when the legato switch is pressed and this is the only new touch in the split,
// release all the latched notes after the new note on message
if (isSwitchLegatoPressed(sensorSplit) && !hasOtherTouchInSplit(sensorSplit)) {
noteTouchMapping[sensorSplit].releaseLatched(sensorSplit);
noteTouchMapping[sensorSplit].releaseLatched();
}
}

Expand Down
46 changes: 40 additions & 6 deletions ls_noteTouchMapping.ino
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ iteration to constitute the arpeggiated sequence.
void resetAllTouches() {
midiSendNoteOffForAllTouches(LEFT);
midiSendNoteOffForAllTouches(RIGHT);
noteTouchMapping[LEFT].initialize();
noteTouchMapping[RIGHT].initialize();
noteTouchMapping[LEFT].initialize(LEFT);
noteTouchMapping[RIGHT].initialize(RIGHT);
}

boolean validNoteNumAndChannel(signed char noteNum, signed char noteChannel) {
Expand Down Expand Up @@ -45,6 +45,10 @@ inline byte NoteEntry::getRow() {
return (colRow & B11100000) >> 5;
}

inline boolean NoteEntry::hasTouch() {
return colRow != 0;
}

inline byte NoteEntry::getNextNote() {
return nextNote;
}
Expand All @@ -69,7 +73,8 @@ inline byte NoteEntry::getPreviousChannel() {
return (nextPreviousChannel & B00001111) + 1;
}

void NoteTouchMapping::initialize() {
void NoteTouchMapping::initialize(byte mappedSplit) {
split = mappedSplit;
noteCount = 0;
firstNote = -1;
firstChannel = -1;
Expand All @@ -90,7 +95,7 @@ void NoteTouchMapping::initialize() {
performContinuousTasks();
}

void NoteTouchMapping::releaseLatched(byte split) {
void NoteTouchMapping::releaseLatched() {
signed char entryNote = firstNote;
signed char entryChannel = firstChannel;
while (entryNote != -1) {
Expand Down Expand Up @@ -148,7 +153,7 @@ void NoteTouchMapping::noteOn(signed char noteNum, signed char noteChannel, byte

musicalTouchCount[channel] += 1;

if (mapping[noteNum][channel].colRow == 0) {
if (!mapping[noteNum][channel].hasTouch()) {
noteCount++;

// no notes are in the chain yet, add this one as the first note
Expand Down Expand Up @@ -218,6 +223,14 @@ void NoteTouchMapping::noteOn(signed char noteNum, signed char noteChannel, byte

mapping[noteNum][channel].setColRow(col, row);

// this note on is the same as the currently playing note, restore the arp step to this note
if (!isSwitchLegatoPressed(split) && !isSwitchLatchPressed(split)) {
if (playingArpNote[split] == noteNum && playingArpChannel[split] == noteChannel) {
stepArpNote[split] = noteNum;
stepArpChannel[split] = noteChannel;
}
}

debugNoteChain();
}

Expand Down Expand Up @@ -256,6 +269,27 @@ void NoteTouchMapping::noteOff(signed char noteNum, signed char noteChannel) {
signed char prevChannel = mapping[noteNum][channel].getPreviousChannel();
signed char nxtNote = mapping[noteNum][channel].nextNote;
signed char nxtChannel = mapping[noteNum][channel].getNextChannel();

// verify if the removed note entry is the current active arp step, if that's
// the case, update the step so that linked chain of notes is not interrupted
if (!isSwitchLegatoPressed(split) && !isSwitchLatchPressed(split)) {
if (stepArpNote[split] == noteNum && stepArpChannel[split] == noteChannel) {
// when the arp is going up, update the step with the previous note entry,
// however when it's in up/down mode, don't do this when the last note is reached
// otherwise the arp will skip a note
if (Global.arpDirection == ArpUp || (Global.arpDirection == ArpUpDown && arpUpDownState[split] == ArpUp && (noteNum != lastNote || noteChannel != lastChannel))) {
stepArpNote[split] = prevNote;
stepArpChannel[split] = prevChannel;
}
// when the arp is going down, update the step with the next note entry
else if (Global.arpDirection == ArpDown || (Global.arpDirection == ArpUpDown && arpUpDownState[split] == ArpDown)) {
stepArpNote[split] = nxtNote;
stepArpChannel[split] = nxtChannel;
}
}
}

// update the next and previous entries
mapping[prevNote][prevChannel - 1].nextNote = nxtNote;
mapping[prevNote][prevChannel - 1].setNextChannel(nxtChannel);
if (nxtNote == -1) {
Expand Down Expand Up @@ -286,7 +320,7 @@ void NoteTouchMapping::changeCell(signed char noteNum, signed char noteChannel,
// offset the channel for a 0-based arrays
signed char channel = noteChannel - 1;

if (mapping[noteNum][channel].colRow != 0) {
if (mapping[noteNum][channel].hasTouch()) {
mapping[noteNum][channel].setColRow(col, row);
}
}
Expand Down
2 changes: 1 addition & 1 deletion ls_switches.ino
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ void performSwitchAssignmentOff(byte whichSwitch, byte assignment, byte split) {

case ASSIGNED_LEGATO:
case ASSIGNED_LATCH:
noteTouchMapping[split].releaseLatched(split);
noteTouchMapping[split].releaseLatched();
break;

case ASSIGNED_REVERSE_PITCH_X:
Expand Down

0 comments on commit 9d9f528

Please sign in to comment.