<|start_filename|>src/main.js<|end_filename|>
import MaxConsole from './MaxConsole';
console = new MaxConsole();

import { Config, Controller, Model, View } from '.';

const { OUTLET } = Config;
const outletNames = Object.keys(OUTLET);
outlets = outletNames.length;
outletNames.forEach((name, index) => setoutletassist(index, name));

const model = new Model;
const controller = new Controller(model, new View(model));

export function init() { controller.initViews(); }
export function bang() { controller.refreshViews(); }
export function reset() { controller.reset(); }
export function setmodel(...data) { controller.setModel(data); }

export function scale(...offsets) { controller.setScaleOffsetsRelativeToC(offsets); }
export function root(root) { controller.setScaleRoot(root); }
export function duration(duration) { controller.setDuration(duration); }
export function modsum(mode) { controller.setModulationSummingMode(mode); }
export function modslew(slew) { controller.setModulationSlew(slew); }

export function pitch(pitch) { controller.setTrackPitch(pitch); }
export function velocity(velocity) { controller.setTrackVelocity(velocity); }
export function gate(gate) { controller.setTrackGate(gate); }
export function gatemode(mode) { controller.setTrackGateMode(mode) }
export function gatesum(mode) { controller.setTrackGateSummingMode(mode); }
export function multiplier(multiplier) { controller.setTrackMultiplier(multiplier); }
export function trackmute(mute) { controller.setTrackMute(mute); }

export function start(stepNumber) { controller.setPatternStart(stepNumber - 1); }
export function end(stepNumber) { controller.setPatternEnd(stepNumber - 1); }
export function patternmute(mute) { controller.setPatternMute(mute); }
export function grid(x, y, enabled) { controller.handleGridClick(x, y, enabled); }

export function track(trackIndex) { controller.selectTrack(trackIndex); }
export function pattern(patternIndex) { controller.selectPattern(patternIndex); }
export function value(value) { controller.selectOrToggleValue(value); }

export function lpnote(pitch, velocity) { controller.handleLaunchpadNote(pitch, velocity); }
export function lpcc(cc, value) { controller.handleLaunchpadCC(cc, value); }
export function note(pitch, velocity) { controller.handleTrackNote(pitch, velocity); }
export function cc(cc, value) { controller.handleTrackCC(cc, value); }
export function clock(index) { controller.handleClockTick(index); }

export function shiftleft() { controller.shiftPatternLeft(); }
export function shiftright() { controller.shiftPatternRight(); }
export function shiftup() { controller.shiftPatternUp(); }
export function shiftdown() { controller.shiftPatternDown(); }
export function reverse() { controller.reversePattern(); }
export function random() { controller.randomizePattern(); }
export function clear() { controller.clearPattern(); }
export function copy() { controller.copyPatternSteps(); }
export function paste() { controller.pastePatternSteps(); }

export function trackreverse() { controller.reverseTrack(); }
export function trackrandom() { controller.randomizeTrack(); }
export function trackclear() { controller.clearTrack(); }
export function trackcopy() { controller.copyTrackSteps(); }
export function trackpaste() { controller.pasteTrackSteps(); }

console.log(
  `________________________________________________________________________________
reloaded at: ${new Date}`
);


