Final Project - Synthesia Clone Code Refactoring, Cleanup

Final Project - Synthesia Clone Code Refactoring, Cleanup

Prepare a 5 minute presentation to demonstrate what your project does that emphasizes its computational aspects. - How did "coding" this project make it different from doing it in software? - You don't have to explain the whole thing. Pick one algorithm you wrote and deconstruct it for us. - If your project is interactive, please be prepared to have someone else in the class interact with it to demo. - If your project can only be demo'd outside of class, please show a short video (< 2 minutes) of the experience.

Overview - Arduino & P5.js Interface Framework

For my project I revisited my Physical Computing project and conducted a code refactor and cleanup of the code and interfaces used. One particular aspect that was challenging for me was the repetitive code invovled in each new sketch and I wanted to find a way to make it reusable and repeatable across multiple projects without having to rewrite the same boilerplate code.

image

Two Areas of Interest

Arduino Helper

  • Automatically Connects to Arduino Port: Constructor sets up, detects automatically the arduino port, and then connects to it. Using handleAutomaticPortSelection
  • (Duplex) Call & Response Polling Method of Communication: Provides a configurable framework within which to receive and write data serially. Provides an onData handler for receiving data, and a pollFn function that implements the serialWrite interval necessary to implement this.

Synthesia Clone

  • Detection of Which Notes to Hit: There's a small implementation of which notes to hit that detects the notes at the edge of the screen using detection of the shape from the edge in a naive collision detection way using distance.
// Code Snippet Arduino Helper
class SerialHelper {
    constructor(opts = {}) {
        this.opts = opts;
        this.pollInterval = opts.pollInterval || 300;
        // Requires portName
        // - Otherwise don't create the object
        if (!opts.portName) {
            console.log("INFO: No portname provided.")

            // List serial ports & return
            // - Autoselects if that configuration is opted for
            const serial = new p5.SerialPort();
            serial.on('list', this.onList.bind(this));
            serial.list();
            return
        }

        // Run as usual
        // ------------

        // Baud Rate
        const portName = opts.portName
        const baudRate = opts.baudRate || 9600;

        // Create the serial helper object
        this.serial = new p5.SerialPort();

        // Attach functions for
        this.serial.on('connected', this.onConnected.bind(this));
        this.serial.on('open', this.onOpen.bind(this));
        this.serial.on('data', _.throttle(this.onData.bind(this), 6, {
            leading: true,
            trailing: false
        })),
        this.serial.on('error', this.onError.bind(this));
        this.serial.on('close', this.onClose.bind(this));

        // Open the port
        this.serial.open(portName, {
            baudRate: baudRate
        });

        // Request data every 300 miliseconds
        setInterval(() => {
            this.serial.write("\n");
        }, this.pollInterval);
    }

    onList(portList) {
        // Display the list of ports
        portList.forEach((port, i) => {
            console.log(`${i} | ${port}`);
        })

        // Select default portname (using magic)
        const opts = this.opts;
        if (opts.autoSelectPortName) {
            this.handleAutomaticPortSelection(portList);
        } else {
            console.log("ERR: Please specify a portname from the list below.");
            return; // Break - Since we cannot determine portname
        }

        return portList;
    }

    handleAutomaticPortSelection(portList) {
        const opts = this.opts;
        const validPortNames = portList.filter(i => i.indexOf('tty.usbmodem') >= 0);
        console.log(`INFO: Selecting portname automatically from ${validPortNames}`)
        if (validPortNames.length > 0) {
            opts.portName = validPortNames[0];
            this.serial = new SerialHelper(opts);
            console.log(`INFO: Selected portname ${opts.portName}`)
        } else {
            console.log("ERR: Autoselect Failed. Please specify a portname from the list below.");
            return; // Break - Since we cannot determine portname
        }
    }

    onConnected() {
        if (isValidFunction(this.opts, 'onConnected')) {
            this.opts.onConnected();
            this.serial.clear();
        } else {
            console.log('INFO: onConnected not defined | Running default')
            console.log('INFO: The serial port opened.')
        }
    }

    onOpen() {
        if (isValidFunction(this.opts, 'onOpen')) {
            this.opts.onOpen();
            this.serial.clear();
        } else {
            console.log('INFO: onOpen not defined | Running default')
            console.log('INFO: Connected to server. Clearing serial buffer...');
            this.serial.clear();
        }
    }

    onData() {
        if (isValidFunction(this.opts, 'onData')) {
            let inData = this.serial.readLine();
            this.opts.onData(inData);
        } else {
            console.log('INFO: onData not defined | Running default')
        }
    }

    onError() {
        if (isValidFunction(this.opts, 'onError')) {2
            this.opts.onError();
        } else {
            console.log('INFO: onError not defined | Running default')
        }
    }

    onClose() {
        if (isValidFunction(this.opts, 'onClose')) {
            this.opts.onClose();
        } else {
            console.log('INFO: onClose not defined | Running default')
        }
    }
}

