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+ }
0 commit comments