Skip to content

Commit

Permalink
feat(simpleSynth): add simple synth (no loop capability yet)
Browse files Browse the repository at this point in the history
  • Loading branch information
domi7777 committed Nov 4, 2024
1 parent 47fa9ea commit 38afd3f
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 0 deletions.
120 changes: 120 additions & 0 deletions src/samples/synth-frequencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// copied from https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Simple_synth

/* eslint-disable no-loss-of-precision */

type Note = 'A' | 'A#' | 'B' | 'C' | 'C#' | 'D' | 'D#' | 'E' | 'F' | 'F#' | 'G' | 'G#';

export type NoteFreq = {
[key in Note]: number;
}

function createNoteTable() {
const noteFreq: Partial<NoteFreq>[] = [];
for (let i=0; i< 9; i++) {
noteFreq[i] = {};
}

noteFreq[0]['A'] = 27.500000000000000;
noteFreq[0]['A#'] = 29.135235094880619;
noteFreq[0]['B'] = 30.867706328507756;

noteFreq[1]['C'] = 32.703195662574829;
noteFreq[1]['C#'] = 34.647828872109012;
noteFreq[1]['D'] = 36.708095989675945;
noteFreq[1]['D#'] = 38.890872965260113;
noteFreq[1]['E'] = 41.203444614108741;
noteFreq[1]['F'] = 43.653528929125485;
noteFreq[1]['F#'] = 46.249302838954299;
noteFreq[1]['G'] = 48.999429497718661;
noteFreq[1]['G#'] = 51.913087197493142;
noteFreq[1]['A'] = 55.000000000000000;
noteFreq[1]['A#'] = 58.270470189761239;
noteFreq[1]['B'] = 61.735412657015513;
// …

noteFreq[2]['C'] = 65.406391325149658;
noteFreq[2]['C#'] = 69.295657744218024;
noteFreq[2]['D'] = 73.41619197935189;
noteFreq[2]['D#'] = 77.781745930520227;
noteFreq[2]['E'] = 82.406889228217482;
noteFreq[2]['F'] = 87.307057858250971;
noteFreq[2]['F#'] = 92.498605677908599;
noteFreq[2]['G'] = 97.998858995437323;
noteFreq[2]['G#'] = 103.826174394986284;
noteFreq[2]['A'] = 110.0;
noteFreq[2]['A#'] = 116.540940379522479;
noteFreq[2]['B'] = 123.470825314031027;

noteFreq[3]['C'] = 130.812782650299317;
noteFreq[3]['C#'] = 138.591315488436048;
noteFreq[3]['D'] = 146.83238395870378;
noteFreq[3]['D#'] = 155.563491861040455;
noteFreq[3]['E'] = 164.813778456434964;
noteFreq[3]['F'] = 174.614115716501942;
noteFreq[3]['F#'] = 184.997211355817199;
noteFreq[3]['G'] = 195.997717990874647;
noteFreq[3]['G#'] = 207.652348789972569;
noteFreq[3]['A'] = 220.0;
noteFreq[3]['A#'] = 233.081880759044958;
noteFreq[3]['B'] = 246.941650628062055;

noteFreq[4]['C'] = 261.625565300598634;
noteFreq[4]['C#'] = 277.182630976872096;
noteFreq[4]['D'] = 293.66476791740756;
noteFreq[4]['D#'] = 311.12698372208091;
noteFreq[4]['E'] = 329.627556912869929;
noteFreq[4]['F'] = 349.228231433003884;
noteFreq[4]['F#'] = 369.994422711634398;
noteFreq[4]['G'] = 391.995435981749294;
noteFreq[4]['G#'] = 415.304697579945138;
noteFreq[4]['A'] = 440.0;
noteFreq[4]['A#'] = 466.163761518089916;
noteFreq[4]['B'] = 493.883301256124111;

noteFreq[5]['C'] = 523.251130601197269;
noteFreq[5]['C#'] = 554.365261953744192;
noteFreq[5]['D'] = 587.32953583481512;
noteFreq[5]['D#'] = 622.253967444161821;
noteFreq[5]['E'] = 659.255113825739859;
noteFreq[5]['F'] = 698.456462866007768;
noteFreq[5]['F#'] = 739.988845423268797;
noteFreq[5]['G'] = 783.990871963498588;
noteFreq[5]['G#'] = 830.609395159890277;
noteFreq[5]['A'] = 880.0;
noteFreq[5]['A#'] = 932.327523036179832;
noteFreq[5]['B'] = 987.766602512248223;

noteFreq[6]['C'] = 1046.502261202394538;
noteFreq[6]['C#'] = 1108.730523907488384;
noteFreq[6]['D'] = 1174.659071669630241;
noteFreq[6]['D#'] = 1244.507934888323642;
noteFreq[6]['E'] = 1318.510227651479718;
noteFreq[6]['F'] = 1396.912925732015537;
noteFreq[6]['F#'] = 1479.977690846537595;
noteFreq[6]['G'] = 1567.981743926997176;
noteFreq[6]['G#'] = 1661.218790319780554;
noteFreq[6]['A'] = 1760.0;
noteFreq[6]['A#'] = 1864.655046072359665;
noteFreq[6]['B'] = 1975.533205024496447;

noteFreq[7]['C'] = 2093.004522404789077;
noteFreq[7]['C#'] = 2217.461047814976769;
noteFreq[7]['D'] = 2349.318143339260482;
noteFreq[7]['D#'] = 2489.015869776647285;
noteFreq[7]['E'] = 2637.020455302959437;
noteFreq[7]['F'] = 2793.825851464031075;
noteFreq[7]['F#'] = 2959.955381693075191;
noteFreq[7]['G'] = 3135.963487853994352;
noteFreq[7]['G#'] = 3322.437580639561108;
noteFreq[7]['A'] = 3520.000000000000000;
noteFreq[7]['A#'] = 3729.310092144719331;
noteFreq[7]['B'] = 3951.066410048992894;

noteFreq[8]['C'] = 4186.009044809578154;
return noteFreq;
}

