// 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.