Ryle Radio 1.0.0
An open-source "radio" system for Unity, allowing tracks, tuning, broadcasters, and more!
Loading...
Searching...
No Matches
RadioTrackPlayer.cs
2using System;
3using UnityEngine;
4
5namespace RyleRadio.Tracks
6{
7
8 /// <summary>
9 /// A class that plays a certain \ref RadioTrack at runtime. It's created newly for each track on each RadioOutput, and manages the playback entirely.
10 ///
11 /// As such, this script is a central point for information about the playback process.
12 /// </summary>
13 public class RadioTrackPlayer
14 {
15 /// <summary>
16 /// The different types of Player- mainly changes what it does when the end of the track is reached
17 /// </summary>
18 public enum PlayerType
19 {
20 Loop, ///< When it ends, the player goes back to the start and plays the track again
21 OneShot, ///< When it ends, the player destroys itself and stops playing
22 }
23
24 /// <summary>
25 /// The track that this player is associated with and plays during runtime
26 /// </summary>
27 public RadioTrackWrapper TrackW { get; private set; }
28
29 /// <summary>
30 /// How many samples through the track this player is- not a whole number as we increment it with different values depending on the track's sample rate
31 /// </summary>
32 /// <remarks>
33 /// This is stored as a double for greater precision with sample rates- using a float here causes clipping or distortion.
34 /// <br>We could use a `decimal` here, but we're opting to change sample rates of the tracks rather than messing with them here
35 /// </remarks>
36 public double Progress { get; private set; } = 0;
37
38 /// <summary>
39 /// How far through the track this player is, from [0 - 1]
40 /// </summary>
41 public float ProgressFraction
42 {
43 get
44 {
45 // if progress is < 0, the player has finished i.e it's a one-shot
46 if (Progress < 0)
47 return 1;
48
49 float maxSamples = TrackW.SampleCount - 1; // get the total samples
50 return Mathf.Clamp01((float)Progress / maxSamples); // get the progress at those samples
51 }
52 }
53
54 /// <summary>
55 /// The type of player this is- what happens when the track ends.
56 /// </summary>
57 public PlayerType PlayType { get; private set; }
58
59 /// <summary>
60 /// Event called in order to destroy this player- this can either be invoked directly or as part of the \ref Destroy() method.
61 ///
62 /// We're using an event here so that other scripts can add their own functions to be called when this player is destroyed- e.g: removing one-shot players from a \ref RadioOutput
63 /// </summary>
64 public Action<RadioTrackPlayer> DoDestroy { get; set; } = new(_ => { });
65
66 /// Event called when the player starts playing
67 public Action<RadioTrackPlayer> OnPlay { get; set; } = new(_ => { });
68 /// Event called when the player is stopped through \ref Stop()
69 public Action<RadioTrackPlayer> OnStop { get; set; } = new(_ => { });
70 /// Event called when the player's pause state is changed- the bool is true if the player is being paused, false if unpaused
71 public Action<RadioTrackPlayer, bool> OnPause { get; set; } = new((_, _) => { });
72 /// Event called when this player retreieves a sample from its track
73 public Action<RadioTrackPlayer> OnSample { get; set; } = new(_ => { }); // when a sample is retrieved from this track
74
75#if !SKIP_IN_DOXYGEN
76 // internal event for OnEnd
77 private Action<RadioTrackPlayer> onEnd = _ => { };
78#endif
79 /// Event called when the player reaches the end of its track naturally, before it takes an action depending on the \ref PlayType (e.g: looping). This is not invoked when the player is stopped or reset.
80 public Action<RadioTrackPlayer> OnEnd
81 {
82 get => onEnd; // we need an alias here as we're adressing the delegate with a ref in this class' constructor- we can't use ref on a property
83 set => onEnd = value;
84 }
85
86 /// Event called when the volume of this player is captured for a sample. Volume is the product of Tune power, Broadcast power, and Insulation
87 public Action<RadioTrackPlayer, float> OnVolume { get; set; } = new((_, _) => { });
88 /// Event called when the gain for this player is captured for a sample. Gain is a direct change to the loudness of a track.
89 public Action<RadioTrackPlayer, float> OnGain { get; set; } = new((_, _) => { });
90 /// Event called when the tune power for this player is captured for a sample. Tune power is the loudness of a track based on the Tune value of the \ref RadioOutput
91 public Action<RadioTrackPlayer, float> OnTunePower { get; set; } = new((_, _) => { });
92 /// Event called when the broadcast power for this player is captured for a sample. Broadcast power is the loudness of a track based on the position of the \ref RadioOutput relative to any \ref RadioBroadcaster
93 public Action<RadioTrackPlayer, float> OnBroadcastPower { get; set; } = new((_, _) => { });
94 /// Event called when the insulation for this player is captured for a sample. Insulation is the quietness of a track based on the position of the \ref RadioOutput relative to any \ref RadioInsulator
95 public Action<RadioTrackPlayer, float> OnInsulation { get; set; } = new((_, _) => { });
96
97#if !SKIP_IN_DOXYGEN
98 // internal value for Paused
99 private bool paused = false;
100#endif
101 /// <summary>
102 /// Whether or not this player has been paused, temporarily halting playback of the track. Changing this value pauses/unpauses the player
103 /// </summary>
104 public bool Paused
105 {
106 get => paused;
107 set
108 {
109 paused = value;
110 OnPause(this, value); // call the delegate
111 }
112 }
113
114 /// <summary>
115 /// The amount that \ref Progress is increased by every sample- the ratio of the track's sample speed to the \ref baseSampleRate
116 /// </summary>
117 private float sampleIncrement;
118
119 /// <summary>
120 /// The sample rate of the \ref RadioOutput that this player is used by- that is, the sample rate of the radio
121 /// </summary>
122 private float baseSampleRate;
123
124 /// <summary>
125 /// Whether or not this player has been stopped- prevents it from being stopped multiple times
126 /// </summary>
127 private bool isStopped = false;
128
129
130 /// <summary>
131 /// Creates a new player for the provided track
132 /// </summary>
133 /// <param name="_trackW">The track for this player to play</param>
134 /// <param name="_playerType">The type of player this is (what happens when the track ends)</param>
135 /// <param name="_baseSampleRate">The sample rate of the RadioOutput using this Player</param>
136 public RadioTrackPlayer(RadioTrackWrapper _trackW, PlayerType _playerType, float _baseSampleRate)
137 {
138 TrackW = _trackW;
139 Progress = 0;
140
141 PlayType = _playerType;
142
143 // assign the sample rate of the Oistener
144 baseSampleRate = _baseSampleRate;
146
147 // add any applicable track methods to the end delegate
148 TrackW.AddToPlayerEndCallback(ref onEnd);
149 }
150
151 /// <summary>
152 /// Updates the \ref sampleIncrement variable to match the current track
153 /// </summary>
155 {
156 // we need to have a float variable for sampleIncrement as the sample rate of the track and the sample rate of the output may be different
157 // if they're different, it means that incrememnting Progress by 1 will make this track sound faster or slower depending on its SampleRate
158 // in order to counteract this, we set up a specific increment as the ratio between these two sample rates
159 //
160 // let's say that the Output's sampleRate is 44100, but the track's sample rate is 48000
161 // if we incremented Progress by 1 every sample, then, the track would sound slightly slower than usual as we're using the wrong SampleRate
162 // if we use this method though, we get an increment of ~0.92
163 // if we increment it by 0.92 every sample, then, the track sounds to be playing at the correct speed!
164 //
165 // this can get a bit confusing when we use Progress to get the sample INDEX, though, as we can't get an index with a float
166 // the solution here is just to convert it to an int, as seen in GetSample below
167 // this means that instead of skipping samples, it simply repeats the same one until the increment reaches the next sample
168 // the samples are happening so quickly that this repeat is completely unnoticeable- as far as i can tell, this is how all audio
169 // software and programs operate. as such, we use it here :)
170 //
171 // note: this explanation was mostly for my own future reference if i forget how this works lol
172 sampleIncrement = TrackW.SampleRate / baseSampleRate;
173
174 //if (TrackW.id == "music_old2")
175 // Debug.Log(TrackW.SampleCount);
176 }
177
178 /// <summary>
179 /// Gets the current sample from the track according to playback. <i>I would recommend reading the code comments for this method as they explain how the entire sample playback and evaluation process works</i>
180 /// </summary>
181 /// <param name="_tune">The tune value of the Output</param>
182 /// <param name="_receiverPosition">The position of the Output</param>
183 /// <param name="_otherVolume">The sum of the samples of previous tracks, according to the order in \ref RadioData.<br><b>See: </b>\ref RadioTrack.attenuation</param>
184 /// <param name="_outVolume">The volume of this sample to be added to `_otherVolume`</param>
185 /// <param name="_applyVolume">Whether or not Volume (`tune power * broadcast power * insulation`) should be applied</param>
186 /// <returns>The current sample</returns>
187 public float GetSample(float _tune, Vector3 _receiverPosition, float _otherVolume, out float _outVolume, bool _applyVolume = true)
188 {
189 // if this track is paused, return silence
190 if (isStopped || Paused)
191 {
192 _outVolume = 0f;
193 return 0;
194 }
195
196 // the output volume of this track right now
197 float volume = 0;
198 float gain = 100;
199
200 if (_applyVolume)
201 {
202 // long explanation of each method is in here so this can be kind of a core script for documentation
203
204 // get the gain of the track- this is a variable assigned in the inspector that serves as a basic increase to the
205 // loudness of the track without affecting attenuation or other audio values
206 // if the gain is at 100, it will be at the default loudness
207 // if the gain is at 200, it will be double the loudness
208 // if the gain is at 50, it will be half the loudness
209 gain = TrackW.Gain;
210 OnGain(this, gain);
211
212 // get the tunePower of the track- this is a combination of the track's tuning power and attenuation
213 // the tuning power is defined by how closely the Output is tuned to this track. e.g, if this track's range is 100 - 300, and the
214 // Output's tune is 200, the track will likely be very loud- but if the tune is 120, it will be very quiet- the amount of loudness or
215 // quietness defined by the tune is named tune power here
216 //
217 // as well as the tuning power, this value is created with attenutation: how much quieter this track is when there are others playing
218 // the _otherVolume variable is the volume of other samples so far- if the attenuation of the track is high, then the tunePower will be
219 // lower depending on how high the other tracks' calculated tunePower is
220 //
221 // for more info, check inside the GetTunePower method
222 float tunePower = TrackW.GetTunePower(_tune, _otherVolume);
223 OnTunePower(this, tunePower);
224
225 // get the broadcast power of the track- this is dependent on where the Output is in relation to any RadioBroadcasters in the scene
226 // if the range of a Broadcaster is 100 units, and the output is 5 units away from it, it will likely hear the track loudly
227 // if the output is 95 units away, though, it will be heard quietly
228 // this works with many broadcasters and outputs- see RadioBroadcaster for more info
229 float broadcastPower = GetBroadcastPower(_receiverPosition);
230 OnBroadcastPower(this, broadcastPower);
231
232 // get the insulation of the track- this is effectively the inverse of broadcastPower, and is dependent on the position of the Output
233 // if the Insulator is a box 100 units wide, and the output is outside of it- the insulationMultiplier will be set to 1, as the track
234 // is not being insulated at all. if the output is inside the box, though, insulationMultiplier will be < 1 depending on the power in
235 // the RadioInsulator script.
236 // this was introduced to simulate "dead zones", or areas in which a broadcast can't be heard. if your output is inside an Insulator,
237 // the track will sound quieter- hence a lower insulationMultiplier the stronger the insulation
238 float insulationMultiplier = GetInsulation(_receiverPosition);
239 OnInsulation(this, insulationMultiplier);
240
241 // combine the broadcast power, tune power and insulation multiplier to the unified volume
242 volume = tunePower * broadcastPower * insulationMultiplier;
243 OnVolume(this, volume);
244 }
245
246 // get the sample at this moment from the track, and apply the volume and tunePower to it
247 float sample = TrackW.GetSample((int)Progress) * gain * volume;
248
249 // give back the volume to the Output
250 _outVolume = volume;
251
252 // and return the sample
253 return sample;
254 }
255
256 /// <summary>
257 /// Move this player to the next sample
258 /// </summary>
259 public void IncrementSample()
260 {
261 // if this track can't be played, don't increment its sample
262 if (isStopped || Paused)
263 return;
264
265 // the normal method if incrementing the sample if the track hasn't ended
266 void NormalIncrement()
267 {
268 OnSample.Invoke(this); // invoke the sample callback
269
270 // increment progress with the sampleIncrement, and clamp it between 0 and the maximum sample count
271 // see UpdateSampleIncrement() for more info on the sampleIncrement
272 Progress = Math.Clamp(Progress + sampleIncrement, 0, TrackW.SampleCount - 1);
273
275 }
276
277 switch (PlayType)
278 {
279 // if this player loops when the track ends,
280 case PlayerType.Loop:
281 if (ProgressFraction >= 1) // if the track has ended,
282 {
283 OnEnd.Invoke(this); // invoke the end callback
284 Progress = 0; // reset the progress to 0, i.e loop the track
285 OnPlay.Invoke(this); // invoke the play callback as the track has been restarted
286 }
287 else // if the track is still partway playing,
288 {
289 NormalIncrement(); // increment normally
290 }
291
292 break;
293
294 // if this player only plays once,
295 case PlayerType.OneShot:
296 if (ProgressFraction >= 1) // if the track has ended,
297 {
298 OnEnd.Invoke(this); // invoke the end callback
299 Stop(); // stop and destroy this track
300 }
301 else
302 {
303 NormalIncrement(); // increment normally
304 }
305
306 break;
307 }
308 }
309
310 /// <summary>
311 /// Stop playback and destroy this player. Make sure any references to it are removed as well
312 /// </summary>
313 public void Stop()
314 {
315 if (isStopped) // if it's already been stopped, don't do it again
316 return;
317
318 isStopped = true;
319
320 // the track does not reach the end, so we don't invoke the end callback
321 // but we do invoke the stop callback
322 OnStop.Invoke(this);
323
324 // destroy this player
325 Destroy();
326 }
327
328 /// <summary>
329 /// Resets the \ref Progress of this player to 0
330 /// </summary>
331 public void ResetProgress()
332 {
333 Progress = 0;
334 }
335
336 /// <summary>
337 /// Invokes \ref DoDestroy, destroying the player
338 /// </summary>
339 public void Destroy()
340 {
341 DoDestroy.Invoke(this);
342 }
343
344 /// <summary>
345 /// Gets the broadcast power of this player using the position of the Output and any any \ref RadioBroadcaster
346 /// </summary>
347 /// <param name="_receiverPosition">The position of the Output</param>
348 /// <returns>The broadcast power- higher the closer the Output is to broadcasters</returns>
349 public float GetBroadcastPower(Vector3 _receiverPosition)
350 {
351 // if this track is forced to be global, it is forced to not use broadcasters- as such, its broadcast power is always 1
352 if (TrackW.forceGlobal)
353 return 1;
354
355 float outPower = 0;
356
357 // if this track has broadcasters,
358 if (TrackW.broadcasters.Count > 0)
359 {
360 foreach (RadioBroadcaster broadcaster in TrackW.broadcasters) // get the power from each broadcaster
361 outPower = Mathf.Max(outPower, broadcaster.GetPower(_receiverPosition)); // select the highest broadcast power and use that
362
363 // short discussion- this initially just added the broadcast power of each broadcaster together and used that- but after some thinking
364 // and research, i've changed it to use the maximum power overall to be closest to real-world function
365 // in real life, if we put too weak broadcasters next to each other with a receiver in the middle, the receiver doesn't get the combined
366 // signal of both broadcasters- it just gets two weak signals, from which is chooses the strongest. as such, we recreate this here-
367 // combining the power of the signals just wouldn't make sense from a practical standpoint
368 //
369 // i will note, however, that i was debating making this a modifiable option, like a BroadcasterCombineType package preference or
370 // something, but for the time being we're just using the one method here
371 }
372 // if this track has no broadcasters (and is therefore global),
373 else
374 outPower = 1; // the broadcast power is 1
375
376 // return the broadcast power, which should only be from 0 to 1
377 return Mathf.Clamp01(outPower);
378 }
379
380 /// <summary>
381 /// Gets the insulation multiplier of this player using the position of the output and the bounds of any \ref RadioInsulator
382 /// </summary>
383 /// <param name="_receiverPosition">The position of the Output</param>
384 /// <returns>The insulation multiplier- the more insulated the Output is, the lower the multiplier</returns>
385 public float GetInsulation(Vector3 _receiverPosition)
386 {
387 // if there are no insulators, the multiplier is 1
388 float outGain = 1;
389
390 foreach (RadioInsulator insulator in TrackW.insulators) // get the insulation power from each insulator
391 outGain -= insulator.GetPower(_receiverPosition); // subtract that power from the insulation multiplier
392
393 // shorter discussion- we use the opposite method to broadcast power here, that is combining all the insulation into one value- this makes
394 // more sense to me than just choosing the maximum- i imagine if we put two lead blocks in front of a receiever, the signal will be reduced
395 // by the combined insulation of both blocks, not just the biggest one
396 // (if this is incorrect, please tell me so i don't sound too stupid :(((( )
397
398 // return the insulation multiplier, which can only be from 0 to 1
399 return Mathf.Clamp01(outGain);
400 }
401 }
402
403}
A "broadcaster" for a RadioTrackWrapper - the closer the RadioOutput that's playing the track is to a...
float GetPower(Vector3 _receiverPos)
Gets the broadcast power of this particular broadcaster using the Output's position.
An "insulator" for a RadioTrackWrapper - if a RadioOutput is inside the bounds of this object,...
float GetPower(Vector3 _position)
Gets the power of this insulator at a specific position.
float ProgressFraction
How far through the track this player is, from [0 - 1].
Action< RadioTrackPlayer > OnSample
Event called when this player retreieves a sample from its track.
Action< RadioTrackPlayer, float > OnInsulation
Event called when the insulation for this player is captured for a sample. Insulation is the quietnes...
double Progress
How many samples through the track this player is- not a whole number as we increment it with differe...
void Stop()
Stop playback and destroy this player. Make sure any references to it are removed as well.
RadioTrackPlayer(RadioTrackWrapper _trackW, PlayerType _playerType, float _baseSampleRate)
Creates a new player for the provided track.
Action< RadioTrackPlayer, float > OnGain
Event called when the gain for this player is captured for a sample. Gain is a direct change to the l...
float GetSample(float _tune, Vector3 _receiverPosition, float _otherVolume, out float _outVolume, bool _applyVolume=true)
Gets the current sample from the track according to playback. I would recommend reading the code comm...
PlayerType
The different types of Player- mainly changes what it does when the end of the track is reached.
@ Loop
When it ends, the player goes back to the start and plays the track again.
@ OneShot
When it ends, the player destroys itself and stops playing.
void UpdateSampleIncrement()
Updates the sampleIncrement variable to match the current track.
Action< RadioTrackPlayer > OnStop
Event called when the player is stopped through Stop()
bool Paused
Whether or not this player has been paused, temporarily halting playback of the track....
float baseSampleRate
The sample rate of the RadioOutput that this player is used by- that is, the sample rate of the radio...
Action< RadioTrackPlayer, float > OnVolume
Event called when the volume of this player is captured for a sample. Volume is the product of Tune p...
PlayerType PlayType
The type of player this is- what happens when the track ends.
float GetBroadcastPower(Vector3 _receiverPosition)
Gets the broadcast power of this player using the position of the Output and any any RadioBroadcaster...
void Destroy()
Invokes DoDestroy, destroying the player.
void IncrementSample()
Move this player to the next sample.
bool isStopped
Whether or not this player has been stopped- prevents it from being stopped multiple times.
Action< RadioTrackPlayer, float > OnTunePower
Event called when the tune power for this player is captured for a sample. Tune power is the loudness...
Action< RadioTrackPlayer, bool > OnPause
Event called when the player's pause state is changed- the bool is true if the player is being paused...
Action< RadioTrackPlayer, float > OnBroadcastPower
Event called when the broadcast power for this player is captured for a sample. Broadcast power is th...
float sampleIncrement
The amount that Progress is increased by every sample- the ratio of the track's sample speed to the b...
void ResetProgress()
Resets the Progress of this player to 0.
Action< RadioTrackPlayer > DoDestroy
Event called in order to destroy this player- this can either be invoked directly or as part of the D...
RadioTrackWrapper TrackW
The track that this player is associated with and plays during runtime.
float GetInsulation(Vector3 _receiverPosition)
Gets the insulation multiplier of this player using the position of the output and the bounds of any ...
Action< RadioTrackPlayer > OnPlay
Event called when the player starts playing.
Action< RadioTrackPlayer > OnEnd
Event called when the player reaches the end of its track naturally, before it takes an action depend...
A wrapper class for RadioTrack so that track types can be switched between in the inspector!...
Components to be placed on scene objects, e.g: Outputs, Broadcasters, Observers.
Tracks to be used on a radio- includes base classes.
Definition RadioUtils.cs:20