function isValidFunction(obj, fnName) {
    return obj[fnName] !== undefined && obj[fnName] instanceof Function
}
// Code Snippet - Sketch
let piano;
let debug;
let possibleNotes = [65, 83, 68, 70, 71];
let serialHelper;
let inData = "Awaiting Connection...."
let osc

function setup() {
    createCanvas(600, 600)
    piano = new PianoSynth();

    // Create a helper for managing p5 Serial Processing
    serialHelper = new SerialHelper({
        autoSelectPortName: true,
        onData: (data) => {
            if (data !== undefined && data !== "") {
                inData = data;
                // Handle key press
                fill('black');
                ellipse(50, 50, 50)
                if (data.indexOf("1") !== -1) {
                    inData = inData.split(",").map(i => parseInt(i)).map((v, i) => v === 1 ? possibleNotes[i] : v);
                    piano.keyPressed();
                }
            }
        },
        pollFn: (serial) => {
            serial.write(byte('\n'))
        },
        pollInterval: 1000 / 60,
    });

    // A triangle oscillator
    osc = new p5.TriOsc();
    // Start silent
    osc.start();
    osc.amp(0);
}

function draw() {
    background(240);

    frameRate(60);
    noStroke();

    text(`${keyCode}, ${keyIsPressed}`, 450, 65)
    piano.draw();

    if (keyIsPressed) {
        piano.keyPressed();
    }

}

class PianoSynth {
    gap = 60;
    width = 40;
    unit = 20;
    notes = [];

    constructor() {
        this.startingPoint = 0;
        this.globalTime = 0;

        this.notes = _.times(50).map(i => {
            let selectedNote = random(possibleNotes.filter((i, v) => v !== 3));
            let selectedNoteIndex = possibleNotes.indexOf(selectedNote);

            let l = random([1, 2, 3]);
            return {
                id: selectedNoteIndex,
                t: (i * 4) + (l),
                length: l,
                key: selectedNote
            }
        })
        fill('black');
    }

    draw() {
        text(`${keyCode}, ${keyIsPressed}`, 450, 50)
        text(`${inData}`, 450, 125)

        // Draw each note
        this.notes.forEach((note, i) => {
            let x = (note.id * this.gap);
            let y = this.startingPoint + this.globalTime - (note.t * this.unit);
            let l = this.unit * note.length;

            // Update the position
            note.x = x;
            note.y = y;

            // Draw the note
            fill('blue');
            rect(x, y, 30, l);

            // Add Text
            fill('white');
            text(char(note.key), x + 10, y + 20);

            fill('black');
        })

        if (this.notesPending().length === 0) {
            this.moveDown();
        }

        if (inData !== undefined && inData !== "" && inData instanceof Array) {
            inData.forEach((v, i) => {
                fill(color(0, 150));

                if (v !== 0) {
                    ellipse((i * 60) + 20, height, 25)
                }
                fill('black')
            })

            const notes = [60, 62, 64, 65, 67, 69, 71];
            inData.map((v, i) => v === 0 ? -1 : possibleNotes.indexOf(v)).filter((v) => v !== -1).forEach(key => {
                playNote(notes[key], 50);
            });
        }
    }

    moveDown(t = 1) {
        this.globalTime = this.globalTime + t;
    }

    notesPending() {
        let notesAtBottom = [];

        // Remove any note off screen;
        this.notes = this.notes.filter(note => note.y < height)

        this.notes.forEach((note, i) => {
            // Ignore if the note is off screen
            if (note.y > height) {
                return;
            }

            let d = dist(note.x, note.y, note.x, height)
            // Or the distance is less than the size
            if (d < this.unit * note.length) {
                notesAtBottom.push(note);
            }
        })

        notesAtBottom.forEach((note, i) => {
            text(`${note.y}, ${note.key}, ${height}`, 450, 90 + (i * 30))
        })

        return notesAtBottom;
    }

    correctNoteBeingPlayed() {
        // Each note that should be pressed is being pressed
        let notesPending = this.notesPending().map(i => i.key);
        let correctNotes = _.intersection(possibleNotes, notesPending);
        let incorrectNotes = _.difference(possibleNotes, notesPending);
        if (correctNotes.length === 0) {
            return false;
        }

        // Only correct notes are played
        let keyIsPlayed = key => this.keyIsDown(key);
        let correctNotesPlayed = _.every(correctNotes, keyIsPlayed);
        return correctNotesPlayed
    }

    keyIsDown(key) {
        let notesPlayed = inData.filter(v => v !== 0);
        if (notesPlayed.indexOf(key) !== -1) {
            return true;
        }
        return keyIsDown(key)
    }

    keyPressed() {
        if (this.correctNoteBeingPlayed()) {
            this.moveDown();
        }
    }
}


function playNote(note, duration) {
    osc.freq(midiToFreq(note));
    // Fade it in
    osc.fade(0.5, 0.2);

    // If we sest a duration, fade it out
    if (duration) {
        setTimeout(function () {
            osc.fade(0, 0.2);
        }, duration - 50);
    }

    setTimeout(function () {
        osc.fade(0, 0.5);
    }, duration);

}

// Fade it out when we release
function mouseReleased() {

}