export const notesFrequencies = createNoteTable();
export const allFrequencies = notesFrequencies
.flatMap(note => Object.values(note))
.sort((a, b) => a - b);
8 changes: 8 additions & 0 deletions src/scenes/EmptyScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {hexToColor} from '../utils/colors.ts';
import {FontFamily} from '../utils/fonts.ts';
import {DrumsScene} from './DrumsScene.ts';
import {GibberishScene} from './GiberishScene.ts';
import {SimpleSynthScene} from './SimpleSynthScene.ts';

export class EmptyScene extends Phaser.Scene {
static key = 'EmptyScene';
Expand All @@ -24,6 +25,9 @@ export class EmptyScene extends Phaser.Scene {

this.createMatrix();

const simpleSynthButton = this.instrumentButtons[2][1];
simpleSynthButton.setData('text', this.addText(simpleSynthButton, 'Simple Synth'));

const drumsButton = this.instrumentButtons[1][2];
drumsButton.setData('text', this.addText(drumsButton, 'Drums'));

Expand All @@ -37,6 +41,7 @@ export class EmptyScene extends Phaser.Scene {
drumsButton,
otherDrumsButton,
gibberishButton,
simpleSynthButton
];
activeButtons.forEach(button => button
.setFillStyle(hexToColor('#FFF'), 0.5)
Expand All @@ -56,6 +61,9 @@ export class EmptyScene extends Phaser.Scene {
gibberishButton.on(Phaser.Input.Events.POINTER_UP, () => {
this.scene.add(trackSceneKey, GibberishScene, true, {numberOfPads: 8});
});
simpleSynthButton.on(Phaser.Input.Events.POINTER_UP, () => {
this.scene.add(trackSceneKey, SimpleSynthScene, true);
});

window.addEventListener('resize', () => this.resizeScene());
this.resizeScene();
Expand Down
78 changes: 78 additions & 0 deletions src/scenes/PadsScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Phaser from 'phaser';
import {LoopTracksScene} from './LoopTracksScene.ts';
import {rotateArray} from '../utils/math.ts';
import {hexToColor} from '../utils/colors.ts';

type Pad = {
instrument: number,
button: Phaser.GameObjects.Rectangle,
}
export abstract class PadsScene extends Phaser.Scene {

private pads: Pad[] = [];
protected constructor(private cols: number, private rows: number) {
super();
}

abstract playSound(index: number): void;

create() {
this.createPads();
this.scene.get(LoopTracksScene.key).events.emit('track-selected');
}

protected createPads(){
const numberOfPads = this.cols * this.rows;
this.pads = new Array(numberOfPads).fill(0).map((_, index) => {
return this.createPad(index, numberOfPads);
});

const resizePads = () => {
const isPortrait = window.innerWidth < window.innerHeight;
const colNumber = isPortrait ? this.cols : this.rows;
const rowNumber = isPortrait ? this.rows : this.cols;
const width = isPortrait ? window.innerWidth / colNumber : (window.innerWidth - LoopTracksScene.sceneWidthHeight) / colNumber;
const height = isPortrait ? (window.innerHeight - LoopTracksScene.sceneWidthHeight) / rowNumber : window.innerHeight / rowNumber;

const currentPads = isPortrait ? rotateArray(this.pads, rowNumber, colNumber): this.pads;

currentPads.forEach(({ button }, index) => {
const x = (index % colNumber) * width;
const y = Math.floor(index / colNumber) * height;
const offsetX = isPortrait ? 0 : LoopTracksScene.sceneWidthHeight;
const offsetY = isPortrait ? LoopTracksScene.sceneWidthHeight : 0;
button.setSize(width, height).setPosition(offsetX + x, offsetY + y);
});
};

// Attach the event listener and initial call
window.addEventListener('resize', resizePads);
resizePads();
}

protected createPad(index: number, numberOfPads: number): Pad {
const padColor = Phaser.Display.Color.HSLToColor((numberOfPads - index) / (numberOfPads * 1.5), 1, 0.5)
const inactiveColor = padColor.darken(40).color;
const hitColor = padColor.brighten(4).color;
const button = this.add.rectangle()
.setFillStyle(inactiveColor)
.setStrokeStyle(2, hexToColor('#FFF'), 0.8)
.setInteractive()
.setOrigin(0, 0);
button.on('pointerdown', (e: Phaser.Input.Pointer) => {
if (e.downElement?.tagName?.toLowerCase() !== 'canvas') {
return;
}
this.playSound(index);
button.setFillStyle(hitColor);
// TODO send a call to the loop scene to play the instrument ? or an object
// this.scene.get(LoopTracksScene.key).events.emit('instrument-played', {instrument, scene: this});
}).on('pointerup', () => button.setFillStyle(inactiveColor))
.on('pointerout', () => button.setFillStyle(inactiveColor));
return {
instrument: index,
button
};
}

}
31 changes: 31 additions & 0 deletions src/scenes/SimpleSynthScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {PadsScene} from './PadsScene.ts';
import {allFrequencies} from '../samples/synth-frequencies.ts';
import {createAudioContext} from '../samples/sample-utils.ts';

export class SimpleSynthScene extends PadsScene {

constructor() {
super(8, 11);
}

create() {
super.create();
}

playSound(index: number): void {
const note = allFrequencies[index];
console.log('Playing note', note);
// play the sound
const audioContext = createAudioContext();
const oscillator = audioContext.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(note, audioContext.currentTime);
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(1, audioContext.currentTime); // Start loud
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 1.5); // Long decay (1.5 seconds)
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.5);
}
}

0 comments on commit 38afd3f

Please sign in to comment.