-
Notifications
You must be signed in to change notification settings - Fork 56
Stress tests #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Stress tests #205
Changes from all commits
dca568d
95afbb4
158d0fa
47b6cd1
1d45381
43a8e41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| using System; | ||
| using UnityEngine; | ||
|
|
||
| namespace LiveKit.PlayModeTests.Utils | ||
| { | ||
| /// <summary> | ||
| /// MonoBehaviour that identifies audio pulses by frequency using the Goertzel algorithm. | ||
| /// Attach to the same GameObject as an AudioSource after AudioStream has added its | ||
| /// AudioProbe, so this filter runs second and sees the filled audio data. | ||
| /// </summary> | ||
| public class AudioPulseDetector : MonoBehaviour | ||
| { | ||
| public int TotalPulses; | ||
| public double BaseFrequency; | ||
| public double FrequencyStep; | ||
| public double MagnitudeThreshold; | ||
|
|
||
| /// <summary> | ||
| /// Fired on the audio thread when a pulse is detected. | ||
| /// Parameters: (pulseIndex, magnitude). | ||
| /// </summary> | ||
| public event Action<int, double> PulseReceived; | ||
|
|
||
| private int _sampleRate; | ||
|
|
||
| void OnEnable() | ||
| { | ||
| _sampleRate = AudioSettings.outputSampleRate; | ||
| AudioSettings.OnAudioConfigurationChanged += OnAudioConfigChanged; | ||
| } | ||
|
|
||
| void OnDisable() | ||
| { | ||
| AudioSettings.OnAudioConfigurationChanged -= OnAudioConfigChanged; | ||
| } | ||
|
|
||
| void OnAudioConfigChanged(bool deviceWasChanged) | ||
| { | ||
| _sampleRate = AudioSettings.outputSampleRate; | ||
| } | ||
|
|
||
| void OnAudioFilterRead(float[] data, int channels) | ||
| { | ||
| int sampleRate = _sampleRate; | ||
| int samples = data.Length / channels; | ||
| if (samples == 0) return; | ||
|
|
||
| int bestPulse = -1; | ||
| double bestMag = 0; | ||
|
|
||
| for (int p = 0; p < TotalPulses; p++) | ||
| { | ||
| double freq = BaseFrequency + p * FrequencyStep; | ||
| double mag = Goertzel(data, channels, samples, sampleRate, freq); | ||
| if (mag > bestMag) | ||
| { | ||
| bestMag = mag; | ||
| bestPulse = p; | ||
| } | ||
| } | ||
|
|
||
| // Debug.Log($"bestPulse: {bestPulse} | bestMag: {bestMag}"); | ||
| if (bestPulse >= 0 && bestMag > MagnitudeThreshold) | ||
| PulseReceived?.Invoke(bestPulse, bestMag); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Goertzel algorithm — computes the magnitude of a single frequency bin. | ||
| /// O(N) per frequency, much cheaper than a full FFT. | ||
| /// </summary> | ||
| static double Goertzel(float[] data, int channels, int N, int sampleRate, double freq) | ||
| { | ||
| double k = 0.5 + (double)N * freq / sampleRate; | ||
| double w = 2.0 * Math.PI * k / N; | ||
| double coeff = 2.0 * Math.Cos(w); | ||
| double s0 = 0, s1 = 0, s2 = 0; | ||
|
|
||
| for (int i = 0; i < N; i++) | ||
| { | ||
| s0 = data[i * channels] + coeff * s1 - s2; | ||
| s2 = s1; | ||
| s1 = s0; | ||
| } | ||
|
|
||
| double power = s1 * s1 + s2 * s2 - coeff * s1 * s2; | ||
| return Math.Sqrt(Math.Abs(power)) / N; | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| using System; | ||
|
|
||
| namespace LiveKit.PlayModeTests.Utils | ||
| { | ||
| /// <summary> | ||
| /// A programmatic audio source for testing. Allows pushing audio frames | ||
| /// directly without requiring a Unity AudioSource or microphone. | ||
| /// </summary> | ||
| public class TestAudioSource : RtcAudioSource | ||
| { | ||
| public override event Action<float[], int, int> AudioRead; | ||
|
|
||
| public TestAudioSource(int channels = 1) | ||
| : base(channels, RtcAudioSourceType.AudioSourceCustom) { } | ||
|
|
||
| /// <summary> | ||
| /// Push an audio frame into the FFI capture pipeline. | ||
| /// Must call Start() first so the base class is subscribed to AudioRead. | ||
| /// </summary> | ||
| public void PushFrame(float[] data, int channels, int sampleRate) | ||
| { | ||
| AudioRead?.Invoke(data, channels, sampleRate); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| using System.Collections; | ||
| using UnityEngine; | ||
|
|
||
| namespace LiveKit.PlayModeTests.Utils | ||
| { | ||
| /// <summary> | ||
| /// Helper for running coroutines from non-MonoBehaviour test code. | ||
| /// Creates a temporary GameObject with a MonoBehaviour to host coroutines. | ||
| /// </summary> | ||
| public class TestCoroutineRunner : MonoBehaviour | ||
| { | ||
| private static TestCoroutineRunner _instance; | ||
|
|
||
| private static TestCoroutineRunner Instance | ||
| { | ||
| get | ||
| { | ||
| if (_instance == null) | ||
| { | ||
| var go = new GameObject("TestCoroutineRunner"); | ||
| _instance = go.AddComponent<TestCoroutineRunner>(); | ||
| } | ||
| return _instance; | ||
| } | ||
| } | ||
|
|
||
| public static Coroutine Start(IEnumerator routine) | ||
| { | ||
| return Instance.StartCoroutine(routine); | ||
| } | ||
|
|
||
| public static void Stop(Coroutine coroutine) | ||
| { | ||
| if (_instance != null && coroutine != null) | ||
| _instance.StopCoroutine(coroutine); | ||
| } | ||
|
|
||
| public static void Cleanup() | ||
| { | ||
| if (_instance != null) | ||
| { | ||
| Destroy(_instance.gameObject); | ||
| _instance = null; | ||
| } | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| using LiveKit.Proto; | ||
| using Unity.Collections; | ||
|
|
||
| namespace LiveKit.PlayModeTests.Utils | ||
| { | ||
| /// <summary> | ||
| /// A programmatic video source for testing. Encodes a pulse index as a | ||
| /// spatial binary pattern: the frame is divided into 4 vertical strips, | ||
| /// each either black or white, representing 4 bits of the index. | ||
| /// This survives WebRTC lossy compression because detection only needs | ||
| /// to distinguish black vs white in large uniform regions. | ||
| /// </summary> | ||
| public class TestVideoSource : RtcVideoSource | ||
| { | ||
| private readonly int _width; | ||
| private readonly int _height; | ||
| private int _pulseIndex = -1; | ||
|
|
||
| /// <summary> | ||
| /// Number of vertical strips used for binary encoding. | ||
| /// 4 strips = 4 bits = values 0–15. | ||
| /// </summary> | ||
| public const int NumStrips = 4; | ||
|
|
||
| public TestVideoSource(int width = 64, int height = 64) | ||
| : base(VideoStreamSource.Texture, VideoBufferType.Rgba) | ||
| { | ||
| _width = width; | ||
| _height = height; | ||
| base.Init(); | ||
| } | ||
|
|
||
| public override int GetWidth() => _width; | ||
| public override int GetHeight() => _height; | ||
| protected override VideoRotation GetVideoRotation() => VideoRotation._0; | ||
|
|
||
| /// <summary> | ||
| /// Set the pulse index to encode. -1 = all black (between pulses). | ||
| /// Index is encoded as 4-bit binary across vertical strips. | ||
| /// </summary> | ||
| public void SetPulseIndex(int index) | ||
| { | ||
| _pulseIndex = index; | ||
| } | ||
|
|
||
| protected override bool ReadBuffer() | ||
| { | ||
| int size = _width * _height * 4; | ||
| if (!_captureBuffer.IsCreated || _captureBuffer.Length != size) | ||
| { | ||
| if (_captureBuffer.IsCreated) _captureBuffer.Dispose(); | ||
| _captureBuffer = new NativeArray<byte>(size, Allocator.Persistent); | ||
| } | ||
|
|
||
| int stripWidth = _width / NumStrips; | ||
|
|
||
| for (int y = 0; y < _height; y++) | ||
| { | ||
| for (int x = 0; x < _width; x++) | ||
| { | ||
| int strip = x / stripWidth; | ||
| bool bright = _pulseIndex >= 0 && ((_pulseIndex >> strip) & 1) == 1; | ||
| byte value = bright ? (byte)255 : (byte)0; | ||
|
|
||
| int offset = (y * _width + x) * 4; | ||
| _captureBuffer[offset] = value; // R | ||
| _captureBuffer[offset + 1] = value; // G | ||
| _captureBuffer[offset + 2] = value; // B | ||
| _captureBuffer[offset + 3] = 255; // A | ||
| } | ||
| } | ||
|
|
||
| _requestPending = true; | ||
| return false; | ||
| } | ||
|
|
||
| ~TestVideoSource() | ||
| { | ||
| Dispose(false); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Todo: revert back to random room names.