Skip to content

Commit ba9aa4a

Browse files
authored
Merge pull request #22 from MongooseMoo/gmcp-midi-fix-disconnect
feat: Add intentional disconnect flags to prevent unwanted MIDI auto-reconnection
2 parents 12eb2fd + 3a5415e commit ba9aa4a

File tree

4 files changed

+111
-9
lines changed

4 files changed

+111
-9
lines changed

src/MidiService.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ class MidiService {
6868
outputConnected: false
6969
};
7070

71+
// Flags to track intentional disconnections (prevent auto-reconnect until next server connection)
72+
private intentionalDisconnectFlags = {
73+
input: false,
74+
output: false
75+
};
76+
7177
async initialize(): Promise<boolean> {
7278
try {
7379
this.jzz = await JZZ();
@@ -222,6 +228,9 @@ class MidiService {
222228
}
223229
});
224230

231+
// Clear intentional disconnect flag since user manually connected
232+
this.intentionalDisconnectFlags.input = false;
233+
225234
console.log(`Connected to MIDI input: ${this.connectionState.inputDeviceName}`);
226235
return true;
227236
} catch (error) {
@@ -254,6 +263,9 @@ class MidiService {
254263
lastOutputDeviceId: deviceId
255264
}
256265
});
266+
267+
// Clear intentional disconnect flag since user manually connected
268+
this.intentionalDisconnectFlags.output = false;
257269

258270
console.log(`Connected to virtual MIDI synthesizer: ${virtualMidiService.getPortName()}`);
259271
return true;
@@ -284,6 +296,9 @@ class MidiService {
284296
}
285297
});
286298

299+
// Clear intentional disconnect flag since user manually connected
300+
this.intentionalDisconnectFlags.output = false;
301+
287302
console.log(`Connected to MIDI output: ${this.connectionState.outputDeviceName}`);
288303
return true;
289304
} catch (error) {
@@ -319,6 +334,44 @@ class MidiService {
319334
}
320335
}
321336

