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
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';
4584import { User } from ' @/db/userDB' ;
4685import { InputEventNoteon } from ' webmidi' ;
4786import { getCurrentUser } from ' @/stores/useAuthStore' ;
87+ import PianoRangeVisualizer from ' ./PianoRangeVisualizer.vue' ;
4888
4989interface MidiDevice {
5090 text: string ;
@@ -53,6 +93,10 @@ interface MidiDevice {
5393export 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