Skip to content

Commit e2eaa9b

Browse files
committed
Add simple note player (by name) 12-TET
1 parent f8ff561 commit e2eaa9b

File tree

8 files changed

+303
-69
lines changed

8 files changed

+303
-69
lines changed

demo/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import NavBar from './components/NavBar.vue'
77
<header>
88
<NavBar />
99
</header>
10-
<body>
10+
<body class="flex flex-col items-center text-center pt-8 gap-4">
1111
<RouterView />
1212
</body>
1313
</template>

demo/src/assets/main.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,19 @@ select:hover {
7272
color: var(--bg-color);
7373
cursor: pointer;
7474
}
75+
76+
.bordered {
77+
border: 4px solid var(--sub-color);
78+
padding: 0.5rem;
79+
border-radius: 0.5rem;
80+
gap: calc(var(--spacing) * 4);
81+
}
82+
83+
[disabled] {
84+
opacity: 0.5;
85+
}
86+
87+
hr {
88+
color: var(--sub-color);
89+
border-top-width: 2px;
90+
}

demo/src/components/PlayNote.vue

Lines changed: 175 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,140 @@
11
<template>
2-
<div class="flex flex-col items-center gap-4 pt-8 text-center">
3-
<label :for="a4_note_frequency_text">
4-
{{ a4_note_frequency_text }}
5-
<br />
6-
<input
7-
:id="a4_note_frequency_text"
8-
v-model="a4_note_frequency"
9-
type="number"
10-
:placeholder="a4_note_frequency_text"
11-
/>
12-
</label>
13-
14-
<label :for="note_name_text">
15-
{{ note_name_text }}
16-
<br />
17-
<input :id="note_name_text" v-model="note_name" type="text" :placeholder="note_name_text" />
18-
</label>
19-
20-
<label :for="note_frequency_text">
21-
{{ note_frequency_text }}
22-
<br />
23-
<input
24-
:id="note_frequency_text"
25-
v-model="note_frequency"
26-
type="number"
27-
:placeholder="note_frequency_text"
28-
/>
29-
<br />
30-
<input type="range" v-model="note_frequency" :id="note_frequency_text" min="20" max="2000" />
31-
</label>
32-
<span class="relative inline-flex">
33-
<button @click="playNote()">
34-
<span> {{ play_button_text }}</span>
35-
<span v-if="is_playing" class="absolute top-0 right-0 -mt-1 -mr-1 flex size-3">
36-
<span
37-
class="absolute h-full w-full animate-ping rounded-full bg-[var(--caret-color)] opacity-75"
38-
></span>
39-
<span class="relative size-3 rounded-full bg-[var(--main-color)]"></span>
40-
</span>
41-
</button>
42-
</span>
43-
44-
<label :for="volume_text">
45-
{{ volume_text }}
46-
<input :id="volume_text" type="range" v-model="volume" min="0" max="100" />
47-
{{ volume }}%
48-
</label>
49-
<label :for="oscillator_type_text" class="align-middle">
50-
{{ oscillator_type_text }}
51-
<span class="align-middle mr-1" :class="oscillator_icon"></span>
52-
<select v-model="oscillator_type" :id="oscillator_type_text">
53-
<option :key="index" v-for="(type, index) in oscillator_types" :value="type">
54-
{{ type }}
55-
</option>
56-
</select>
57-
</label>
2+
<div class="flex flex-col gap-4 w-[40vw]">
3+
<div class="flex flex-col bordered">
4+
<label :for="toggle_temperament_text">
5+
{{ toggle_temperament_text }}
6+
<div>
7+
<button
8+
@click="toggle_temperament = true"
9+
:class="{ 'text-[var(--bg-color)] !bg-[var(--text-color)]': toggle_temperament }"
10+
>
11+
ON
12+
</button>
13+
<button
14+
@click="toggle_temperament = false"
15+
:class="{ 'text-[var(--bg-color)] !bg-[var(--text-color)]': !toggle_temperament }"
16+
>
17+
OFF
18+
</button>
19+
</div>
20+
</label>
21+
22+
<hr />
23+
<div class="flex flex-col gap-4" :disabled="!toggle_temperament || null">
24+
<label :for="concert_pitch_text">
25+
{{ concert_pitch_text }}
26+
<i class="icon-[ph--bell-simple]"></i>
27+
<br />
28+
<input
29+
:id="concert_pitch_text"
30+
v-model="concert_pitch"
31+
type="number"
32+
:placeholder="concert_pitch_text"
33+
:disabled="!toggle_temperament"
34+
/>
35+
</label>
36+
37+
<label :for="temperament_text" class="align-middle">
38+
{{ temperament_text }}
39+
<i class="icon-[ph--divide] mr-2"></i>
40+
<select v-model="temperament" :id="temperament_text" :disabled="!toggle_temperament">
41+
<option :key="index" v-for="(type, index) in temperaments" :value="type">
42+
{{ type }}
43+
</option>
44+
</select>
45+
</label>
46+
47+
<label :for="note_name_text">
48+
{{ note_name_text }}
49+
<i class="icon-[ph--music-note-simple]"></i>
50+
<br />
51+
<input
52+
:id="note_name_text"
53+
v-model="note_name"
54+
type="text"
55+
:placeholder="note_name_text"
56+
:disabled="!toggle_temperament"
57+
/>
58+
</label>
59+
60+
<label :for="steps_text">
61+
{{ steps_text }}
62+
<i class="icon-[ph--ladder-simple]"></i>
63+
<br />
64+
<input
65+
:id="steps_text"
66+
v-model="steps"
67+
type="number"
68+
:placeholder="steps_text"
69+
:disabled="!toggle_temperament"
70+
:min="MIN_STEPS"
71+
:max="MAX_STEPS"
72+
/>
73+
<br />
74+
<input
75+
type="range"
76+
v-model="steps"
77+
:id="steps_text"
78+
:min="MIN_STEPS"
79+
:max="MAX_STEPS"
80+
:disabled="!toggle_temperament"
81+
/>
82+
</label>
83+
</div>
84+
</div>
85+
86+
<div class="flex flex-col bordered">
87+
<label :for="note_frequency_text">
88+
{{ note_frequency_text }}
89+
<i class="icon-[ph--bell-simple-ringing]"></i>
90+
<br />
91+
<input
92+
:id="note_frequency_text"
93+
v-model="note_frequency"
94+
type="number"
95+
:placeholder="note_frequency_text"
96+
:min="MIN_FREQEUNCY"
97+
:max="MAX_FREQEUNCY"
98+
/>
99+
<br />
100+
<input
101+
type="range"
102+
v-model="note_frequency"
103+
:id="note_frequency_text"
104+
:min="MIN_FREQEUNCY"
105+
:max="MAX_FREQEUNCY"
106+
/>
107+
</label>
108+
<span class="relative inline-flex mx-auto">
109+
<button @click="playNote()">
110+
<i v-if="is_playing" class="icon-[ph--megaphone] mr-2"></i>
111+
<span>{{ play_button_text }}</span>
112+
113+
<span v-if="is_playing" class="absolute top-0 right-0 -mt-1 -mr-1 flex size-3">
114+
<span
115+
class="absolute h-full w-full animate-ping rounded-full bg-[var(--caret-color)] opacity-75"
116+
></span>
117+
<span class="relative size-3 rounded-full bg-[var(--main-color)]"></span>
118+
</span>
119+
</button>
120+
</span>
121+
122+
<label :for="volume_text">
123+
{{ volume_text }}
124+
<i class="icon-[ph--speaker-high]"></i>
125+
<input :id="volume_text" type="range" v-model="volume" min="0" max="100" />
126+
{{ volume }}%
127+
</label>
128+
<label :for="oscillator_type_text" class="align-middle">
129+
{{ oscillator_type_text }}
130+
<span class="align-middle mr-2" :class="oscillator_icon"></span>
131+
<select v-model="oscillator_type" :id="oscillator_type_text">
132+
<option :key="index" v-for="(type, index) in oscillator_types" :value="type">
133+
{{ type }}
134+
</option>
135+
</select>
136+
</label>
137+
</div>
58138
</div>
59139
</template>
60140

