John Legelis – Final Project

sketch

// John Legelis
// Section D
// FINAL

// Board dimensons
var b
var boardW = 200
var boardH = 350
var boardX = 10
var boardY = 10

// Oscillator dimensions/variables
var oscSpacing = boardH/4.1
var numberboxS = 20

// Dial dimensions/variables
var dR = 17
var dialStroke = 4
var dialSpacing = boardW/5
var theta = Math.PI/2
var overDial = false
var moving = false
var iy
var oldTheta
var thetaMin = -1/4*Math.PI
var thetaMax = 5/4*Math.PI

var pressed = false
var oscOn = false
var c = 2**(1/12) // Frequency diffrence between half steps
var cf = 2 **(1/100/12) // Frequency diffrence between cents
var volKnob = 0 // Index of volume knob
var pitchKnob = 1 // Index of pitch knob
var fineKnob = 2 // Index of fine knob

var wList = ["z", "x", "c", "v", "b", "n", "m", ",", ".", "/" ]
var bList = ["s", "d", false, "g", "h", "j", false, "l", ";"]



function setup() {
    createCanvas(600, 385);
    background(0);
    noFill()

    // Draw the board
    b = new ThreeWaveBoard(boardX, boardY, boardW, boardH)
    b.drawBoard()
}

function ThreeWaveBoard(X,Y,W,H) {
    // Board Dimensions
    this.bX = X
    this.bY = Y
    this.bW = W
    this.bH = H
    this.bStroke = 4
    this.oscillators = []

    // Draw board border ==> rectangle
    this.drawBoard = function() {
        stroke(255)
        strokeWeight(this.bStroke)
        rect(this.bX, this.bY, this.bW, this.bH)

        strokeWeight(2)
        rect(this.bW/2,this.bY + 50, 10,10)
        strokeWeight(1)
        textSize(15)
        text("FAT", this.bW/2 + 15, this.bY+60)
        textSize(29)
        text("3 Wave Oscillator", this.bX+8, this.bY+40)

        // Make 3 oscillators
        for (osc=0; osc<3; osc++){
            var oX = this.bX
            var oY = this.bY + oscSpacing*(osc+1)
            var oW = this.bW
            var oNum = osc + 1
            this.oscillators.push(new oscillator(oX, oY, oW, oNum))
            this.oscillators[osc].drawoscillator()
        }
    }

    // Update board
    this.updateBoard = function() {
        stroke(255)
        strokeWeight(this.bStroke)
        rect(this.bX, this.bY, this.bW, this.bH)

        strokeWeight(1)
        textSize(26)
        text("3 Wave Oscillator", this.bX+8, this.bY+40)

        // Update 3 oscillators
        for (osc=0; osc<3; osc++){
            this.oscillators[osc].updateOscillators()
        }
    }
}

function oscillator(X,Y,W,num) {
    // Oscillator Dimensions
    this.oX = X
    this.oY = Y
    this.oW = W
    this.oStroke = 4
    this.oTextStroke = 1
    this.oTextSize = 15
    this.number = num
    this.dials = []
    this.wave = new p5.Oscillator()
    this.f = false
    this.fine = 0
    this.volume = 0

    // Draw oscillator ==> line, number box, number
    this.drawoscillator = function() {
        //line and number box
        stroke(255)
        strokeWeight(this.oStroke)
        line(this.oX, this.oY, this.oX+this.oW, this.oY)
        rect(this.oX, this.oY, 20, 20)

        // Number text
        strokeWeight(this.oTextStroke)
        textSize(this.oTextSize)
        text(num, this.oX+5, this.oY+15)

        // Make volume dial
        var dX = this.oX + dR + this.oW/5 * (2)
        var dY = this.oY + oscSpacing/2
        this.dials.push(new Dial(dX, dY, dR, theta, -12, 12, "Volume"))
        this.dials[0].drawDials()

        // Make pitch dial
        var dX = this.oX + dR + this.oW/5 * (3)
        var dY = this.oY + oscSpacing/2
        this.dials.push(new Dial(dX, dY, dR, theta, -24, 24, "    Pitch"))
        this.dials[1].drawDials()

        // Make fine dial
        var dX = this.oX + dR + this.oW/5 * (4)
        var dY = this.oY + oscSpacing/2
        this.dials.push(new Dial(dX, dY, dR, theta, -12, 12, "    Fine"))
        this.dials[2].drawDials()
    }

    this.updateOscillators = function() {

        // Line and number box
        stroke(255)
        strokeWeight(this.oStroke)
        line(this.oX, this.oY, this.oX+this.oW, this.oY)
        rect(this.oX, this.oY, 20, 20)

        // Number text
        strokeWeight(this.oTextStroke)
        textSize(this.oTextSize)
        text(num, this.oX+5, this.oY+15)

        for (i=0; i<this.dials.length; i++){
            this.dials[i].drawDials()
        }
    }
}