<|start_filename|>package.json<|end_filename|>
{
  "name": "m4l-launchpad-sequencer",
  "version": "1.2.0",
  "description": "A step sequencer for the Novation Launchpad, for use with Ableton Live and Max for Live.",
  "dependencies": {},
  "devDependencies": {
    "@babel/core": "^7.8.7",
    "@babel/preset-env": "^7.7.6",
    "@babel/register": "^7.8.6",
    "@rollup/plugin-node-resolve": "^7.1.1",
    "mocha": "^7.1.0",
    "rollup": "^2.0.6",
    "rollup-plugin-babel": "^4.3.3"
  },
  "scripts": {
    "build": "npx rollup -c",
    "ci": "npm run test-strict && npm run build",
    "clean": "rm -f device/launchpad-sequencer.js",
    "dev": "npx rollup -c -w",
    "test": "npx mocha -c -r @babel/register tests/setup 'tests/**/*.test.js'",
    "test-strict": "npx mocha --forbid-only -c -r @babel/register tests/setup 'tests/**/*.test.js'",
    "tdd": "npx mocha -c -w -R min -r @babel/register tests/setup 'tests/**/*.test.js'",
    "sync-examples": "cp device/* 'examples/Template Project/Presets/MIDI Effects/Max MIDI Effect/Imported' && cp device/* 'examples/Advanced Example Project/Presets/MIDI Effects/Max MIDI Effect/Imported'",
    "release": "npm run build && npm run sync-examples && zip -r releases/launchpad-sequencer-1.2.zip device examples -x */Backup/*"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/adamjmurray/m4l-launchpad-sequencer.git"
  },
  "author": "<NAME>",
  "license": "BSD"
}


<|start_filename|>src/Model/Track.js<|end_filename|>
import { DEFAULT, MODE, NUMBER_OF, PATTERN } from '../config';
import Note from './Note';
import Pattern from './Pattern';

const { GATE, GATE_SUMMING } = MODE;

const randomItem = (list) => list[Math.floor(Math.random() * list.length)];

export default class Track {

  constructor(index) {
    this.index = index;
    this.patterns = [...Array(NUMBER_OF.PATTERNS)].map((_, patternIndex) => new Pattern({
      trackIndex: index,
      index: patternIndex,
      type: DEFAULT.PATTERN_TYPES[patternIndex],
    }));
    this.reset();
  }

  reset() {
    this.pitch = DEFAULT.PITCH;
    this.velocity = DEFAULT.VELOCITY;
    this.gate = DEFAULT.GATE;
    this.gateMode = DEFAULT.GATE_MODE;
    this.gateSummingMode = DEFAULT.GATE_SUMMING_MODE;
    this.patterns.forEach(pattern => pattern.reset());
    this.durationMultiplier = 1;
    this.mute = false;
    this._notes = [new Note, new Note, new Note]; // multi sum mode can output up to 3 notes
  }

  get pitch() {
    return this._pitch;
  }

  set pitch(pitch) {
    this._pitch = pitch;
    this._octave = Math.floor(pitch / 12);
    this._offset = pitch % 12;
  }

