Skip to content

Commit

Permalink
Added polyphonic expressive step sequencer independent for each split.
Browse files Browse the repository at this point in the history
Added support for note off release velocity.
Much improved note on velocity with better stabilization and adapted curves.
Added four new colors: white, orange, lime and pink.
Added support for 14-bit Loudness CC values.
Increased compatibility of USB MIDI messages by tweaking their intervals.
Removed occasional flicker when switching between LED displays.
Fix to reception of pitch bend sensitivity RPN.
14 bit MIDI output improvement.
Set version to 2.0.0 build 35.
  • Loading branch information
gbevin committed Oct 8, 2016
1 parent f7d5721 commit 45afc0e
Show file tree
Hide file tree
Showing 21 changed files with 3,655 additions and 503 deletions.
286 changes: 206 additions & 80 deletions linnstrument-firmware.ino

Large diffs are not rendered by default.

36 changes: 16 additions & 20 deletions ls_arpeggiator.ino
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,26 @@ inline void checkAdvanceArpeggiator() {
checkAdvanceArpeggiatorForSplit(RIGHT);
}

#define TEMPO_SIXTEENTH_SWING 0xff
byte tempoChoices[9] { 24, 12, 8, 6, TEMPO_SIXTEENTH_SWING, 4, 3, 2, 1 };
byte arpTempoChoices[9] {
24, // ArpFourth
12, // ArpEighth
8, // ArpEighthTriplet
6, // ArpSixteenth
TEMPO_ARP_SIXTEENTH_SWING, // ArpSixteenthSwing
4, // ArpSixteenthTriplet
3, // ArpThirtysecond
2, // ArpThirtysecondTriplet
1 // ArpSixtyfourthTriplet
};

