simple-synth/synth.js

402 lines
11 KiB
JavaScript

let canvas;
let analyserNode;
let signalData;
function noteToHz(note) {
switch (note) {
case "C":
return 261.63;
case "D":
return 293.66;
case "E":
return 329.63;
case "F":
return 349.23;
case "G":
return 392;
case "A":
return 440;
case "B":
return 493.88;
default:
return 0;
}
}
function composeChord(chord) {
switch (chord) {
case "C": {
return ["C", "G", "E"];
}
case "D": {
return ["D", "F", "A"];
}
case "E": {
return ["E", "G", "B"];
}
case "F": {
return ["F", "A", "C"];
}
case "G": {
return ["G", "B", "D"];
}
case "A": {
return ["A", "C", "E"];
}
case "B": {
return ["B", "D", "F"];
}
default: {
return ["C", "D", "E"];
}
}
}
class Synth {
constructor() {
this.audioContext = new AudioContext();
this.gain = this.audioContext.createGain();
this.oscillators = [
this.createOscillator("sine", 261.63, 0),
this.createOscillator("sine", 261.63, 0),
this.createOscillator("sine", 261.63, 0),
];
this.gain.connect(this.audioContext.destination);
}
createOscillator(type = "sine", freq = 440, startOctave) {
const osc = this.audioContext.createOscillator();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.audioContext.currentTime);
// connect it to the gain node
this.gain.gain.setTargetAtTime(0.1, this.audioContext.currentTime, 0);
osc.connect(this.gain);
// wrap around the container and add to array
const oscContainer = {
osc,
isPlaying: false,
baseFreq: freq,
currentOctave: startOctave,
};
return oscContainer;
}
startOsc(oscContainer) {
if (!oscContainer.isPlaying) {
oscContainer.osc.start();
oscContainer.isPlaying = true;
}
}
stopOsc(oscContainer) {
if (oscContainer.isPlaying) {
let currentFreq = oscContainer.osc.frequency.value;
console.log("the current frequency: " + currentFreq);
let currentType = oscContainer.osc.type;
console.log("the current type: " + currentType);
oscContainer.osc.stop();
oscContainer.isPlaying = false;
oscContainer.osc = this.audioContext.createOscillator();
oscContainer.osc.type = currentType;
oscContainer.osc.frequency.setValueAtTime(
currentFreq,
this.audioContext.currentTime
);
oscContainer.osc.connect(this.gain);
console.log(oscContainer);
}
}
stopAll() {
for (let osc of this.oscillators) {
this.stopOsc(osc);
}
}
createFilter(type, freq, Q) {
const filter = this.audioContext.createBiquadFilter();
filter.type = type;
filter.frequency.setValueAtTime(freq, this.audioContext.currentTime);
filter.Q.setValueAtTime(Q, this.audioContext.currentTime);
return filter;
}
}
let synth = new Synth();
function updateFrequency(
event,
synth,
oscContainer,
voiceIndex,
note,
octaveShift,
detuneAmount
) {
let baseFreq = oscContainer.baseFreq;
let currentFreq = baseFreq;
if (octaveShift) {
if (octaveShift === "up") {
currentFreq = baseFreq * 2;
oscContainer.baseFreq = currentFreq;
} else {
currentFreq = baseFreq / 2;
oscContainer.baseFreq = currentFreq;
}
}
if (note) {
let noteInHz = noteToHz(note);
// if the note in hz is not in the octave the oscillator is currently at
// I will need to transform it down to the correct octave.
currentFreq = noteInHz;
oscContainer.baseFreq = noteInHz;
oscContainer.currentOctave = 0;
let octaveDisplay = document.getElementById(
"octavedisplay" + (voiceIndex + 1)
);
console.log(octaveDisplay);
console.log("octavedisplay" + (voiceIndex + 1));
octaveDisplay.value = 0;
console.log(noteInHz);
console.log("current octave for voice: " + oscContainer.currentOctave);
}
if (detuneAmount) {
currentFreq = currentFreq + detuneAmount;
}
console.log(currentFreq);
oscContainer.osc.frequency.setValueAtTime(
currentFreq,
synth.audioContext.currentTime
);
}
function setupOctaveControls(voiceIndex, synth) {
// Get the display element for the current voice
let octaveDisplay = document.getElementById(
"octavedisplay" + (voiceIndex + 1)
);
// Set up event listener for the octave down button for the current voice
document
.getElementById("octavedown" + (voiceIndex + 1))
.addEventListener("click", (event) => {
const osc = synth.oscillators[voiceIndex];
osc.currentOctave--;
octaveDisplay.value = osc.currentOctave;
updateFrequency(event, synth, osc, voiceIndex, null, "down", null);
});
// Set up event listener for the octave up button for the current voice
document
.getElementById("octaveup" + (voiceIndex + 1))
.addEventListener("click", (event) => {
const osc = synth.oscillators[voiceIndex];
osc.currentOctave++;
octaveDisplay.value = osc.currentOctave;
updateFrequency(event, synth, osc, voiceIndex, null, "up", null);
});
}
// onload ------------------------------------------------------------------
window.onload = function () {
// start button
document
.getElementById("activateVoice1")
.addEventListener("click", (event) => {
console.log("voice 1 start clicked");
synth.audioContext.resume();
let osc1 = synth.oscillators[0];
if (osc1.isPlaying) {
synth.stopOsc(osc1);
event.target.textContent = "On";
event.target.style.backgroundColor = "#2f855a";
} else {
synth.startOsc(osc1);
event.target.textContent = "Off";
event.target.style.backgroundColor = "red";
}
});
document
.getElementById("activateVoice2")
.addEventListener("click", (event) => {
console.log("voice 2 start clicked");
synth.audioContext.resume();
let osc2 = synth.oscillators[1];
if (osc2.isPlaying) {
synth.stopOsc(osc2);
event.target.textContent = "On";
event.target.style.backgroundColor = "#2f855a";
} else {
synth.startOsc(osc2);
event.target.textContent = "Off";
event.target.style.backgroundColor = "red";
}
});
document
.getElementById("activateVoice3")
.addEventListener("click", (event) => {
console.log("voice 3 start clicked");
synth.audioContext.resume();
let osc3 = synth.oscillators[2];
if (osc3.isPlaying) {
synth.stopOsc(osc3);
event.target.textContent = "On";
event.target.style.backgroundColor = "#2f855a";
} else {
synth.startOsc(osc3);
event.target.textContent = "Off";
event.target.style.backgroundColor = "red";
}
});
// handle waveform selection
document.querySelectorAll("input[name='wavechoice1']").forEach((rb) => {
rb.addEventListener("change", (event) => {
let selectedWaveform = document.querySelector(
"input[name='wavechoice1']:checked"
).value;
synth.oscillators[0].osc.type = selectedWaveform;
});
});
document.querySelectorAll("input[name='wavechoice2']").forEach((rb) => {
rb.addEventListener("change", (event) => {
let selectedWaveform = document.querySelector(
"input[name='wavechoice2']:checked"
).value;
synth.oscillators[1].osc.type = selectedWaveform;
});
});
document.querySelectorAll("input[name='wavechoice3']").forEach((rb) => {
rb.addEventListener("change", (event) => {
let selectedWaveform = document.querySelector(
"input[name='wavechoice3']:checked"
).value;
synth.oscillators[2].osc.type = selectedWaveform;
});
});
// Loop through each voice and set up its octave control buttons
for (let i = 0; i < synth.oscillators.length; i++) {
setupOctaveControls(i, synth); // Call setupOctaveControls for each voice
}
// detune
const detuneSliderVoice1 = document.getElementById("detunevoice1");
const detunevoice1Display = document.getElementById("detunevoice1display");
detuneSliderVoice1.addEventListener("input", (event) => {
let osc = synth.oscillators[0];
let detune = parseFloat(detuneSliderVoice1.value);
console.log(detune);
updateFrequency(event, synth, osc, 0, null, null, detune);
});
// const filterSliderVoice1 = document.getElementById("filtervoice1");
// const filtervoice1Display = document.getElementById("filtervoice1display");
// filterSliderVoice1.addEventListener("input", (event) => {
// const osc = synth.oscillators[0];
// let selectedFreq = parseFloat(filterSliderVoice1.value);
// const lpf = synth.createFilter("lowpass", 500, 1);
// osc.osc.connect(lpf);
// lpf.connect(synth.gain);
// });
// handle chord changes
document.querySelectorAll("input[name='chordchoice']").forEach((rb) => {
rb.addEventListener("change", (event) => {
let selectedChord = document.querySelector(
"input[name='chordchoice']:checked"
).value;
let notesForChord = composeChord(selectedChord);
console.log(notesForChord);
for (let i = 0; i < synth.oscillators.length; i++) {
if (synth.oscillators[i].isPlaying) {
console.log("playing note: " + notesForChord[i] + " on voice: " + i);
updateFrequency(
event,
synth,
synth.oscillators[i],
i,
notesForChord[i],
null,
null
);
}
}
});
});
};
function setup() {
let wave1Canvas = document.getElementById("waveform1viz");
if (synth) {
analyserNode = synth.audioContext.createAnalyser();
analyserNode.smoothingTimeConstant = 1;
signalData = new Float32Array(analyserNode.fftSize);
synth.oscillators[0].osc.connect(analyserNode);
canvas1 = createCanvas(
wave1Canvas.clientWidth,
wave1Canvas.clientHeight,
wave1Canvas
);
}
}
function draw() {
background("black");
const dim = min(height, width);
if (synth) {
if (synth.audioContext) {
if (analyserNode) {
analyserNode.getFloatTimeDomainData(signalData);
const signal = rmss(signalData);
const scale = 0.015;
const size = dim * scale * signal;
const redOffset = map(signal, 0, 0.5, -50, 200);
const blueOffset = map(signal, 0, 0.5, 255, -30);
const fillAlpha = map(signal, 0, 0.5, 0, 200);
stroke(Math.min(50 + redOffset, 256), 10, 0 + blueOffset);
fill(Math.min(50 + redOffset, 256), 10, 0 + blueOffset, fillAlpha);
// noFill();
strokeWeight(10);
// circle(width / 2, height / 2, size);
let numPoints = 100;
let radius = 100;
let glitchMagnitude = 150;
beginShape(POINTS);
for (let i = 0; i <= numPoints; i++) {
let angle = (TWO_PI / numPoints) * i;
let r = radius + (size / 2) * glitchMagnitude + Math.random() * 10;
let x = width / 2 + r * cos(angle);
let y = height / 2 + r * sin(angle);
vertex(x, y);
}
endShape();
}
}
}
}
function rmss(data) {
let rms = 0;
for (let i = 0; i < data.length; i++) {
rms += data[i] * data[i];
}
rms = Math.sqrt(rms / data.length);
return rms;
}