  notesForClock(clock, scale) {
    const [note, note2, note3] = this._notes; // avoid creating and garbage collecting objects each clock tick
    if (this.mute || clock < 0 || clock % this.durationMultiplier !== 0) {
      // return the previous note to maintain the modulation and aftertouch value when we're in between track steps
      note.enabled = false;
      return [note]; // TODO: Can we avoid constructing an array here?
    }
    const trackClock = clock / this.durationMultiplier;

    this._notes.forEach(note => note.reset());
    note.pitch = this._pitch;
    note.velocity = this.velocity;

    this.patterns.forEach((pattern, index) => {
      switch (index) {
        case PATTERN.GATE2:
          pattern.processNote(note2, trackClock, scale);
          break;
        case PATTERN.GATE3:
          pattern.processNote(note3, trackClock, scale);
          break;
        default:
          pattern.processNote(note, trackClock, scale);
      }
    });

    const gateValue = this._gateValue();
    if (gateValue > 0 && !note.mute) {
      note.enabled = true;
      note.duration *= this.gate * this.durationMultiplier; // track.gate and durationMultiplier scales the note's duration

      const velocity = note.velocity;
      const duration = note.duration;

      switch (this.gateMode) {
        case GATE.PITCH:
          const octave = this._octave;
          const offset = this._offset;
          if (this.gateSummingMode === GATE_SUMMING.MULTI) {
            if (note.gateValue > 0) {
              // the first gate value maps to the track pitch, so subtract 1 and apply the scale
              note.pitch = scale.pitchAt(octave, offset + (note.gateValue - 1));
            } else {
              note.enabled = false;
            }
            if (note2.gateValue > 0) {
              note2.enabled = true;
              note2.pitch = scale.pitchAt(octave, offset + (note2.gateValue - 1));
              note2.velocity = velocity;
              note2.duration = duration;
            }
            if (note3.gateValue > 0) {
              note3.enabled = true;
              note3.pitch = scale.pitchAt(octave, offset + (note3.gateValue - 1));
              note3.velocity = velocity;
              note3.duration = duration;
            }
          } else {
            // AVERAGE mode can result in fractional values so we round() first to ensure we end up with a scale pitch
            note.pitch = scale.pitchAt(octave, offset + Math.round(gateValue) - 1);
          }
          break;

        case GATE.VELOCITY:
          const deltaToMax = 127 - velocity;
          if (this.gateSummingMode === GATE_SUMMING.MULTI) {
            const pitch = note.pitch;
            if (note.gateValue > 0) {
              // Pitches are not constrained to scale in for velocity gates.
              // Start with the highest pitch and go down to the track pitch on the last pattern.
              note.pitch = pitch + 2;
              note.velocity = velocity + (deltaToMax * (note.gateValue - 1) / 3);
            } else {
              note.enabled = false;
            }
            if (note2.gateValue > 0) {
              note2.enabled = true;
              note2.pitch = pitch + 1;
              note2.velocity = velocity + (deltaToMax * (note2.gateValue - 1) / 3);
              note2.duration = duration;
            }
            if (note3.gateValue > 0) {
              note3.enabled = true;
              note3.pitch = pitch;
              note3.velocity = velocity + (deltaToMax * (note3.gateValue - 1) / 3);
              note3.duration = duration;
            }
          } else {
            // With ADD mode, the velocity can exceed 127
            note.velocity = Math.min(127, velocity + (deltaToMax * (gateValue - 1) / 3));
          }
          break;

        default: console.log(`ERROR in notesForClock(). Unexpected gate mode "${this.gateMode}"`);

      }
    }
    return this._notes;
  }

  patternStepIndexForClock(clock, patternIndex) {
    if (clock < 0) return -1;
    const trackClock = Math.floor(clock / this.durationMultiplier);
    const pattern = this.patterns[patternIndex];
    return pattern.startStepIndex + trackClock.mod(pattern.length);
  }

  _gateValue() {
    const values = this._notes.map(n => n.gateValue);
    switch (this.gateSummingMode) {
      case GATE_SUMMING.ADD:
        return values.reduce((a, b) => a + b);

      case GATE_SUMMING.AVERAGE:
        const average = values.reduce((a, b) => a + b) / values.length;
        // The minimum value for a gate needs to be 1 so we don't go below the track's pitch/velocity
        // See the `- 1` logic in notesForClock().
        return (average > 0 && average < 1) ? 1 : average;

      case GATE_SUMMING.HIGHEST:
        return Math.max(...values);

      case GATE_SUMMING.LOWEST:
        const nonZeros = values.filter(v => v > 0);
        return nonZeros.length ? Math.min(...nonZeros) : 0;

      case GATE_SUMMING.MULTI:
        // notesForClock() handles the multi-note behavior.
        // We only need to attempt to process this step if there's a non-zero value:
        return values.reduce((a, b) => a + b);

      case GATE_SUMMING.RANDOM:
        return randomItem(values.filter(v => v > 0)) || 0;

      case GATE_SUMMING.RANDOM_WITH_0:
        return randomItem(values);

      default:
        console.error(`Error in _gateValue(). Unexpected gate summing mode "${this.gateSummingMode}"`);
        return 0;
    }
  }
}


<|start_filename|>src/Model/Pattern.js<|end_filename|>
import { NUMBER_OF, STEP_VALUE } from '../config';

// A pattern corresponds to the 8x8 grid of buttons on the Launchpad.
//
// It consists of 64 steps with integer values (typically 0-4 for off,green,yellow,orange,red lights),
// a start step, and an end step.
//
export default class Pattern {

  constructor({ trackIndex, index, type } = {}) {
    this.trackIndex = trackIndex;
    this.index = index;
    this.type = type;
    this.reset();
  }

