Skip to content

Commit

Permalink
updating readmes
Browse files Browse the repository at this point in the history
  • Loading branch information
CrookedToe committed Jan 26, 2025
1 parent 49b5137 commit d0044ca
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 96 deletions.
202 changes: 118 additions & 84 deletions Modules/OSCAudioReaction/OSCAudioReactionModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ public class OSCAudioDirectionModule : Module
private float[]? audioBuffer;
private readonly object bufferLock = new();
private bool isRunning;
private float audioLevel;
private float audioPeak;
private float currentVolume; // Renamed from audioLevel for clarity
private float currentDirection; // Renamed from audioPeak for clarity
private int bytesPerSample;
private float leftRaw;
private float rightRaw;
private bool shouldUpdate = true;
private volatile bool shouldUpdate = true; // Added volatile for thread safety
private const float TARGET_LEVEL = 0.5f; // Target average volume level
private const float AGC_SPEED = 0.1f; // How fast AGC adjusts (0-1)
private float currentGain = 1.0f; // Current automatic gain value
Expand All @@ -37,6 +37,7 @@ public class OSCAudioDirectionModule : Module
private const int MAX_ERRORS = 3;
private DateTime lastErrorTime = DateTime.MinValue;
private const int ERROR_RESET_MS = 5000;
private readonly object recoveryLock = new(); // Added lock for recovery synchronization

// Smoothing
private float smoothedVolume = 0f;
Expand Down Expand Up @@ -86,84 +87,121 @@ protected override void OnPreLoad()

private void SetupAudioCapture()
{
try
lock (recoveryLock) // Prevent multiple simultaneous recovery attempts
{
if (deviceEnumerator == null)
try
{
deviceEnumerator = new MMDeviceEnumerator();
Debug.WriteLine("[AudioDirection] Created new MMDeviceEnumerator");
if (deviceEnumerator == null)
{
deviceEnumerator = new MMDeviceEnumerator();
Debug.WriteLine("[AudioDirection] Created new MMDeviceEnumerator");
}

// Get default output device
MMDevice? newDevice = null;
try
{
newDevice = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (newDevice == null)
{
Debug.WriteLine("[AudioDirection] ERROR: Failed to get default audio device");
return;
}

Debug.WriteLine($"[AudioDirection] Found default device: {newDevice.FriendlyName}");

// Check if we need to switch devices
if (selectedDevice != null && selectedDevice.FriendlyName == newDevice.FriendlyName &&
audioCapture?.CaptureState == CaptureState.Capturing)
{
newDevice.Dispose();
return;
}

// Clean up old capture
CleanupAudioCapture();

// Set up new capture
selectedDevice = newDevice;
audioCapture = new WasapiLoopbackCapture(selectedDevice);
audioCapture.DataAvailable += OnDataAvailable;
audioCapture.WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(48000, 2);
bytesPerSample = audioCapture.WaveFormat.BitsPerSample / audioCapture.WaveFormat.BlockAlign;

Debug.WriteLine($"[AudioDirection] Set up new audio capture: {audioCapture.WaveFormat}");

audioCapture.StartRecording();
isRunning = true;
errorCount = 0; // Reset error count on successful setup
}
catch (Exception)
{
newDevice?.Dispose(); // Ensure we dispose if setup fails
throw;
}
}

// Get default output device
var newDevice = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (newDevice == null)
{
Debug.WriteLine("[AudioDirection] ERROR: Failed to get default audio device");
return;
}

Debug.WriteLine($"[AudioDirection] Found default device: {newDevice.FriendlyName}");

// Check if we need to switch devices
if (selectedDevice != null && selectedDevice.FriendlyName == newDevice.FriendlyName &&
audioCapture?.CaptureState == CaptureState.Capturing)
catch (Exception ex)
{
newDevice.Dispose();
return;
Debug.WriteLine($"[AudioDirection] ERROR: Failed to set up audio capture: {ex.Message}");
isRunning = false;

// Handle error recovery
var now = DateTime.Now;
if ((now - lastErrorTime).TotalMilliseconds > ERROR_RESET_MS)
{
errorCount = 0; // Reset error count if enough time has passed
}

errorCount++;
lastErrorTime = now;

if (errorCount >= MAX_ERRORS)
{
Debug.WriteLine("[AudioDirection] ERROR: Too many errors, stopping audio capture");
return;
}

// Schedule recovery attempt with delay
Task.Delay(1000).ContinueWith(_ =>
{
if (errorCount < MAX_ERRORS) // Double-check error count before recovery
{
SetupAudioCapture();
}
});
}
}
}