@@ -68,7 +148,7 @@ const note_frequency_text = 'Frequency'
68148
const np = new notePlayer()
69149
70150
const is_playing = ref(false)
71-
const play_button_text = computed(() => (!is_playing.value ? 'Play note' : '🔊 Playing note...'))
151+
const play_button_text = computed(() => (!is_playing.value ? 'Play note' : 'Playing note...'))
72152
73153
function playNote() {
74154
if (!is_playing.value) {
@@ -82,6 +162,7 @@ function playNote() {
82162
83163
watch(note_frequency, () => {
84164
np.setFrequency(note_frequency.value)
165+
if (toggle_temperament.value) steps.value = np.getStepsFromFrequency(note_frequency.value)
85166
})
86167
87168
const volume = ref(50)
@@ -90,19 +171,51 @@ watch(volume, () => {
90171
np.setGain(volume.value / 100)
91172
})
92173
93-
const oscillator_types = ref<OscillatorType[]>(['sawtooth', 'sine', 'square', 'triangle'])
94-
const oscillator_type = ref<OscillatorType>('sine')
95174
const oscillator_type_text = 'Oscillator type'
175+
const oscillator_types = ref<OscillatorType[]>(['sine', 'square', 'triangle', 'sawtooth'])
176+
const oscillator_type = ref<OscillatorType>(oscillator_types.value[0])
96177
const oscillator_icon = computed(
97178
() => `icon-[ph--wave-${oscillator_type.value}${is_playing.value ? '-duotone' : ''}]`,
98179
)
99180
watch(oscillator_type, () => {
100181
np.setOscillatorType(oscillator_type.value)
101182
})
102183
103-
const a4_note_frequency = ref(440)
104-
const a4_note_frequency_text = 'A4 Frequency'
184+
const toggle_temperament = ref(true)
185+
const toggle_temperament_text = 'Toggle Tone Equal Temperament'
186+
187+
const concert_pitch_text = 'A4 Frequency (Concert pitch)'
188+
const concert_pitch = ref(440)
189+
watch(concert_pitch, () => {
190+
np.setConcertPitch(concert_pitch.value)
191+
updateLowestMetrics()
192+
})
193+
194+
const lowest_metrics = ref(np.getLowestMetrics())
195+
function updateLowestMetrics() {
196+
lowest_metrics.value = np.getLowestMetrics()
197+
}
198+
const MIN_FREQEUNCY = computed(() => lowest_metrics.value.frequency)
199+
const MIN_STEPS = computed(() => lowest_metrics.value.steps)
200+
const MAX_FREQEUNCY = 20000
201+
const MAX_STEPS = np.getStepsFromFrequency(MAX_FREQEUNCY)
202+
203+
type Temperament = 12
204+
const temperament_text = ref('Temperament')
205+
const temperaments = ref<Temperament[]>([12])
206+
const temperament = ref<Temperament>(temperaments.value[0])
207+
watch(temperament, () => {
208+
np.setTemperament(temperament.value)
209+
updateLowestMetrics()
210+
})
105211
106-
const note_name = ref('A4')
107212
const note_name_text = 'Note name'
213+
const note_name = ref('A4')
214+
215+
const steps_text = 'Steps'
216+
const steps = ref(0)
217+
watch(steps, () => {
218+
note_frequency.value = np.getFrenquencyFromSteps(steps.value)
219+
note_name.value = np.getNoteNameFromSteps(steps.value)
220+
})
108221
</script>

demo/src/views/HomeView.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@ import PlayNote from '../components/PlayNote.vue'
33
</script>
44

55
<template>
6-
<main>
7-
<PlayNote />
8-
</main>
6+
<PlayNote />
97
</template>

dist/index.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@ declare class notePlayer {
44
private oscillator;
55
private DEFAULT_FREQUENCY;
66
private DEFAULT_OSCILLATOR_TYPE;
7+
private concert_pitch;
8+
private CONCERT_PITCH_OCTAVE;
9+
private temperament;
10+
private noteNames;
711
constructor();
8-
private setOscillatorDefaultSettings;
12+
setOscillatorDefaultSettings(): void;
913
setOscillatorType(type: OscillatorType): void;
1014
setFrequency(frequency: number): void;
1115
setGain(gain: number): void;
1216
play(frequency?: number): void;
1317
stop(): void;
18+
setTemperament(temperament: number): void;
19+
setConcertPitch(concert_pitch: number): void;
20+
getFrenquencyFromSteps(steps: number): number;
21+
getStepsFromFrequency(frequency: number): number;
22+
getNoteNameFromSteps(steps: number): string;
23+
getLowestMetrics(): {
24+
steps: number;
25+
frequency: number;
26+
};
1427
}
1528

1629
export { notePlayer as default };

dist/index.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ var notePlayer = class {
55
oscillator;
66
DEFAULT_FREQUENCY = 440;
77
DEFAULT_OSCILLATOR_TYPE = "sine";
8+
concert_pitch = 440;
9+
// based on A4
10+
CONCERT_PITCH_OCTAVE = 4;
11+
// based on A4
12+
temperament = 12;
13+
noteNames = [
14+
"A",
15+
"A#",
16+
"B",
17+
"C",
18+
"C#",
19+
"D",
20+
"D#",
21+
"E",
22+
"F",
23+
"F#",
24+
"G",
25+
"G#"
26+
];
27+
// Based on Chromatic scale (12-TET) only, TODO: auto detect notes based on temperament
828
constructor() {
929
this.audioCtx = new AudioContext();
1030
this.gainNode = this.audioCtx.createGain();
@@ -41,6 +61,30 @@ var notePlayer = class {
4161
stop() {
4262
this.gainNode.disconnect(this.audioCtx.destination);
4363
}
64+
setTemperament(temperament) {
65+
this.temperament = temperament;
66+
}
67+
setConcertPitch(concert_pitch) {
68+
this.concert_pitch = concert_pitch;
69+
}
70+
getFrenquencyFromSteps(steps) {
71+
const frequency = 2 ** (steps / this.temperament) * this.concert_pitch;
72+
return frequency;
73+
}
74+
getStepsFromFrequency(frequency) {
75+
const steps = this.temperament * Math.log2(frequency / this.concert_pitch);
76+
return Math.round(steps);
77+
}
78+
getNoteNameFromSteps(steps) {
79+
const octave = Math.floor(steps / this.temperament) + this.CONCERT_PITCH_OCTAVE;
80+
let noteIndex = (steps >= 0 ? steps : Math.abs(this.noteNames.length + steps)) % this.temperament;
81+
return `${this.noteNames[noteIndex]}${octave}`;
82+
}
83+
getLowestMetrics() {
84+
const steps = -this.temperament * this.CONCERT_PITCH_OCTAVE;
85+
const frequency = this.getFrenquencyFromSteps(steps);
86+
return { steps, frequency };
87+
}
4488
};
4589
export {
4690
notePlayer as default

0 commit comments

Comments
 (0)