Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 86 additions & 3 deletions src/MidiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
try {
this.jzz = await JZZ();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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?.();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -447,36 +505,42 @@ 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`);
}
}

// Public method to attempt auto-reconnection when callback is available
async attemptAutoReconnectInput(callback: MidiInputCallback): Promise<boolean> {
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;
Expand All @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -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);
}
Expand Down
19 changes: 13 additions & 6 deletions src/components/MidiStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,19 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ 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) {
Expand All @@ -101,18 +103,21 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ 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`);
}
}
}

// 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) {
Expand All @@ -127,6 +132,8 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ 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`);
}
}
}
Expand Down Expand Up @@ -235,7 +242,7 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ client }) => {
};

const handleDisconnectInput = () => {
midiService.disconnect();
midiService.disconnectWithIntent('input');
setConnectionState(midiService.connectionStatus);
loadDevices(); // Refresh to update reconnectable devices
};
Expand All @@ -251,7 +258,7 @@ const MidiStatus: React.FC<MidiStatusProps> = ({ client }) => {
};

const handleDisconnectOutput = () => {
midiService.disconnect();
midiService.disconnectWithIntent('output');
setConnectionState(midiService.connectionStatus);
loadDevices(); // Refresh to update reconnectable devices
};
Expand Down
1 change: 1 addition & 0 deletions src/gmcp/Client/Midi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export class GMCPClientMidi extends GMCPPackage {
this.debugCallback = callback;
}


shutdown(): void {
midiService.disconnect();
this.activeNotes.forEach((timeout) => {
Expand Down