Skip to content

Commit ace5c2a

Browse files
authored
piano debug (#608)
- Improves MidiConfig component with specifying note range - improves loading of existing config while entering a `study` session - **padding** - **change config object passing to v-select...** - **styling** - **fix default clobber of configured midi devices** - **condition the `save settings` button on state** - **gnetly fade save btn** - **add piano vis helper** - **tweaks** - **add midi range config**
2 parents 4949319 + 7cf4980 commit ace5c2a

File tree

4 files changed

+532
-16
lines changed

4 files changed

+532
-16
lines changed

packages/vue/src/components/Courses/CourseInformation.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
</router-link>
3333
</div>
3434
</transition>
35-
<midi-config v-if="isPianoCourse" :_id="_id" />
35+
<midi-config v-if="isPianoCourse" :_id="_id" class="my-3" />
3636

3737
<v-card class="my-2">
3838
<v-toolbar density="compact">

packages/vue/src/courses/piano/utility/MidiConfig.vue

Lines changed: 238 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<template>
22
<v-card>
3-
<v-card-title class="text-h5 bg-grey-lighten-2" primary-title> Configure Midi Device </v-card-title>
4-
3+
<v-toolbar dense>
4+
<v-toolbar-title>Configure Midi Device</v-toolbar-title>
5+
</v-toolbar>
56
<v-card-text>
67
<v-form v-if="midiSupported" onsubmit="return false;">
78
<v-select
@@ -11,8 +12,46 @@
1112
hint="Play some notes on your input device to test the connection"
1213
></v-select>
1314
<v-select v-model="selectedOutput" :items="outputs" label="Select Output"></v-select>
14-
<v-btn :loading="updatePending" @click="saveSettings"> Save these settings </v-btn>
15-
<v-btn color="primary" @click="playSound">Test midi output</v-btn>
15+
<v-divider class="my-4"></v-divider>
16+
<h3 class="text-subtitle-1 mb-2">Keyboard Range</h3>
17+
18+
<v-select
19+
v-model="selectedKeyboardRange"
20+
:items="keyboardRangeOptions"
21+
label="Select Keyboard Range"
22+
@update:model-value="updateCustomRangeFromPreset"
23+
></v-select>
24+
25+
<div v-if="selectedKeyboardRange === 'custom'" class="custom-range-inputs d-flex gap-4">
26+
<v-select
27+
v-model="lowestNote"
28+
:items="noteOptions"
29+
label="Lowest Note"
30+
@update:model-value="updateRangeAndCheckChanges"
31+
></v-select>
32+
33+
<v-select
34+
v-model="highestNote"
35+
:items="noteOptions"
36+
label="Highest Note"
37+
@update:model-value="updateRangeAndCheckChanges"
38+
></v-select>
39+
</div>
40+
41+
<piano-range-visualizer :lowest-note="lowestNote" :highest-note="highestNote" />
42+
43+
<div class="d-flex justify-space-between mt-3">
44+
<v-btn color="primary" @click="playSound">Test midi output</v-btn>
45+
<v-btn
46+
:loading="updatePending"
47+
:disabled="!configChanged && !updatePending"
48+
color="info"
49+
class="save-button"
50+
@click="saveSettings"
51+
>
52+
Save these settings
53+
</v-btn>
54+
</div>
1655
</v-form>
1756
<div v-else>
1857
<p>This quilt requires a midi input device, which is not supported by this browser.</p>
@@ -45,6 +84,7 @@ import { Status } from '@vue-skuilder/common';
4584
import { User } from '@/db/userDB';
4685
import { InputEventNoteon } from 'webmidi';
4786
import { getCurrentUser } from '@/stores/useAuthStore';
87+
import PianoRangeVisualizer from './PianoRangeVisualizer.vue';
4888
4989
interface MidiDevice {
5090
text: string;
@@ -53,6 +93,10 @@ interface MidiDevice {
5393
export default defineComponent({
5494
name: 'MidiConfig',
5595
96+
components: {
97+
PianoRangeVisualizer,
98+
},
99+
56100
props: {
57101
_id: {
58102
type: String,
@@ -70,6 +114,97 @@ export default defineComponent({
70114
const updatePending = ref(false);
71115
const user = ref<User>();
72116
117+
// managing config state updates
118+
const savedInputId = ref<string>('');
119+
const savedOutputId = ref<string>('');
120+
const configChanged = ref(false);
121+
122+
// Keyboard range
123+
const selectedKeyboardRange = ref('full-88');
124+
const lowestNote = ref(21); // A0
125+
const highestNote = ref(108); // C8
126+
const savedKeyboardRange = ref('');
127+
const savedLowestNote = ref(0);
128+
const savedHighestNote = ref(0);
129+
130+
const noteOptions = ref<Array<{ title: string; value: number }>>([]);
131+
132+
// Initialize noteOptions with all MIDI notes (0-127) with proper naming
133+
const initNoteOptions = () => {
134+
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
135+
const options = [];
136+
137+
// Generate all 128 MIDI notes with proper labeling
138+
for (let i = 0; i <= 127; i++) {
139+
const octave = Math.floor(i / 12) - 1;
140+
const noteName = noteNames[i % 12];
141+
142+
options.push({
143+
title: `${noteName}${octave} (${i})`, // Format: "C4 (60)"
144+
value: i,
145+
});
146+
}
147+
148+
noteOptions.value = options;
149+
};
150+
151+
const keyboardRangeOptions = ref([
152+
{ title: 'Full 88-key Piano (A0-C8)', value: 'full-88' },
153+
{ title: '76-key Keyboard (E1-G7)', value: '76-key' },
154+
{ title: '61-key Keyboard (C2-C7)', value: '61-key' },
155+
{ title: '49-key Keyboard (C3-C7)', value: '49-key' },
156+
{ title: '37-key Keyboard (C3-C6)', value: '37-key' },
157+
{ title: '25-key Keyboard (C4-C6)', value: '25-key' },
158+
{ title: 'Custom Range', value: 'custom' },
159+
]);
160+
161+
const checkConfigChanged = () => {
162+
configChanged.value =
163+
selectedInput.value !== savedInputId.value ||
164+
selectedOutput.value !== savedOutputId.value ||
165+
selectedKeyboardRange.value !== savedKeyboardRange.value ||
166+
(selectedKeyboardRange.value === 'custom' &&
167+
(lowestNote.value !== savedLowestNote.value || highestNote.value !== savedHighestNote.value));
168+
};
169+
170+
const updateCustomRangeFromPreset = () => {
171+
switch (selectedKeyboardRange.value) {
172+
case 'full-88':
173+
lowestNote.value = 21; // A0
174+
highestNote.value = 108; // C8
175+
break;
176+
case '76-key':
177+
lowestNote.value = 28; // E1
178+
highestNote.value = 103; // G7
179+
break;
180+
case '61-key':
181+
lowestNote.value = 36; // C2
182+
highestNote.value = 96; // C7
183+
break;
184+
case '49-key':
185+
lowestNote.value = 48; // C3
186+
highestNote.value = 96; // C7
187+
break;
188+
case '37-key':
189+
lowestNote.value = 48; // C3
190+
highestNote.value = 84; // C6
191+
break;
192+
case '25-key':
193+
lowestNote.value = 60; // C4
194+
highestNote.value = 84; // C6
195+
break;
196+
}
197+
checkConfigChanged();
198+
};
199+
200+
const updateRangeAndCheckChanges = () => {
201+
// Ensure lowest is always below highest
202+
if (lowestNote.value >= highestNote.value) {
203+
highestNote.value = lowestNote.value + 12; // At least an octave higher
204+
}
205+
checkConfigChanged();
206+
};
207+
73208
const playSound = () => {
74209
midi.value?.play([
75210
{
@@ -244,20 +379,58 @@ export default defineComponent({
244379
245380
watch(selectedInput, () => {
246381
midi.value?.selectInput(selectedInput.value);
382+
checkConfigChanged();
247383
});
248384
249385
watch(selectedOutput, () => {
250386
midi.value?.selectOutput(selectedOutput.value);
387+
checkConfigChanged();
251388
});
252389
253390
const retrieveSettings = async () => {
254391
const s = await user.value?.getCourseSettings(props._id);
392+
255393
if (s?.midiinput) {
256-
selectedInput.value = s.midiinput.toString();
394+
const savedInput = s.midiinput.toString();
395+
const inputExists = inputs.value.some((input) => input.value === savedInput);
396+
if (inputExists) {
397+
selectedInput.value = savedInput;
398+
savedInputId.value = savedInput;
399+
}
257400
}
401+
258402
if (s?.midioutput) {
259-
selectedOutput.value = s.midioutput.toString();
403+
const savedOutput = s.midioutput.toString();
404+
const outputExists = outputs.value.some((output) => output.value === savedOutput);
405+
if (outputExists) {
406+
selectedOutput.value = savedOutput;
407+
savedOutputId.value = savedOutput;
408+
}
409+
}
410+
411+
// Load keyboard range settings
412+
if (s?.keyboardRange) {
413+
savedKeyboardRange.value = s.keyboardRange.toString();
414+
selectedKeyboardRange.value = savedKeyboardRange.value;
415+
}
416+
417+
if (s?.lowestNote) {
418+
savedLowestNote.value = parseInt(s.lowestNote.toString());
419+
lowestNote.value = savedLowestNote.value;
420+
}
421+
422+
if (s?.highestNote) {
423+
savedHighestNote.value = parseInt(s.highestNote.toString());
424+
highestNote.value = savedHighestNote.value;
425+
}
426+
427+
// If we have custom values but not the 'custom' range type, set it
428+
if (s?.lowestNote && s?.highestNote && !s?.keyboardRange) {
429+
selectedKeyboardRange.value = 'custom';
260430
}
431+
432+
// Initialize with no pending changes after loading
433+
configChanged.value = false;
261434
};
262435
263436
const saveSettings = async () => {
@@ -271,12 +444,39 @@ export default defineComponent({
271444
key: 'midioutput',
272445
value: selectedOutput.value,
273446
},
447+
{
448+
key: 'keyboardRange',
449+
value: selectedKeyboardRange.value,
450+
},
451+
{
452+
key: 'lowestNote',
453+
value: lowestNote.value,
454+
},
455+
{
456+
key: 'highestNote',
457+
value: highestNote.value,
458+
},
274459
]);
460+
461+
// Update saved state references
462+
savedInputId.value = selectedInput.value;
463+
savedOutputId.value = selectedOutput.value;
464+
savedKeyboardRange.value = selectedKeyboardRange.value;
465+
savedLowestNote.value = lowestNote.value;
466+
savedHighestNote.value = highestNote.value;
467+
468+
configChanged.value = false;
275469
updatePending.value = false;
470+
471+
alertUser({
472+
text: 'Settings updated.',
473+
status: Status.ok,
474+
});
276475
};
277476
278477
onMounted(async () => {
279478
user.value = await getCurrentUser();
479+
initNoteOptions();
280480
try {
281481
midi.value = await SkMidi.instance();
282482
midiSupported.value = midi.value.state === 'ready' || midi.value.state === 'nodevice';
@@ -291,24 +491,30 @@ export default defineComponent({
291491
inputs.value = midi.value.inputs
292492
.filter((i) => i.state === 'connected')
293493
.map((i) => ({
294-
text: `${i.manufacturer}: ${i.name}`,
494+
title: `${i.manufacturer}: ${i.name}`,
295495
value: i.id,
296496
}));
297497
outputs.value = midi.value?.outputs
298498
.filter((i) => i.state === 'connected')
299499
.map((i) => ({
300-
text: `${i.manufacturer}: ${i.name}`,
500+
title: `${i.manufacturer}: ${i.name}`,
301501
value: i.id,
302502
}));
303503
} else {
304-
inputs.value = [{ text: 'No inputs available', value: '' }];
305-
outputs.value = [{ text: 'No outputs available', value: '' }];
504+
inputs.value = [{ title: 'No inputs available', value: '' }];
505+
outputs.value = [{ title: 'No outputs available', value: '' }];
306506
}
307507
308-
selectedInput.value = inputs.value[0].text;
309-
selectedOutput.value = outputs.value[0].text;
508+
await retrieveSettings();
310509
311-
retrieveSettings();
510+
// Only set defaults if no saved settings were loaded
511+
if (!selectedInput.value && inputs.value.length > 0) {
512+
selectedInput.value = inputs.value[0].value;
513+
}
514+
515+
if (!selectedOutput.value && outputs.value.length > 0) {
516+
selectedOutput.value = outputs.value[0].value;
517+
}
312518
}
313519
});
314520
@@ -321,7 +527,26 @@ export default defineComponent({
321527
updatePending,
322528
playSound,
323529
saveSettings,
530+
configChanged,
531+
lowestNote,
532+
highestNote,
533+
keyboardRangeOptions,
534+
selectedKeyboardRange,
535+
noteOptions,
536+
updateCustomRangeFromPreset,
537+
updateRangeAndCheckChanges,
324538
};
325539
},
326540
});
327541
</script>
542+
543+
<style scoped>
544+
.save-button {
545+
transition: opacity 0.5s ease-out;
546+
opacity: 1;
547+
}
548+
549+
.save-button.v-btn--disabled:not(.v-btn--loading) {
550+
opacity: 0;
551+
}
552+
</style>

0 commit comments

Comments
 (0)