inline void checkAdvanceArpeggiatorForSplit(byte split) {
if (isArpeggiatorEnabled(split)) {

byte combinedTempoIndex = constrain(Global.arpTempo + arpTempoDelta[split], 0, 8);
byte combinedTempo = tempoChoices[combinedTempoIndex];
byte combinedTempo = arpTempoChoices[combinedTempoIndex];

if ((combinedTempo == TEMPO_SIXTEENTH_SWING && ((clock24PPQ % 12 == 0) || (clock24PPQ % 12 == 7))) || // we need to handle swing differently since it's irregular
(combinedTempo != TEMPO_SIXTEENTH_SWING && (clock24PPQ % combinedTempo == 0 ))) {
if ((combinedTempo == TEMPO_ARP_SIXTEENTH_SWING && ((clock24PPQ % 12 == 0) || (clock24PPQ % 12 == 7))) || // we need to handle swing differently since it's irregular
(combinedTempo != TEMPO_ARP_SIXTEENTH_SWING && (clock24PPQ % combinedTempo == 0 ))) {
advanceArpeggiatorForSplit(split);
}
}
Expand Down Expand Up @@ -376,7 +385,8 @@ void advanceArpeggiatorForSplit(byte split) {
// if this is the first step in a new sequence, this will be an odd step (starting at one)
if (lastArpNote[split] == -1) {
lastArpStepOdd[split] = true;
} else {
}
else {
lastArpStepOdd[split] = !lastArpStepOdd[split];
}

Expand All @@ -399,17 +409,3 @@ void advanceArpeggiatorForSplit(byte split) {
inline boolean isArpeggiatorEnabled(byte split) {
return Split[split].arpeggiator || isLowRowArpeggiatorPressed(split);
}

void tapTempoPress() {
unsigned long now = micros();
resetClockAdvancement(now);

unsigned long tapDelta = calcTimeDelta(now, lastTapTempo);

if (tapDelta < 6000000) { // minimum 6 seconds between taps
fxd4CurrentTempo -= FXD4_DIV(fxd4CurrentTempo, FXD4_FROM_INT(4));
fxd4CurrentTempo += FXD4_DIV(FXD4_FROM_INT(60000000 / tapDelta), FXD4_FROM_INT(4));
}

lastTapTempo = now;
}
42 changes: 21 additions & 21 deletions ls_calibration.ino
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ byte CALROWNUM;
byte CALCOLNUM;

// these are default starting points for uncalibrated LinnStruments, might need tweaking
int32_t CALX_DEFAULT_LEFT_EDGE;
int32_t CALX_DEFAULT_FIRST_CELL;
int32_t CALX_DEFAULT_CELL_WIDTH;
int32_t CALX_DEFAULT_RIGHT_EDGE;
int32_t FXD_CALX_DEFAULT_LEFT_EDGE;
int32_t FXD_CALX_DEFAULT_FIRST_CELL;
int32_t FXD_CALX_DEFAULT_CELL_WIDTH;
int32_t FXD_CALX_DEFAULT_RIGHT_EDGE;

// the leftmost and rightmost cells don't reach as far on the edges as other cells, this compensates for that
int32_t CALX_BORDER_OFFSET;
int32_t FXD_CALX_BORDER_OFFSET;

const short CALY_DEFAULT_MIN[MAXROWS] = {243, 781, 1299, 1810, 2281, 2718, 3187, 3599};
const short CALY_DEFAULT_MAX[MAXROWS] = {473, 991, 1486, 1965, 2449, 2925, 3401, 3851};
Expand All @@ -39,26 +39,26 @@ void initializeCalibration() {
CALCOLNUM = 9;

// these are default starting points for uncalibrated LinnStruments, might need tweaking
CALX_DEFAULT_LEFT_EDGE = FXD_MAKE(188);
CALX_DEFAULT_FIRST_CELL = FXD_MAKE(248);
CALX_DEFAULT_CELL_WIDTH = FXD_MAKE(157);
CALX_DEFAULT_RIGHT_EDGE = FXD_MAKE(4064);
FXD_CALX_DEFAULT_LEFT_EDGE = FXD_MAKE(188);
FXD_CALX_DEFAULT_FIRST_CELL = FXD_MAKE(248);
FXD_CALX_DEFAULT_CELL_WIDTH = FXD_MAKE(157);
FXD_CALX_DEFAULT_RIGHT_EDGE = FXD_MAKE(4064);

// the leftmost and rightmost cells don't reach as far on the edges as other cells, this compensates for that
CALX_BORDER_OFFSET = FXD_MAKE(10);
FXD_CALX_BORDER_OFFSET = FXD_MAKE(10);
}
else if (LINNMODEL == 128) {
CALROWNUM = 4;
CALCOLNUM = 6;

// these are default starting points for uncalibrated LinnStruments, might need tweaking
CALX_DEFAULT_LEFT_EDGE = FXD_MAKE(293.75);
CALX_DEFAULT_FIRST_CELL = FXD_MAKE(387.5);
CALX_DEFAULT_CELL_WIDTH = FXD_MAKE(245.3125);
CALX_DEFAULT_RIGHT_EDGE = FXD_MAKE(4064);
FXD_CALX_DEFAULT_LEFT_EDGE = FXD_MAKE(293.75);
FXD_CALX_DEFAULT_FIRST_CELL = FXD_MAKE(387.5);
FXD_CALX_DEFAULT_CELL_WIDTH = FXD_MAKE(245.3125);
FXD_CALX_DEFAULT_RIGHT_EDGE = FXD_MAKE(4064);

// the leftmost and rightmost cells don't reach as far on the edges as other cells, this compensates for that
CALX_BORDER_OFFSET = FXD_MAKE(15.625);
FXD_CALX_BORDER_OFFSET = FXD_MAKE(15.625);
}
}

Expand Down Expand Up @@ -86,18 +86,18 @@ void initializeCalibrationData() {

// Initialize default X calibration data
for (byte row = 0; row < CALROWNUM; ++row) {
Device.calRows[0][row].fxdReferenceX = FXD_MUL(FXD_FROM_INT(-1), CALX_HALF_UNIT) + CALX_BORDER_OFFSET;
Device.calRows[0][row].fxdMeasuredX = CALX_DEFAULT_LEFT_EDGE;
Device.calRows[0][row].fxdReferenceX = FXD_MUL(FXD_FROM_INT(-1), CALX_HALF_UNIT) + FXD_CALX_BORDER_OFFSET;
Device.calRows[0][row].fxdMeasuredX = FXD_CALX_DEFAULT_LEFT_EDGE;
Device.calRows[0][row].fxdRatio = 0;

for (byte col = 1; col < NUMCOLS; ++col) {
Device.calRows[col][row].fxdReferenceX = FXD_MUL(CALX_FULL_UNIT, FXD_FROM_INT(col - 1)); // center in the middle of the cells
Device.calRows[col][row].fxdMeasuredX = CALX_DEFAULT_FIRST_CELL + FXD_MUL(CALX_DEFAULT_CELL_WIDTH, FXD_FROM_INT(col - 1));
Device.calRows[col][row].fxdRatio = FXD_DIV(CALX_FULL_UNIT, CALX_DEFAULT_CELL_WIDTH);
Device.calRows[col][row].fxdMeasuredX = FXD_CALX_DEFAULT_FIRST_CELL + FXD_MUL(FXD_CALX_DEFAULT_CELL_WIDTH, FXD_FROM_INT(col - 1));
Device.calRows[col][row].fxdRatio = FXD_DIV(CALX_FULL_UNIT, FXD_CALX_DEFAULT_CELL_WIDTH);
}

Device.calRows[NUMCOLS][row].fxdReferenceX = FXD_MUL(CALX_FULL_UNIT, FXD_FROM_INT(NUMCOLS - 1)) - CALX_HALF_UNIT - CALX_BORDER_OFFSET;
Device.calRows[NUMCOLS][row].fxdMeasuredX = CALX_DEFAULT_RIGHT_EDGE;
Device.calRows[NUMCOLS][row].fxdReferenceX = FXD_MUL(CALX_FULL_UNIT, FXD_FROM_INT(NUMCOLS - 1)) - CALX_HALF_UNIT - FXD_CALX_BORDER_OFFSET;
Device.calRows[NUMCOLS][row].fxdMeasuredX = FXD_CALX_DEFAULT_RIGHT_EDGE;
Device.calRows[NUMCOLS][row].fxdRatio = 0;
}

Expand Down
14 changes: 14 additions & 0 deletions ls_clock.ino
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,17 @@ inline boolean checkUpdateClock(unsigned long now) {

return true;
}

void tapTempoPress() {
unsigned long now = micros();
resetClockAdvancement(now);

unsigned long tapDelta = calcTimeDelta(now, lastTapTempo);

if (tapDelta < 6000000) { // minimum 6 seconds between taps
fxd4CurrentTempo -= FXD4_DIV(fxd4CurrentTempo, FXD4_FROM_INT(4));
fxd4CurrentTempo += FXD4_DIV(FXD4_FROM_INT(60000000 / tapDelta), FXD4_FROM_INT(4));
}

lastTapTempo = now;
}
71 changes: 48 additions & 23 deletions ls_displayModes.ino
Original file line number Diff line number Diff line change
Expand Up @@ -199,39 +199,49 @@ void updateSwitchLeds() {
return;
}

CellDisplay displaySwitch1 = switchState[SWITCH_SWITCH_1][focusedSplit] ? cellOn : cellOff;
CellDisplay displaySwitch1 = switchState[SWITCH_SWITCH_1][Global.currentPerSplit] ? cellOn : cellOff;
if (Global.switchAssignment[SWITCH_SWITCH_1] == ASSIGNED_ARPEGGIATOR) {
displaySwitch1 = isArpeggiatorEnabled(focusedSplit) ? cellOn : cellOff;
displaySwitch1 = isArpeggiatorEnabled(Global.currentPerSplit) ? cellOn : cellOff;
}
else if ((Global.switchAssignment[SWITCH_SWITCH_1] == ASSIGNED_SUSTAIN && isSustainEnabled(focusedSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_1] == ASSIGNED_AUTO_OCTAVE && isSwitchAutoOctavePressed(focusedSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_1] == ASSIGNED_CC_65 && isSwitchCC65Pressed(focusedSplit))) {
else if ((Global.switchAssignment[SWITCH_SWITCH_1] == ASSIGNED_SUSTAIN && isSustainEnabled(Global.currentPerSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_1] == ASSIGNED_AUTO_OCTAVE && isSwitchAutoOctavePressed(Global.currentPerSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_1] == ASSIGNED_CC_65 && isSwitchCC65Pressed(Global.currentPerSplit))) {
displaySwitch1 = cellOn;
}
setLed(0, SWITCH_1_ROW, globalColor, displaySwitch1);

CellDisplay displaySwitch2 = switchState[SWITCH_SWITCH_2][focusedSplit] ? cellOn : cellOff;
CellDisplay displaySwitch2 = switchState[SWITCH_SWITCH_2][Global.currentPerSplit] ? cellOn : cellOff;
if (Global.switchAssignment[SWITCH_SWITCH_2] == ASSIGNED_ARPEGGIATOR) {
displaySwitch2 = isArpeggiatorEnabled(focusedSplit) ? cellOn : cellOff;
displaySwitch2 = isArpeggiatorEnabled(Global.currentPerSplit) ? cellOn : cellOff;
}
else if ((Global.switchAssignment[SWITCH_SWITCH_2] == ASSIGNED_SUSTAIN && isSustainEnabled(focusedSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_2] == ASSIGNED_AUTO_OCTAVE && isSwitchAutoOctavePressed(focusedSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_2] == ASSIGNED_CC_65 && isSwitchCC65Pressed(focusedSplit))) {
else if ((Global.switchAssignment[SWITCH_SWITCH_2] == ASSIGNED_SUSTAIN && isSustainEnabled(Global.currentPerSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_2] == ASSIGNED_AUTO_OCTAVE && isSwitchAutoOctavePressed(Global.currentPerSplit)) ||
(Global.switchAssignment[SWITCH_SWITCH_2] == ASSIGNED_CC_65 && isSwitchCC65Pressed(Global.currentPerSplit))) {
displaySwitch2 = cellOn;
}
setLed(0, SWITCH_2_ROW, globalColor, displaySwitch2);

if (splitActive) {
setLed(0, SPLIT_ROW, Split[focusedSplit].colorMain, cellOn);
if (Split[Global.currentPerSplit].sequencer) {
setLed(0, SPLIT_ROW, Split[Global.currentPerSplit].colorMain, cellOn);
}
else if (splitActive) {
setLed(0, SPLIT_ROW, Split[Global.currentPerSplit].colorMain, cellOn);
}
else {
clearLed(0, SPLIT_ROW);
}

updateSequencerSwitchLeds();
}

// paintNormalDisplay:
// Paints all non-switch columns of the display with the normal performance colors
void paintNormalDisplay() {
if (Split[Global.currentPerSplit].sequencer) {
paintSequencerDisplay(Global.currentPerSplit);
return;
}

// highlight global settings red when user firmware mode is active
if (userFirmwareActive) {
setLed(0, GLOBAL_SETTINGS_ROW, COLOR_YELLOW, cellOn);
Expand Down Expand Up @@ -361,7 +371,7 @@ void paintNormalDisplayCell(byte split, byte col, byte row) {
byte colour = COLOR_OFF;
CellDisplay cellDisplay = cellOff;

short displayedNote = getNoteNumber(split, col, row) - Split[split].transposeLights;
short displayedNote = getNoteNumber(split, col, row);
short actualnote = transposedNote(split, col, row);

// the note is out of MIDI note range, disable it
Expand All @@ -388,7 +398,7 @@ void paintNormalDisplayCell(byte split, byte col, byte row) {
// show pulsating middle root note
if (blinkMiddleRootNote && displayedNote == 60) {
colour = Split[split].colorAccent;
cellDisplay = cellPulse;
cellDisplay = cellFastPulse;
}

// if the low row is anything but normal, set it to the appropriate color
Expand Down Expand Up @@ -528,7 +538,7 @@ void paintPerSplitDisplay(byte side) {
// Set "Color" lights
setLed(11, 7, Split[side].colorMain, cellOn);
setLed(11, 6, Split[side].colorAccent, cellOn);
setLed(11, 5, Split[side].colorNoteon, cellOn);
setLed(11, 5, Split[side].colorPlayed, cellOn);
setLed(11, 4, Split[side].colorLowRow, cellOn);

// Set "Low row" lights
Expand Down Expand Up @@ -560,20 +570,25 @@ void paintPerSplitDisplay(byte side) {
}

// set Arpeggiator
if (Split[side].arpeggiator == true) {
if (Split[side].arpeggiator) {
setLed(14, 7, Split[side].colorMain, cellOn);
}

// set CC faders
if (Split[side].ccFaders == true) {
if (Split[side].ccFaders) {
setLed(14, 6, getCCFadersColor(side), cellOn);
}

// set strum
if (Split[side].strum == true) {
if (Split[side].strum) {
setLed(14, 5, Split[side].colorMain, cellOn);
}

// set sequencer
if (Split[side].sequencer) {
setLed(14, 4, Split[side].colorMain, cellOn);
}

// set "show split" led
paintShowSplitSelection(side);
}
Expand Down Expand Up @@ -612,7 +627,7 @@ byte getCCForYColor(byte side) {

byte getLimitsForZColor(byte side) {
byte color = Split[side].colorMain;
if (Split[side].minForZ != 0 || Split[side].maxForZ != 127) {
if (Split[side].minForZ != 0 || Split[side].maxForZ != 127 || Split[side].ccForZ14Bit) {
color = Split[side].colorAccent;
}
return color;
Expand Down Expand Up @@ -734,14 +749,23 @@ void paintLimitsForZDisplay(byte side) {
clearDisplay();

switch (limitsForZConfigState) {
case 1:
case 2:
condfont_draw_string(0, 0, "L", Split[side].colorMain, true);
paintSplitNumericDataDisplay(side, Split[side].minForZ, 4, true);
break;
case 0:
case 1:
condfont_draw_string(0, 0, "H", Split[side].colorMain, true);
paintSplitNumericDataDisplay(side, Split[side].maxForZ, 4, true);
break;
case 0:
if (Split[side].ccForZ14Bit) {
condfont_draw_string(0, 0, "14BT", Split[side].colorMain, true);
}
else {
condfont_draw_string(3, 0, "7BT", Split[side].colorMain, true);
}
paintShowSplitSelection(side);
break;
}
}

Expand Down Expand Up @@ -903,7 +927,7 @@ void paintSplitNumericDataDisplay(byte side, unsigned short value, byte offset,
paintNumericDataDisplay(Split[side].colorMain, value, offset, condensed);
}

void paintNumericDataDisplay(byte color, unsigned short value, byte offset, boolean condensed) {
void paintNumericDataDisplay(byte color, short value, short offset, boolean condensed) {
char str[10];
const char* format;
byte pos;
Expand Down Expand Up @@ -1143,7 +1167,8 @@ void paintGlobalSettingsDisplay() {
// Show the MIDI input/output configuration
if (Global.midiIO == 1) {
setLed(15, 0, getMIDIUSBColor(), cellOn); // for MIDI over USB
} else {
}
else {
setLed(15, 1, getMIDIThroughColor(), cellOn); // for MIDI jacks
}

Expand Down
Loading

0 comments on commit 45afc0e

Please sign in to comment.