Skip to content

Commit 722ebac

Browse files
committed
fix slient crash probablty, memory leak, neww sources
1 parent 7729ed9 commit 722ebac

4 files changed

Lines changed: 487 additions & 7 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// -----------------------------------------------------------------------
2+
// <copyright file="MixerSource.cs" company="ExMod Team">
3+
// Copyright (c) ExMod Team. All rights reserved.
4+
// Licensed under the CC BY-SA 3.0 license.
5+
// </copyright>
6+
// -----------------------------------------------------------------------
7+
8+
namespace Exiled.API.Features.Audio.PcmSources
9+
{
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Linq;
13+
14+
using Exiled.API.Interfaces.Audio;
15+
using Exiled.API.Structs.Audio;
16+
17+
using UnityEngine;
18+
19+
/// <summary>
20+
/// Provides an <see cref="IPcmSource"/> that dynamically mixes multiple audio sources together in real-time.
21+
/// <para>
22+
/// This allows playing overlapping sounds (e.g., background music + voice announcements) simultaneously
23+
/// through a single speaker without needing multiple Voice Controller IDs.
24+
/// </para>
25+
/// </summary>
26+
public sealed class MixerSource : IPcmSource
27+
{
28+
private readonly List<IPcmSource> sources = new();
29+
private float[] tempBuffer;
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="MixerSource"/> class with the specified initial sources.
33+
/// </summary>
34+
/// <param name="initialSources">An array of <see cref="IPcmSource"/> instances to mix.</param>
35+
public MixerSource(params IPcmSource[] initialSources)
36+
{
37+
if (initialSources != null)
38+
sources.AddRange(initialSources.Where(s => s != null));
39+
40+
TrackInfo = new TrackData { Path = "Audio Mixer", Duration = 0 };
41+
}
42+
43+
/// <summary>
44+
/// Gets or sets a value indicating whether the mixer should stay alive and output silence even when all internal sources have finished playing.
45+
/// </summary>
46+
public bool KeepAlive { get; set; } = false;
47+
48+
/// <summary>
49+
/// Gets the metadata of the mixer track.
50+
/// </summary>
51+
public TrackData TrackInfo { get; }
52+
53+
/// <summary>
54+
/// Gets the maximum total duration of all active sources in the mixer, in seconds.
55+
/// </summary>
56+
public double TotalDuration => sources.Count > 0 ? sources.Max(x => x.TotalDuration) : 0.0;
57+
58+
/// <summary>
59+
/// Gets or sets the current playback position in seconds across all active sources.
60+
/// </summary>
61+
public double CurrentTime
62+
{
63+
get => sources.Count > 0 ? sources.Max(x => x.CurrentTime) : 0.0;
64+
set => Seek(value);
65+
}
66+
67+
/// <summary>
68+
/// Gets a value indicating whether all internal sources have ended and <see cref="KeepAlive"/> is set to false.
69+
/// </summary>
70+
public bool Ended => !KeepAlive && (sources.Count == 0 || sources.All(x => x.Ended));
71+
72+
/// <summary>
73+
/// Reads a sequence of mixed PCM samples from all active sources into the specified buffer.
74+
/// </summary>
75+
/// <param name="buffer">The destination buffer to fill with mixed PCM data.</param>
76+
/// <param name="offset">The zero-based index in <paramref name="buffer"/> at which to begin writing.</param>
77+
/// <param name="count">The maximum number of samples to read and mix.</param>
78+
/// <returns>The number of samples written to the <paramref name="buffer"/>.</returns>
79+
public int Read(float[] buffer, int offset, int count)
80+
{
81+
if (tempBuffer == null || tempBuffer.Length < count)
82+
tempBuffer = new float[count];
83+
84+
Array.Clear(buffer, offset, count);
85+
int maxRead = 0;
86+
87+
for (int i = sources.Count - 1; i >= 0; i--)
88+
{
89+
IPcmSource src = sources[i];
90+
91+
if (src.Ended)
92+
{
93+
src.Dispose();
94+
sources.RemoveAt(i);
95+
continue;
96+
}
97+
98+
int read = src.Read(tempBuffer, 0, count);
99+
if (read > maxRead)
100+
maxRead = read;
101+
102+
for (int j = 0; j < read; j++)
103+
buffer[offset + j] += tempBuffer[j];
104+
}
105+
106+
for (int i = 0; i < maxRead; i++)
107+
buffer[offset + i] = Mathf.Clamp(buffer[offset + i], -1f, 1f);
108+
109+
return KeepAlive ? count : maxRead;
110+
}
111+
112+
/// <summary>
113+
/// Seeks to the specified position in seconds for all active sources in the mixer.
114+
/// </summary>
115+
/// <param name="seconds">The target position in seconds.</param>
116+
public void Seek(double seconds)
117+
{
118+
foreach (IPcmSource pcmSource in sources)
119+
pcmSource.Seek(seconds);
120+
}
121+
122+
/// <summary>
123+
/// Resets the playback position to the start for all active sources in the mixer.
124+
/// </summary>
125+
public void Reset()
126+
{
127+
foreach (IPcmSource pcmSource in sources)
128+
pcmSource.Reset();
129+
}
130+
131+
/// <summary>
132+
/// Releases all resources used by the <see cref="MixerSource"/> and automatically disposes of all internal sources.
133+
/// </summary>
134+
public void Dispose()
135+
{
136+
foreach (IPcmSource pcmSource in sources)
137+
pcmSource?.Dispose();
138+
139+
sources.Clear();
140+
}
141+
142+
/// <summary>
143+
/// Dynamically adds a new <see cref="IPcmSource"/> to the mixer while it is playing.
144+
/// </summary>
145+
/// <param name="source">The audio source to add.</param>
146+
public void AddSource(IPcmSource source)
147+
{
148+
if (source != null)
149+
sources.Add(source);
150+
}
151+
152+
/// <summary>
153+
/// Dynamically removes an existing <see cref="IPcmSource"/> from the mixer.
154+
/// </summary>
155+
/// <param name="source">The audio source to remove.</param>
156+
/// <param name="dispose">If <c>true</c>, automatically calls Dispose on the removed source.</param>
157+
public void RemoveSource(IPcmSource source, bool dispose = true)
158+
{
159+
if (source == null)
160+
return;
161+
162+
if (dispose)
163+
source.Dispose();
164+
165+
sources.Remove(source);
166+
}
167+
}
168+
}

