diff --git a/demo/src/App.vue b/demo/src/App.vue index cb1537e..1e9270f 100644 --- a/demo/src/App.vue +++ b/demo/src/App.vue @@ -7,7 +7,7 @@ import NavBar from './components/NavBar.vue'
- + diff --git a/demo/src/assets/main.css b/demo/src/assets/main.css index 28014e0..cf93471 100644 --- a/demo/src/assets/main.css +++ b/demo/src/assets/main.css @@ -72,3 +72,19 @@ select:hover { color: var(--bg-color); cursor: pointer; } + +.bordered { + border: 4px solid var(--sub-color); + padding: 0.5rem; + border-radius: 0.5rem; + gap: calc(var(--spacing) * 4); +} + +[disabled] { + opacity: 0.5; +} + +hr { + color: var(--sub-color); + border-top-width: 2px; +} diff --git a/demo/src/components/PlayNote.vue b/demo/src/components/PlayNote.vue index a673bef..f34a103 100644 --- a/demo/src/components/PlayNote.vue +++ b/demo/src/components/PlayNote.vue @@ -1,43 +1,140 @@ @@ -51,7 +148,7 @@ const note_frequency_text = 'Frequency' const np = new notePlayer() const is_playing = ref(false) -const play_button_text = computed(() => (!is_playing.value ? 'Play note' : '🔊 Playing note...')) +const play_button_text = computed(() => (!is_playing.value ? 'Play note' : 'Playing note...')) function playNote() { if (!is_playing.value) { @@ -65,6 +162,7 @@ function playNote() { watch(note_frequency, () => { np.setFrequency(note_frequency.value) + if (toggle_temperament.value) steps.value = np.getStepsFromFrequency(note_frequency.value) }) const volume = ref(50) @@ -73,13 +171,54 @@ watch(volume, () => { np.setGain(volume.value / 100) }) -const oscillator_types = ref(['sawtooth', 'sine', 'square', 'triangle']) -const oscillator_type = ref('sine') const oscillator_type_text = 'Oscillator type' +const oscillator_types = ref(['sine', 'square', 'triangle', 'sawtooth']) +const oscillator_type = ref(oscillator_types.value[0]) const oscillator_icon = computed( () => `icon-[ph--wave-${oscillator_type.value}${is_playing.value ? '-duotone' : ''}]`, ) watch(oscillator_type, () => { np.setOscillatorType(oscillator_type.value) }) + +const toggle_temperament = ref(true) +const toggle_temperament_text = 'Toggle Tone Equal Temperament' + +const concert_pitch_text = 'A4 Frequency (Concert pitch)' +const concert_pitch = ref(440) +watch(concert_pitch, () => { + np.setConcertPitch(concert_pitch.value) + updateLowestMetrics() +}) + +const lowest_metrics = ref(np.getLowestMetrics()) +function updateLowestMetrics() { + lowest_metrics.value = np.getLowestMetrics() +} +const MIN_FREQEUNCY = computed(() => lowest_metrics.value.frequency) +const MIN_STEPS = computed(() => lowest_metrics.value.step) +const MAX_FREQEUNCY = 20000 +const MAX_STEPS = np.getStepsFromFrequency(MAX_FREQEUNCY) + +type Temperament = 12 +const temperament_text = ref('Temperament') +const temperaments = ref([12]) +const temperament = ref(temperaments.value[0]) +watch(temperament, () => { + np.setTemperament(temperament.value) + updateLowestMetrics() +}) + +const note_name_text = 'Note name' +const note_name = ref('A4') +watch(note_name, () => { + note_frequency.value = np.getFrequencyFromNoteName(note_name.value) +}) + +const steps_text = 'Steps' +const steps = ref(0) +watch(steps, () => { + note_frequency.value = np.getFrenquencyFromSteps(steps.value) + note_name.value = np.getNoteNameFromSteps(steps.value) +}) diff --git a/demo/src/views/HomeView.vue b/demo/src/views/HomeView.vue index aa0bf6b..1bd5ec7 100644 --- a/demo/src/views/HomeView.vue +++ b/demo/src/views/HomeView.vue @@ -3,7 +3,5 @@ import PlayNote from '../components/PlayNote.vue' diff --git a/dist/index.d.ts b/dist/index.d.ts index 979b4b2..e6a1c94 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -4,13 +4,30 @@ declare class notePlayer { private oscillator; private DEFAULT_FREQUENCY; private DEFAULT_OSCILLATOR_TYPE; + private concert_pitch; + private CONCERT_PITCH_OCTAVE; + private temperament; + private noteNames; + private noteNameRegex; constructor(); - private setOscillatorDefaultSettings; + setOscillatorDefaultSettings(): void; setOscillatorType(type: OscillatorType): void; setFrequency(frequency: number): void; setGain(gain: number): void; play(frequency?: number): void; stop(): void; + setTemperament(temperament: number): void; + setConcertPitch(concert_pitch: number): void; + getFrenquencyFromSteps(steps: number): number; + getStepsFromFrequency(frequency: number): number; + getNoteNameFromSteps(steps: number): string; + getLowestStep(): number; + getLowestFrequency(): number; + getLowestMetrics(): { + step: number; + frequency: number; + }; + getFrequencyFromNoteName(noteFullName: string): number; } export { notePlayer as default }; diff --git a/dist/index.js b/dist/index.js index 59a5760..97a8bff 100644 --- a/dist/index.js +++ b/dist/index.js @@ -5,6 +5,27 @@ var notePlayer = class { oscillator; DEFAULT_FREQUENCY = 440; DEFAULT_OSCILLATOR_TYPE = "sine"; + concert_pitch = 440; + // based on A4 + CONCERT_PITCH_OCTAVE = 4; + // based on A4 + temperament = 12; + noteNames = [ + "A", + "A#", + "B", + "C", + "C#", + "D", + "D#", + "E", + "F", + "F#", + "G", + "G#" + ]; + // Based on Chromatic scale (12-TET) only, TODO: auto detect notes based on temperament + noteNameRegex = /^(A|B|C|D|E|F|G)(#?)(\d)$/; constructor() { this.audioCtx = new AudioContext(); this.gainNode = this.audioCtx.createGain(); @@ -41,6 +62,57 @@ var notePlayer = class { stop() { this.gainNode.disconnect(this.audioCtx.destination); } + setTemperament(temperament) { + this.temperament = temperament; + } + setConcertPitch(concert_pitch) { + this.concert_pitch = concert_pitch; + } + getFrenquencyFromSteps(steps) { + const frequency = 2 ** (steps / this.temperament) * this.concert_pitch; + return frequency; + } + getStepsFromFrequency(frequency) { + const steps = this.temperament * Math.log2(frequency / this.concert_pitch); + return Math.round(steps); + } + getNoteNameFromSteps(steps) { + const octave = Math.floor(steps / this.temperament) + this.CONCERT_PITCH_OCTAVE; + let noteIndex = (steps >= 0 ? steps : Math.abs(this.noteNames.length + steps)) % this.temperament; + return `${this.noteNames[noteIndex]}${octave}`; + } + getLowestStep() { + const step = -this.temperament * this.CONCERT_PITCH_OCTAVE; + return step; + } + getLowestFrequency() { + const step = this.getLowestStep(); + const frequency = this.getFrenquencyFromSteps(step); + return frequency; + } + getLowestMetrics() { + return { step: this.getLowestStep(), frequency: this.getLowestFrequency() }; + } + getFrequencyFromNoteName(noteFullName) { + console.log("Incoming noteFullName:", noteFullName); + const match = noteFullName.match(this.noteNameRegex); + console.log(noteFullName, match); + if (!match) { + throw new Error("Invalid note format"); + } + const [, noteLetter, sharp, octaveStr] = match; + const noteName = `${noteLetter}${sharp}`; + const octave = Number(octaveStr); + const noteIndex = this.noteNames.findIndex((note) => note === noteName); + if (noteIndex === -1) { + throw new Error("Invalid note"); + } + const stepsFromOctave = this.temperament * octave; + const stepsBase = noteIndex; + const steps = this.getLowestStep() + stepsFromOctave + stepsBase; + const frequency = this.getFrenquencyFromSteps(steps); + return frequency; + } }; export { notePlayer as default diff --git a/package.json b/package.json index 535784e..a7ce2ea 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "type": "module", "scripts": { "build": "tsup", - "dev": "tsup --watch" + "dev": "tsup --watch", + "demo": "cd demo && npm run dev" }, "files": [ "dist", diff --git a/src/index.ts b/src/index.ts index e0381e5..ae8daef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,25 @@ export default class notePlayer { private oscillator: OscillatorNode; private DEFAULT_FREQUENCY = 440; private DEFAULT_OSCILLATOR_TYPE: OscillatorType = "sine"; + private concert_pitch = 440; // based on A4 + private CONCERT_PITCH_OCTAVE = 4; // based on A4 + private temperament = 12; + private noteNames = [ + "A", + "A#", + "B", + "C", + "C#", + "D", + "D#", + "E", + "F", + "F#", + "G", + "G#", + ]; // Based on Chromatic scale (12-TET) only, TODO: auto detect notes based on temperament + private noteNameRegex = /^(A|B|C|D|E|F|G)(#?)(\d)$/; + constructor() { this.audioCtx = new AudioContext(); @@ -16,7 +35,7 @@ export default class notePlayer { this.oscillator.start(); } - private setOscillatorDefaultSettings() { + setOscillatorDefaultSettings() { this.oscillator.frequency.setValueAtTime( this.DEFAULT_FREQUENCY, this.audioCtx.currentTime @@ -46,4 +65,68 @@ export default class notePlayer { stop() { this.gainNode.disconnect(this.audioCtx.destination); } + + setTemperament(temperament: number) { + this.temperament = temperament; + } + setConcertPitch(concert_pitch: number) { + this.concert_pitch = concert_pitch; + } + getFrenquencyFromSteps(steps: number) { + const frequency = 2 ** (steps / this.temperament) * this.concert_pitch; + return frequency; + } + getStepsFromFrequency(frequency: number) { + const steps = this.temperament * Math.log2(frequency / this.concert_pitch); + return Math.round(steps); + } + + getNoteNameFromSteps(steps: number) { + const octave = + Math.floor(steps / this.temperament) + this.CONCERT_PITCH_OCTAVE; + + let noteIndex = + (steps >= 0 ? steps : Math.abs(this.noteNames.length + steps)) % + this.temperament; + return `${this.noteNames[noteIndex]}${octave}`; + } + + getLowestStep() { + const step = -this.temperament * this.CONCERT_PITCH_OCTAVE; + return step; + } + getLowestFrequency() { + const step = this.getLowestStep(); + const frequency = this.getFrenquencyFromSteps(step); + return frequency; + } + getLowestMetrics() { + return { step: this.getLowestStep(), frequency: this.getLowestFrequency() }; + } + + getFrequencyFromNoteName(noteFullName: string) { + console.log("Incoming noteFullName:", noteFullName); + + const match = noteFullName.match(this.noteNameRegex); + console.log(noteFullName, match); + if (!match) { + throw new Error("Invalid note format"); + } + + const [, noteLetter, sharp, octaveStr] = match; + + const noteName = `${noteLetter}${sharp}`; + const octave = Number(octaveStr); + + const noteIndex = this.noteNames.findIndex((note) => note === noteName); + + if (noteIndex === -1) { + throw new Error("Invalid note"); + } + const stepsFromOctave = this.temperament * octave; + const stepsBase = noteIndex; + const steps = this.getLowestStep() + stepsFromOctave + stepsBase; + const frequency = this.getFrenquencyFromSteps(steps); + return frequency; + } }