diff --git a/src/MidiService.ts b/src/MidiService.ts index 69fcb60..d2d0633 100644 --- a/src/MidiService.ts +++ b/src/MidiService.ts @@ -68,6 +68,12 @@ class MidiService { outputConnected: false }; + // Flags to track intentional disconnections (prevent auto-reconnect until next server connection) + private intentionalDisconnectFlags = { + input: false, + output: false + }; + async initialize(): Promise { try { this.jzz = await JZZ(); @@ -222,6 +228,9 @@ class MidiService { } }); + // Clear intentional disconnect flag since user manually connected + this.intentionalDisconnectFlags.input = false; + console.log(`Connected to MIDI input: ${this.connectionState.inputDeviceName}`); return true; } catch (error) { @@ -254,6 +263,9 @@ class MidiService { lastOutputDeviceId: deviceId } }); + + // Clear intentional disconnect flag since user manually connected + this.intentionalDisconnectFlags.output = false; console.log(`Connected to virtual MIDI synthesizer: ${virtualMidiService.getPortName()}`); return true; @@ -284,6 +296,9 @@ class MidiService { } }); + // Clear intentional disconnect flag since user manually connected + this.intentionalDisconnectFlags.output = false; + console.log(`Connected to MIDI output: ${this.connectionState.outputDeviceName}`); return true; } catch (error) { @@ -319,6 +334,44 @@ class MidiService { } } + // Disconnect specific device type + disconnectDevice(deviceType: 'input' | 'output'): void { + if (deviceType === 'input') { + if (this.inputDevice) { + this.inputDevice.close?.(); + this.inputDevice = null; + } + this.inputCallback = null; + this.connectionState.inputConnected = false; + this.connectionState.inputDeviceId = undefined; + this.connectionState.inputDeviceName = undefined; + console.log("Disconnected input device"); + } else if (deviceType === 'output') { + if (this.outputDevice) { + this.outputDevice.close?.(); + this.outputDevice = null; + } + this.connectionState.outputConnected = false; + this.connectionState.outputDeviceId = undefined; + this.connectionState.outputDeviceName = undefined; + console.log("Disconnected output device"); + } + } + + // Disconnect with intentional flag setting + disconnectWithIntent(deviceType: 'input' | 'output' | 'both'): void { + this.setIntentionalDisconnect(deviceType); + + if (deviceType === 'input') { + this.disconnectDevice('input'); + } else if (deviceType === 'output') { + this.disconnectDevice('output'); + } else { + // 'both' + this.disconnect(); + } + } + disconnect(): void { if (this.inputDevice) { this.inputDevice.close?.(); @@ -359,6 +412,11 @@ class MidiService { return { ...this.connectionState }; } + // Get intentional disconnect flags + get intentionalDisconnectStatus() { + return { ...this.intentionalDisconnectFlags }; + } + // Device change monitoring onDeviceChange(callback: DeviceChangeCallback): () => void { this.deviceChangeCallbacks.add(callback); @@ -447,22 +505,26 @@ class MidiService { // For input devices, we need a callback, so we'll defer this until someone actually tries to connect // Just log what we would try to reconnect to - if (preferences.lastInputDeviceId) { + if (preferences.lastInputDeviceId && !this.intentionalDisconnectFlags.input) { const inputDevices = this.getInputDevices(); const lastInputDevice = inputDevices.find(d => d.id === preferences.lastInputDeviceId); if (lastInputDevice) { console.log(`Input device available for auto-reconnect: ${lastInputDevice.name}`); } + } else if (preferences.lastInputDeviceId && this.intentionalDisconnectFlags.input) { + console.log(`Skipping input auto-reconnect due to intentional disconnect`); } // For output devices, we can attempt reconnection immediately - if (preferences.lastOutputDeviceId) { + if (preferences.lastOutputDeviceId && !this.intentionalDisconnectFlags.output) { const outputDevices = this.getOutputDevices(); const lastOutputDevice = outputDevices.find(d => d.id === preferences.lastOutputDeviceId); if (lastOutputDevice) { console.log(`Attempting to auto-reconnect to output device: ${lastOutputDevice.name}`); await this.connectOutputDevice(preferences.lastOutputDeviceId); } + } else if (preferences.lastOutputDeviceId && this.intentionalDisconnectFlags.output) { + console.log(`Skipping output auto-reconnect due to intentional disconnect`); } } @@ -470,13 +532,15 @@ class MidiService { async attemptAutoReconnectInput(callback: MidiInputCallback): Promise { const preferences = preferencesStore.getState().midi; - if (preferences.lastInputDeviceId) { + if (preferences.lastInputDeviceId && !this.intentionalDisconnectFlags.input) { const inputDevices = this.getInputDevices(); const lastInputDevice = inputDevices.find(d => d.id === preferences.lastInputDeviceId); if (lastInputDevice) { console.log(`Attempting to auto-reconnect to input device: ${lastInputDevice.name}`); return await this.connectInputDevice(preferences.lastInputDeviceId, callback); } + } else if (preferences.lastInputDeviceId && this.intentionalDisconnectFlags.input) { + console.log(`Skipping input auto-reconnect due to intentional disconnect`); } return false; @@ -498,6 +562,25 @@ class MidiService { return false; } + // Set intentional disconnect flag for a device type + setIntentionalDisconnect(deviceType: 'input' | 'output' | 'both'): void { + if (deviceType === 'input' || deviceType === 'both') { + this.intentionalDisconnectFlags.input = true; + } + if (deviceType === 'output' || deviceType === 'both') { + this.intentionalDisconnectFlags.output = true; + } + console.log(`Set intentional disconnect flag for: ${deviceType}`); + } + + // Reset all intentional disconnect flags (called when server reconnects) + resetIntentionalDisconnectFlags(): void { + this.intentionalDisconnectFlags.input = false; + this.intentionalDisconnectFlags.output = false; + console.log("Reset intentional disconnect flags"); + } + + // Shutdown and cleanup shutdown(): void { this.disconnect(); diff --git a/src/client.ts b/src/client.ts index cab9d37..3075b78 100644 --- a/src/client.ts +++ b/src/client.ts @@ -32,6 +32,7 @@ import { AutoreadMode, preferencesStore } from "./PreferencesStore"; import { WebRTCService } from "./WebRTCService"; import FileTransferManager from "./FileTransferManager.js"; import { GMCPMessageRoomInfo, RoomPlayer } from "./gmcp/Room"; // Import RoomPlayer +import { midiService } from "./MidiService"; export interface WorldData { liveKitTokens: string[]; @@ -261,6 +262,10 @@ class MudClient extends EventEmitter { this.telnet = new TelnetParser(new WebSocketStream(this.ws)); this.ws.onopen = () => { this._connected = true; + + // Reset MIDI intentional disconnect flags when successfully reconnecting to server + midiService.resetIntentionalDisconnectFlags(); + this.emit("connect"); this.emit("connectionChange", true); }; @@ -328,6 +333,12 @@ class MudClient extends EventEmitter { this.currentRoomInfo = null; // Reset room info on cleanup this.webRTCService.cleanup(); this.fileTransferManager.cleanup(); + + // Reset intentional disconnect flag after handling disconnect + if (this.intentionalDisconnect) { + this.intentionalDisconnect = false; + } + this.emit("disconnect"); this.emit("connectionChange", false); } diff --git a/src/components/MidiStatus.tsx b/src/components/MidiStatus.tsx index d7bd6fc..dcafc32 100644 --- a/src/components/MidiStatus.tsx +++ b/src/components/MidiStatus.tsx @@ -76,17 +76,19 @@ const MidiStatus: React.FC = ({ client }) => { // Update reconnectable device suggestions and attempt auto-reconnection const updateReconnectableDevices = async () => { const prefs = preferencesStore.getState().midi; + const intentionalFlags = midiService.intentionalDisconnectStatus; const newReconnectables: { input?: { id: string, name: string }; output?: { id: string, name: string } } = {}; // Check if last input device is available but not connected if (prefs.lastInputDeviceId && !connectionState.inputConnected) { const device = inputDevices.find(d => d.id === prefs.lastInputDeviceId); + if (device && midiService.canReconnectToDevice(prefs.lastInputDeviceId, 'input')) { newReconnectables.input = device; - // Auto-reconnect input device if midiPackage is available - if (midiPackage) { + // Only auto-reconnect if not intentionally disconnected + if (midiPackage && !intentionalFlags.input) { try { const success = await midiPackage.connectInputDevice(prefs.lastInputDeviceId); if (success) { @@ -101,6 +103,8 @@ const MidiStatus: React.FC = ({ client }) => { } catch (error) { console.error('Error during input auto-reconnection:', error); } + } else if (intentionalFlags.input) { + console.log(`Skipping input auto-reconnect due to intentional disconnect`); } } } @@ -108,11 +112,12 @@ const MidiStatus: React.FC = ({ client }) => { // Check if last output device is available but not connected if (prefs.lastOutputDeviceId && !connectionState.outputConnected) { const device = outputDevices.find(d => d.id === prefs.lastOutputDeviceId); + if (device && midiService.canReconnectToDevice(prefs.lastOutputDeviceId, 'output')) { newReconnectables.output = device; - // Auto-reconnect output device if midiPackage is available - if (midiPackage) { + // Only auto-reconnect if not intentionally disconnected + if (midiPackage && !intentionalFlags.output) { try { const success = await midiPackage.connectOutputDevice(prefs.lastOutputDeviceId); if (success) { @@ -127,6 +132,8 @@ const MidiStatus: React.FC = ({ client }) => { } catch (error) { console.error('Error during output auto-reconnection:', error); } + } else if (intentionalFlags.output) { + console.log(`Skipping output auto-reconnect due to intentional disconnect`); } } } @@ -235,7 +242,7 @@ const MidiStatus: React.FC = ({ client }) => { }; const handleDisconnectInput = () => { - midiService.disconnect(); + midiService.disconnectWithIntent('input'); setConnectionState(midiService.connectionStatus); loadDevices(); // Refresh to update reconnectable devices }; @@ -251,7 +258,7 @@ const MidiStatus: React.FC = ({ client }) => { }; const handleDisconnectOutput = () => { - midiService.disconnect(); + midiService.disconnectWithIntent('output'); setConnectionState(midiService.connectionStatus); loadDevices(); // Refresh to update reconnectable devices }; diff --git a/src/gmcp/Client/Midi.ts b/src/gmcp/Client/Midi.ts index 3f7b769..fcace22 100644 --- a/src/gmcp/Client/Midi.ts +++ b/src/gmcp/Client/Midi.ts @@ -321,6 +321,7 @@ export class GMCPClientMidi extends GMCPPackage { this.debugCallback = callback; } + shutdown(): void { midiService.disconnect(); this.activeNotes.forEach((timeout) => {