337+
// Disconnect specific device type
338+
disconnectDevice(deviceType: 'input' | 'output'): void {
339+
if (deviceType === 'input') {
340+
if (this.inputDevice) {
341+
this.inputDevice.close?.();
342+
this.inputDevice = null;
343+
}
344+
this.inputCallback = null;
345+
this.connectionState.inputConnected = false;
346+
this.connectionState.inputDeviceId = undefined;
347+
this.connectionState.inputDeviceName = undefined;
348+
console.log("Disconnected input device");
349+
} else if (deviceType === 'output') {
350+
if (this.outputDevice) {
351+
this.outputDevice.close?.();
352+
this.outputDevice = null;
353+
}
354+
this.connectionState.outputConnected = false;
355+
this.connectionState.outputDeviceId = undefined;
356+
this.connectionState.outputDeviceName = undefined;
357+
console.log("Disconnected output device");
358+
}
359+
}
360+
361+
// Disconnect with intentional flag setting
362+
disconnectWithIntent(deviceType: 'input' | 'output' | 'both'): void {
363+
this.setIntentionalDisconnect(deviceType);
364+
365+
if (deviceType === 'input') {
366+
this.disconnectDevice('input');
367+
} else if (deviceType === 'output') {
368+
this.disconnectDevice('output');
369+
} else {
370+
// 'both'
371+
this.disconnect();
372+
}
373+
}
374+
322375
disconnect(): void {
323376
if (this.inputDevice) {
324377
this.inputDevice.close?.();
@@ -359,6 +412,11 @@ class MidiService {
359412
return { ...this.connectionState };
360413
}
361414

415+
// Get intentional disconnect flags
416+
get intentionalDisconnectStatus() {
417+
return { ...this.intentionalDisconnectFlags };
418+
}
419+
362420
// Device change monitoring
363421
onDeviceChange(callback: DeviceChangeCallback): () => void {
364422
this.deviceChangeCallbacks.add(callback);
@@ -447,36 +505,42 @@ class MidiService {
447505

448506
// For input devices, we need a callback, so we'll defer this until someone actually tries to connect
449507
// Just log what we would try to reconnect to
450-
if (preferences.lastInputDeviceId) {
508+
if (preferences.lastInputDeviceId && !this.intentionalDisconnectFlags.input) {
451509
const inputDevices = this.getInputDevices();
452510
const lastInputDevice = inputDevices.find(d => d.id === preferences.lastInputDeviceId);
453511
if (lastInputDevice) {
454512
console.log(`Input device available for auto-reconnect: ${lastInputDevice.name}`);
455513
}
514+
} else if (preferences.lastInputDeviceId && this.intentionalDisconnectFlags.input) {
515+
console.log(`Skipping input auto-reconnect due to intentional disconnect`);
456516
}
457517

458518
// For output devices, we can attempt reconnection immediately
459-
if (preferences.lastOutputDeviceId) {
519+
if (preferences.lastOutputDeviceId && !this.intentionalDisconnectFlags.output) {
460520
const outputDevices = this.getOutputDevices();
461521
const lastOutputDevice = outputDevices.find(d => d.id === preferences.lastOutputDeviceId);
462522
if (lastOutputDevice) {
463523
console.log(`Attempting to auto-reconnect to output device: ${lastOutputDevice.name}`);
464524
await this.connectOutputDevice(preferences.lastOutputDeviceId);
465525
}
526+
} else if (preferences.lastOutputDeviceId && this.intentionalDisconnectFlags.output) {
527+
console.log(`Skipping output auto-reconnect due to intentional disconnect`);
466528
}
467529
}
468530

469531
// Public method to attempt auto-reconnection when callback is available
470532
async attemptAutoReconnectInput(callback: MidiInputCallback): Promise<boolean> {
471533
const preferences = preferencesStore.getState().midi;
472534

473-
if (preferences.lastInputDeviceId) {
535+
if (preferences.lastInputDeviceId && !this.intentionalDisconnectFlags.input) {
474536
const inputDevices = this.getInputDevices();
475537
const lastInputDevice = inputDevices.find(d => d.id === preferences.lastInputDeviceId);
476538
if (lastInputDevice) {
477539
console.log(`Attempting to auto-reconnect to input device: ${lastInputDevice.name}`);
478540
return await this.connectInputDevice(preferences.lastInputDeviceId, callback);
479541
}
542+
} else if (preferences.lastInputDeviceId && this.intentionalDisconnectFlags.input) {
543+
console.log(`Skipping input auto-reconnect due to intentional disconnect`);
480544
}
481545

482546
return false;
@@ -498,6 +562,25 @@ class MidiService {
498562
return false;
499563
}
500564

565+
// Set intentional disconnect flag for a device type
566+
setIntentionalDisconnect(deviceType: 'input' | 'output' | 'both'): void {
567+
if (deviceType === 'input' || deviceType === 'both') {
568+
this.intentionalDisconnectFlags.input = true;
569+
}
570+
if (deviceType === 'output' || deviceType === 'both') {
571+
this.intentionalDisconnectFlags.output = true;
572+
}
573+
console.log(`Set intentional disconnect flag for: ${deviceType}`);
574+
}
575+
576+
// Reset all intentional disconnect flags (called when server reconnects)
577+
resetIntentionalDisconnectFlags(): void {
578+
this.intentionalDisconnectFlags.input = false;
579+
this.intentionalDisconnectFlags.output = false;
580+
console.log("Reset intentional disconnect flags");
581+
}
582+
583+
501584
// Shutdown and cleanup
502585
shutdown(): void {
503586
this.disconnect();

src/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { AutoreadMode, preferencesStore } from "./PreferencesStore";
3232
import { WebRTCService } from "./WebRTCService";
3333
import FileTransferManager from "./FileTransferManager.js";
3434
import { GMCPMessageRoomInfo, RoomPlayer } from "./gmcp/Room"; // Import RoomPlayer
35+
import { midiService } from "./MidiService";
3536

3637
export interface WorldData {
3738
liveKitTokens: string[];
@@ -261,6 +262,10 @@ class MudClient extends EventEmitter {
261262
this.telnet = new TelnetParser(new WebSocketStream(this.ws));
262263
this.ws.onopen = () => {
263264
this._connected = true;
265+
266+
// Reset MIDI intentional disconnect flags when successfully reconnecting to server
267+
midiService.resetIntentionalDisconnectFlags();
268+
264269
this.emit("connect");
265270
this.emit("connectionChange", true);
266271
};
@@ -328,6 +333,12 @@ class MudClient extends EventEmitter {
328333
this.currentRoomInfo = null; // Reset room info on cleanup
329334
this.webRTCService.cleanup();
330335
this.fileTransferManager.cleanup();
336+
337+
// Reset intentional disconnect flag after handling disconnect
338+
if (this.intentionalDisconnect) {
339+
this.intentionalDisconnect = false;
340+
}
341+
331342
this.emit("disconnect");
332343
this.emit("connectionChange", false);
333344
}

src/components/MidiStatus.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,19 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ client }) => {
7676
// Update reconnectable device suggestions and attempt auto-reconnection
7777
const updateReconnectableDevices = async () => {
7878
const prefs = preferencesStore.getState().midi;
79+
const intentionalFlags = midiService.intentionalDisconnectStatus;
7980
const newReconnectables: { input?: { id: string, name: string }; output?: { id: string, name: string } } = {};
8081

8182

8283
// Check if last input device is available but not connected
8384
if (prefs.lastInputDeviceId && !connectionState.inputConnected) {
8485
const device = inputDevices.find(d => d.id === prefs.lastInputDeviceId);
86+
8587
if (device && midiService.canReconnectToDevice(prefs.lastInputDeviceId, 'input')) {
8688
newReconnectables.input = device;
8789

88-
// Auto-reconnect input device if midiPackage is available
89-
if (midiPackage) {
90+
// Only auto-reconnect if not intentionally disconnected
91+
if (midiPackage && !intentionalFlags.input) {
9092
try {
9193
const success = await midiPackage.connectInputDevice(prefs.lastInputDeviceId);
9294
if (success) {
@@ -101,18 +103,21 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ client }) => {
101103
} catch (error) {
102104
console.error('Error during input auto-reconnection:', error);
103105
}
106+
} else if (intentionalFlags.input) {
107+
console.log(`Skipping input auto-reconnect due to intentional disconnect`);
104108
}
105109
}
106110
}
107111

108112
// Check if last output device is available but not connected
109113
if (prefs.lastOutputDeviceId && !connectionState.outputConnected) {
110114
const device = outputDevices.find(d => d.id === prefs.lastOutputDeviceId);
115+
111116
if (device && midiService.canReconnectToDevice(prefs.lastOutputDeviceId, 'output')) {
112117
newReconnectables.output = device;
113118

114-
// Auto-reconnect output device if midiPackage is available
115-
if (midiPackage) {
119+
// Only auto-reconnect if not intentionally disconnected
120+
if (midiPackage && !intentionalFlags.output) {
116121
try {
117122
const success = await midiPackage.connectOutputDevice(prefs.lastOutputDeviceId);
118123
if (success) {
@@ -127,6 +132,8 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ client }) => {
127132
} catch (error) {
128133
console.error('Error during output auto-reconnection:', error);
129134
}
135+
} else if (intentionalFlags.output) {
136+
console.log(`Skipping output auto-reconnect due to intentional disconnect`);
130137
}
131138
}
132139
}
@@ -235,7 +242,7 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ client }) => {
235242
};
236243

237244
const handleDisconnectInput = () => {
238-
midiService.disconnect();
245+
midiService.disconnectWithIntent('input');
239246
setConnectionState(midiService.connectionStatus);
240247
loadDevices(); // Refresh to update reconnectable devices
241248
};
@@ -251,7 +258,7 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ client }) => {
251258
};
252259

253260
const handleDisconnectOutput = () => {
254-
midiService.disconnect();
261+
midiService.disconnectWithIntent('output');
255262
setConnectionState(midiService.connectionStatus);
256263
loadDevices(); // Refresh to update reconnectable devices
257264
};

src/gmcp/Client/Midi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ export class GMCPClientMidi extends GMCPPackage {
321321
this.debugCallback = callback;
322322
}
323323

324+
324325
shutdown(): void {
325326
midiService.disconnect();
326327
this.activeNotes.forEach((timeout) => {

0 commit comments

Comments
 (0)