EXILED/Exiled.API/Features/Audio/PcmSources/PreloadWebWavPcmSource.cs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ namespace Exiled.API.Features.Audio.PcmSources
2424
public sealed class PreloadWebWavPcmSource : IPcmSource
2525
{
2626
private IPcmSource internalSource;
27+
private UnityWebRequest webRequest;
28+
private CoroutineHandle downloadRoutine;
2729

2830
private bool isReady = false;
2931
private bool isFailed = false;
@@ -35,7 +37,7 @@ public sealed class PreloadWebWavPcmSource : IPcmSource
3537
public PreloadWebWavPcmSource(string url)
3638
{
3739
TrackInfo = default;
38-
Timing.RunCoroutine(Download(url));
40+
downloadRoutine = Timing.RunCoroutine(Download(url));
3941
}
4042

4143
/// <summary>
@@ -105,23 +107,43 @@ public void Reset()
105107
/// <summary>
106108
/// Releases all resources used by the <see cref="PreloadWebWavPcmSource"/>.
107109
/// </summary>
108-
public void Dispose() => internalSource?.Dispose();
110+
public void Dispose()
111+
{
112+
if (downloadRoutine.IsRunning)
113+
downloadRoutine.IsRunning = false;
114+
115+
webRequest?.Abort();
116+
webRequest?.Dispose();
117+
internalSource?.Dispose();
118+
}
109119

110120
private IEnumerator<float> Download(string url)
111121
{
112-
using UnityWebRequest www = UnityWebRequest.Get(url);
113-
yield return Timing.WaitUntilDone(www.SendWebRequest());
122+
webRequest = null;
114123

115-
if (www.result != UnityWebRequest.Result.Success)
124+
try
116125
{
117-
Log.Error($"[WebPreloadWavPcmSource] Failed to download audio! URL: {url} | Error: {www.error}");
126+
webRequest = UnityWebRequest.Get(url);
127+
}
128+
catch (Exception ex)
129+
{
130+
Log.Error($"[WebPreloadWavPcmSource] Failed to download audio! URL: {url} | Error: {ex.Message}");
118131
isFailed = true;
119132
yield break;
120133
}
121134

135+
yield return Timing.WaitUntilDone(webRequest.SendWebRequest());
136+
122137
try
123138
{
124-
byte[] rawBytes = www.downloadHandler.data;
139+
if (webRequest.result != UnityWebRequest.Result.Success)
140+
{
141+
Log.Error($"[WebPreloadWavPcmSource] Failed to download audio! URL: {url} | Error: {webRequest.error}");
142+
isFailed = true;
143+
yield break;
144+
}
145+
146+
byte[] rawBytes = webRequest.downloadHandler.data;
125147
AudioData audioData = WavUtility.WavToPcm(rawBytes);
126148
audioData.TrackInfo.Path = url;
127149

@@ -134,6 +156,11 @@ private IEnumerator<float> Download(string url)
134156
Log.Error($"[WebPreloadWavPcmSource] Failed to read the downloaded file! Ensure the link points to a valid .WAV file.\nException Details: {e.Message}");
135157
isFailed = true;
136158
}
159+
finally
160+
{
161+
webRequest?.Dispose();
162+
webRequest = null;
163+
}
137164
}
138165
}
139166
}

0 commit comments

Comments
 (0)