function Dial(X, Y, R, theta, min, max, kind) {
    // Dial dimensions
    this.dialX = X
    this.dialY = Y
    this.dialR = R
    this.stroke = 4
    this.l1X = X
    this.l1Y = Y
    this.l2X = X + R*cos(theta)
    this.l2Y = Y - R*sin(theta)
    this.theta = theta
    this.over = false
    this.kind = kind
    // Ranges of each dials
    this.min = min
    this.max = max
    this.val = 0 // value of dial initialized to 0
    
    // Turns a decimal radian angle into a number rounded to a specific fration of the circle related to the ticks of the dial
    this.inc = function(dec) {
        this.range = (this.max - this.min)/2
        var increment = 3/4*Math.PI/(this.range)
        var d = dec/increment
        var r = round(d)
        var num = r * increment
        return num // Returns radian value along tick marker
    }

    // Draw dial shape and text
    this.drawDials = function() {
        this.l2X = X + R*cos(this.theta)
        this.l2Y = Y - R*sin(this.theta)
        strokeWeight(this.stroke)
        stroke(255)
        ellipse(this.dialX, this.dialY, this.dialR * 2, this.dialR * 2) // Dial circle
        line(this.l1X, this.l1Y, this.l2X, this.l2Y) // Dial line
        strokeWeight(1)
        text(this.val, this.dialX - 4, this.dialY + 32)
        text(this.kind, this.dialX - 25, this.dialY - 22)
        stroke(0)
        
    }
}

function draw() {

    stroke(255)
    noFill()
    background(0)
    b.updateBoard()
    drawPiano(230,20)

    // If a key is being pressed freq is its frequnncy
    if (keyIsPressed & !oscOn && typeof whichKey() == "string" && whichKey() == key) {
        oscOn = true

        for(waves = 0; waves < b.oscillators.length; waves++) {
            // Math for adjusting pitch and volume according to knobs
            b.oscillators[waves].f = frequencyfunc(whichKey()) * (c ** (b.oscillators[waves].dials[pitchKnob].val)) * (cf ** (b.oscillators[waves].dials[fineKnob].val))
            b.oscillators[waves].volume = 0.5 + map(b.oscillators[waves].dials[volKnob].val, -12, 12, -0.5 ,0.5)

            // Update oscilator parameters
            b.oscillators[waves].wave.amp(b.oscillators[waves].volume)
            b.oscillators[waves].wave.freq(b.oscillators[waves].f)
            b.oscillators[waves].wave.start()
        }
    }

    if (oscOn){
        // If another key is pressed while one is already down, priortize newest key
        if (whichKey != key){
            for(waves = 0; waves < b.oscillators.length; waves++) {
                b.oscillators[waves].f = frequencyfunc(whichKey()) * (c ** (b.oscillators[waves].dials[pitchKnob].val)) * (cf ** (b.oscillators[waves].dials[fineKnob].val))
                b.oscillators[waves].wave.freq(b.oscillators[waves].f)

                b.oscillators[waves].volume = 0.5 + map(b.oscillators[waves].dials[volKnob].val, -12, 12, -0.5 ,0.5)
                b.oscillators[waves].wave.amp(b.oscillators[waves].volume)

            }
        }

        if (!keyIsPressed){
            oscOn = false
        }
    }
    // Turn sounds off if oscillator is off --> no key is pressed
    if (!oscOn) {
        for(waves = 0; waves < b.oscillators.length; waves++) {
            b.oscillators[waves].wave.stop()
        }
    }

    // Check each dial in each oscillator
    for(osc=0; osc < b.oscillators.length; osc++){
        for(d=0; d < b.oscillators[osc].dials.length; d++){

            // If clicked / dragged on dial adjust the angle of specific dial respectively
            if ((dist(mouseX, mouseY, b.oscillators[osc].dials[d].dialX, b.oscillators[osc].dials[d].dialY)) <= (dR) & (mouseIsPressed) && !moving) {
                b.oscillators[osc].dials[d].over = true
                moving = true
                iy = mouseY
                oldTheta = b.oscillators[osc].dials[d].theta
            }
            if (!mouseIsPressed) {
                b.oscillators[osc].dials[d].over = false
                moving = false
            }
            if (b.oscillators[osc].dials[d].over == true) {
                var rawTheta = (oldTheta - map((iy - mouseY)*3, 0, height, 0, 2*Math.PI)) // Smooth theta from mouse
                var cTheta = constrain(rawTheta, thetaMin, thetaMax)    // Constrained theta based on min and max
                var iTheta = b.oscillators[osc].dials[d].inc(cTheta)    // Theta chopped to increment
                b.oscillators[osc].dials[d].val = round((map(iTheta, thetaMin, thetaMax, b.oscillators[osc].dials[d].max, b.oscillators[osc].dials[d].min)))
                b.oscillators[osc].dials[d].theta = iTheta
            }
        }
    }
}