// Clean up old capture
if (audioCapture != null)
private void CleanupAudioCapture()
{
if (audioCapture != null)
{
Debug.WriteLine("[AudioDirection] Cleaning up old audio capture");
try
{
Debug.WriteLine("[AudioDirection] Cleaning up old audio capture");
audioCapture.StopRecording();
audioCapture.Dispose();
audioCapture.DataAvailable -= OnDataAvailable;
audioCapture = null;
audioCapture.Dispose();
}

if (selectedDevice != null)
catch (Exception ex)
{
selectedDevice.Dispose();
selectedDevice = null;
Debug.WriteLine($"[AudioDirection] Warning: Error during cleanup: {ex.Message}");
}

// Set up new capture
selectedDevice = newDevice;
audioCapture = new WasapiLoopbackCapture(selectedDevice);
audioCapture.DataAvailable += OnDataAvailable;
audioCapture.WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(48000, 2);
bytesPerSample = audioCapture.WaveFormat.BitsPerSample / audioCapture.WaveFormat.BlockAlign;

Debug.WriteLine($"[AudioDirection] Set up new audio capture: {audioCapture.WaveFormat}");

audioCapture.StartRecording();
isRunning = true;
errorCount = 0; // Reset error count on successful setup
audioCapture = null;
}
catch (Exception ex)
{
Debug.WriteLine($"[AudioDirection] ERROR: Failed to set up audio capture: {ex.Message}");
isRunning = false;

// Handle error recovery
var now = DateTime.Now;
if ((now - lastErrorTime).TotalMilliseconds > ERROR_RESET_MS)
if (selectedDevice != null)
{
try
{
errorCount = 0; // Reset error count if enough time has passed
selectedDevice.Dispose();
}

errorCount++;
lastErrorTime = now;

if (errorCount >= MAX_ERRORS)
catch (Exception ex)
{
Debug.WriteLine("[AudioDirection] ERROR: Too many errors, stopping audio capture");
return;
Debug.WriteLine($"[AudioDirection] Warning: Error disposing device: {ex.Message}");
}

// Try to recover by forcing a new setup after a delay
Task.Delay(1000).ContinueWith(_ => SetupAudioCapture());
selectedDevice = null;
}
}

Expand All @@ -172,10 +210,10 @@ protected override void OnRegisteredParameterReceived(RegisteredParameter parame
switch (parameter.Lookup)
{
case AudioParameter.AudioVolume:
audioLevel = parameter.GetValue<float>();
currentVolume = parameter.GetValue<float>();
break;
case AudioParameter.AudioDirection:
audioPeak = parameter.GetValue<float>();
currentDirection = parameter.GetValue<float>();
break;
}
}
Expand Down Expand Up @@ -273,20 +311,20 @@ private void OnDataAvailable(object? sender, WaveInEventArgs e)
var now = DateTime.Now;

// Send volume parameter if changed and rate limit passed
if (Math.Abs(smoothedVolume - audioLevel) > 0.001f &&
if (Math.Abs(smoothedVolume - currentVolume) > 0.001f &&
(now - lastVolumeUpdate).TotalMilliseconds >= MIN_UPDATE_MS)
{
audioLevel = smoothedVolume;
SendParameter(AudioParameter.AudioVolume, audioLevel);
currentVolume = smoothedVolume;
SendParameter(AudioParameter.AudioVolume, currentVolume);
lastVolumeUpdate = now;
}

// Send direction parameter if changed and rate limit passed
if (Math.Abs(smoothedDirection - audioPeak) > 0.001f &&
if (Math.Abs(smoothedDirection - currentDirection) > 0.001f &&
(now - lastDirectionUpdate).TotalMilliseconds >= MIN_UPDATE_MS)
{
audioPeak = smoothedDirection;
SendParameter(AudioParameter.AudioDirection, audioPeak);
currentDirection = smoothedDirection;
SendParameter(AudioParameter.AudioDirection, currentDirection);
lastDirectionUpdate = now;
}
}
Expand All @@ -303,22 +341,18 @@ protected override async Task OnModuleStop()
Debug.WriteLine("[AudioDirection] Stopping audio direction module");
isRunning = false;

if (audioCapture != null)
{
audioCapture.StopRecording();
audioCapture.Dispose();
audioCapture = null;
}

if (selectedDevice != null)
{
selectedDevice.Dispose();
selectedDevice = null;
}
CleanupAudioCapture();

if (deviceEnumerator != null)
{
deviceEnumerator.Dispose();
try
{
deviceEnumerator.Dispose();
}
catch (Exception ex)
{
Debug.WriteLine($"[AudioDirection] Warning: Error disposing enumerator: {ex.Message}");
}
deviceEnumerator = null;
}
}
Expand Down
20 changes: 16 additions & 4 deletions Modules/OSCAudioReaction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,19 @@ Two float parameters are available:

## Troubleshooting

- If no audio is detected, check that your default output device is working
- If direction seems incorrect, try adjusting the Direction Threshold
- If volume is too low/high, adjust Manual Gain or enable Auto Gain
- If updates are too jittery, increase the Smoothing value
- If no audio is detected:
- Check that your default output device is working
- Check the debug logs for any error messages
- The module will automatically attempt to recover
- If direction seems incorrect:
- Try adjusting the Direction Threshold
- Make sure you have stereo audio playing
- If volume is too low/high:
- Enable Auto Gain for automatic adjustment
- Or adjust Manual Gain if you prefer fixed gain
- If updates are too jittery:
- Increase the Smoothing value
- Default smoothing uses a 3-sample history
- If the module stops working:
- It will automatically attempt to recover up to 3 times
- Check the debug logs for error messages
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,29 @@ A collection of modules for [VRCOSC](https://github.com/VolcanicArts/VRCOSC).
### OSCLeash
A module for controlling leash physics in VRChat using OSC parameters. For more information, see the [OSCLeash README](CrookedToesModules/OSCLeash/README.md).

### OSCAudioReaction
Captures your system's audio output and sends stereo direction and volume information to VRChat parameters for audio visualization. Based on [VRC-OSC-Audio-Reaction](https://github.com/Codel1417/VRC-OSC-Audio-Reaction) by Codel1417.

## Installation

1. Download the latest release from the releases page
2. Extract the DLL files to your VRCOSC packages folder (`%appdata%/VRCOSC/packages/local/`)
3. Restart VRCOSC if it's running
2. Place the module DLLs in your VRCOSC modules folder (typically `%AppData%/VRCOSC/Packages/local`)
3. Enable the desired modules in VRCOSC
4. Configure each module's settings as needed

## Development

This repository uses .NET 8.0 and targets Windows 10.0.22621.0. To build the modules:
These modules are developed for VRCOSC using C# and .NET 8.0. Each module is contained in its own directory under `Modules/` with its own README detailing specific functionality and setup.

## Contributing

1. Clone the repository
2. Open the solution in Visual Studio or your preferred IDE
3. Build the solution
4. The built DLLs will be automatically copied to your VRCOSC packages folder
Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Credits

- [VRCOSC](https://github.com/VolcanicArts/VRCOSC) by VolcanicArts
- [VRC-OSC-Audio-Reaction](https://github.com/Codel1417/VRC-OSC-Audio-Reaction) by Codel1417

0 comments on commit d0044ca

Please sign in to comment.