  reset() {
    this._steps = Array(NUMBER_OF.STEPS).fill(0);
    this.startStepIndex = 0;
    this.endStepIndex = NUMBER_OF.STEPS - 1;
    this.mute = false;
  }

  get steps() {
    return this._steps;
  }

  set steps(steps) {
    // ensure this._steps remains valid
    for (var i = 0; i < NUMBER_OF.STEPS; i++) {
      this._steps[i] = steps[i] || 0;
    }
  }

  setRange(index1, index2) {
    this.startStepIndex = Math.min(index1, index2);
    this.endStepIndex = Math.max(index1, index2);
  }

  get range() {
    return [this.startStepIndex, this.endStepIndex];
  }

  get length() {
    return (this.endStepIndex - this.startStepIndex) + 1;
  }

  clear() {
    this._steps = Array(NUMBER_OF.STEPS).fill(0);
  }

  randomize() {
    const { steps, startStepIndex: start, endStepIndex: end } = this;
    for (let i = start; i <= end; i++) {
      steps[i] = Math.floor(NUMBER_OF.STEP_VALUES * Math.random())
    }
  }

  // fill in value with 25% chance
  randomFill(value) {
    const { steps, startStepIndex: start, endStepIndex: end } = this;
    for (let i = start; i <= end; i++) {
      if (Math.random() < 0.25) {
        steps[i] = value;
      }
    }
  }

  fill(value) {
    const { steps, startStepIndex: start, endStepIndex: end } = this;
    for (let i = start; i <= end; i++) {
      steps[i] = value
    }
  }

  replace(value) {
    const { steps, startStepIndex: start, endStepIndex: end } = this;
    for (let i = start; i <= end; i++) {
      if (steps[i] > 0) {
        steps[i] = value;
      }
    }
  }

  reverse() {
    const { steps, startStepIndex: start, endStepIndex: end } = this;
    const before = steps.slice(0, start);
    const activeSteps = steps.slice(start, end + 1);
    const after = steps.slice(end + 1);
    this.steps = before.concat(activeSteps.reverse(), after);
  }

  // Flips value 1 with 4, and 2 with 3
  invert() {
    const { steps, startStepIndex: start, endStepIndex: end } = this;
    for (let i = start; i <= end; i++) {
      const value = steps[i];
      if (value > 0) {
        steps[i] = NUMBER_OF.STEP_VALUES - value;
      }
    }
  }

  shift(amount) {
    const { steps, startStepIndex: start, endStepIndex: end, length } = this;
    const rot = start + amount.mod(length);
    const before = steps.slice(0, start);
    const left = steps.slice(start, rot);
    const right = steps.slice(rot, end + 1);
    const after = steps.slice(end + 1);
    this.steps = before.concat(right, left, after);
  }

  processNote(note, clock) {
    if (this.mute) return;

    const stepIndex = this.startStepIndex + clock.mod(this.length);
    const value = this.steps[stepIndex];
    if (value > 0) { // Assumption: 0 is always a NOOP
      switch (this.type) {
        case 'velocity':
          note.velocity += (127 - note.velocity) * value / 4;
          break;
        case 'duration':
          note.duration = STEP_VALUE.DURATION[value];
          break;
        case 'aftertouch':
          note.aftertouch = 127 * value / 4;
          break;
        case 'modulation':
          note.modulation = 127 * value / 4;
          break;
        case 'random mute':
          if (Math.random() <= (value / 4)) {
            note.mute = true;
          }
          break;
        case 'gate':
          note.gateValue = value;
          break;
        default: console.log(`ERROR in processNote(). Unexpected type "${this.type}"`);
      }
    }
  }
}


<|start_filename|>rollup.config.js<|end_filename|>
import babel from 'rollup-plugin-babel';
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/main.js',
  output: {
    file: 'device/launchpad-sequencer.js',
    format: 'es',
  },
  plugins: [
    babel(),
    resolve(), // allow `import './folder'` to resolve './folder/index.js'
    { renderChunk: code => code.replace(/\nexport.*/, '') }, // remove top-level exports
  ],
};