// Return string value of a keyboard key, otherwise false
function whichKey(){
    if ( keyIsDown(90) || keyIsDown(83) || keyIsDown(88) || keyIsDown(68) || keyIsDown(67) || keyIsDown(86) || keyIsDown(71) || keyIsDown(66) || keyIsDown(72) || keyIsDown(78) || keyIsDown(74) || keyIsDown(77) || keyIsDown(188)|| keyIsDown(76) || keyIsDown(190)|| keyIsDown(186)|| keyIsDown(191) ) {
        k = key
    }
    if ((keyIsPressed) && (key == k)) {
        return str(k)
    }

    else{
        return false 
    }
}

// Function that draws the piano shapes and text
function drawPiano(x,y){
    var whiteW = 35
    var whiteH = 200
    var blackW = 20
    var blackH = 115

    // White Keys
    for (i=0; i<10; i++){
        push()
        translate(x,y)
        fill(255)
        rect(i*whiteW, 0, whiteW, whiteH)
        
        fill(0)
        textSize(25)
        text(wList[i],i*whiteW+ whiteW/2 - 6, whiteH - 30)
        pop()
    }

    // Black Keys
    for (j=0; j<9; j++){
        if ((j != 2) & (j != 6)) { // Exclude black key between E and F, and B and C
            push()
            translate(x,y)
            fill(0)
            rect((j+1) * whiteW - blackW/2, 0, blackW, blackH)

            fill(255)
            textSize(22)
            text(bList[j],(j+1) * whiteW - blackW/2 + 6 , blackH - 15)

            pop()
        }
    }
    fill(255)
    stroke(255)
    textSize(30)
    text("Click and Drag Dials!", 250,290)
    text("Play piano with keyboard!", 250, 340)
    noFill()
    strokeWeight(5)
    stroke(255)
    rect(240,250,340,110)
}

// Return the frequency of a keyboard key given string of key pressed
function frequencyfunc(str) {
    if (str == "z") {
        f = 523.25 // C5
    } else if (str == "s") {
        f = 554.37 // C#5
    } else if (str == "x") {
        f = 587.33 // D5
    } else if (str == "d") {
        f = 622.25 // Eb5
    } else if (str == "c") {
        f = 659.25 // E5
    } else if (str == "v") {
        f = 698.46 // F5
    } else if (str == "g") {
        f = 739.99 // F#5
    } else if (str == "b") {
        f = 783.99 // G5
    } else if (str == "h") {
        f = 830.61 // Ab5
    } else if (str == "n") {
        f = 880.00 // A5
    } else if (str == "j") {
        f = 932.33 // Bb5
    } else if (str == "m") {
        f = 987.77 // B5
    } else if (str == ",") {
        f = 1046.50 // C6
    } else if (str == "l") {
        f = 1108.73 // C#6
    } else if (str == ".") {
        f = 1174.66 // D6
    } else if (str == ";") {
        f = 1244.51 // Eb6
    } else if (str == "/") {
        f = 1318.51 // E6
    }
    
    else {
        f = false
    }

    return f
}

 

Click within the project to start!

My final project took a lot of effort on the back end side especially on the dial interaction. In order to make them clickable and draggable and easy to interface with, a lot of thinking and debugging had to be done in order for them to behave properly. Then the p5.Oscillators were used as a basis of the sound of my Oscillator. That package took some troubleshooting but eventually I worked out how to create this monophonic synthesizer. I am very proud of this instrument as it is highly functional and can be used as a real live instrument. It was lots of fun to work on and see it come together.

Leave a Reply