-
-
-
-
-
-
-
+
@@ -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;
+ }
}