Ryle Radio 1.0.1
An open-source "radio" system for Unity, allowing tracks, tuning, broadcasters, and more!
Loading...
Searching...
No Matches
RadioOutput.cs
1using Codice.CM.Common;
2using NaughtyAttributes;
5using System;
6using System.Collections;
7using System.Collections.Generic;
8using System.Linq;
9using UnityEngine;
10using Random = UnityEngine.Random;
11
13{
14
15 /// <summary>
16 /// The main scene component for a radio that plays it through an AudioSource.
17 ///
18 /// <b>See </b>\ref RadioTrackPlayer as well for more info on how playback works
19 /// </summary>
20 [AddComponentMenu("Ryle Radio/Radio Output")]
21 [RequireComponent(typeof(AudioSource))]
23 {
24 /// <summary>
25 /// The method by which a RadioTrackPlayer is chosen from this output. Really only matters when you're playing the same track repeatedly causing overlaps
26 /// </summary>
28 {
29 Youngest, ///< Selects the youngest player
30 Oldest, ///< Selects the oldest player
31 Random ///< Selects a random player (probably useless but funny to have)
32 }
33
34 /// <summary>
35 /// The current tune value of this output- akin to the frequency of a real radio. Controls what tracks can be heard through tune power. Never modify this directly except for in the inspector, use \ref Tune instead
36 /// </summary>
37 [SerializeField, Range(RadioData.LOW_TUNE, RadioData.HIGH_TUNE), OnValueChanged("ExecOnTune")]
38 protected float tune;
39
40 /// <summary>
41 /// The players used by this output
42 /// </summary>
43 protected List<RadioTrackPlayer> players = new();
44
45 /// <summary>
46 /// The position of this object as of the last frame update. We can't access `transform.position` from the audio thread, so we cache it here
47 /// </summary>
48 protected Vector3 cachedPos;
49
50 /// <summary>
51 /// The normal sample rate of this output, applied to each \ref RadioTrackPlayer
52 /// </summary>
53 private float baseSampleRate;
54
55 /// <summary>
56 /// Called at the end of every audio cycle so that we don't interrupt threads when manipulating RadioTrackPlayers
57 /// </summary>
58 private Action playEvents = () => { };
59
60 /// <summary>
61 /// Every \ref RadioObserver associated with this output
62 /// </summary>
63 public List<RadioObserver> Observers { get; private set; } = new();
64
65 /// <summary>
66 /// Event called whenever \ref Tune is changed
67 /// </summary>
68 public Action<float> OnTune { get; set; } = new(_ => { });
69
70 /// <summary>
71 /// Event called when this output is finished initializing- mainly useful for WebGL when we use \ref AwaitLoadingThenInit
72 /// </summary>
73 public Action OnInit { get; set; } = new(() => { });
74
75 /// <summary>
76 /// The \ref tune clamped to the full range
77 /// </summary>
78 public float Tune
79 {
80 get => tune;
81 set
82 {
83 // clamp the tune to the available range (not needed in inspector, needed if tune changed in code)
84 tune = Mathf.Clamp(value, RadioData.LOW_TUNE, RadioData.HIGH_TUNE);
85
86 // invoke the tune change callback
87 OnTune(tune);
88 }
89 }
90
91 /// <summary>
92 /// \ref Tune scaled to [0 - 1], useful for UI
93 /// </summary>
94 public float Tune01
95 {
96 get => Mathf.InverseLerp(RadioData.LOW_TUNE, RadioData.HIGH_TUNE, Tune);
97 }
98
99 /// <summary>
100 /// \ref Tune with limited decimal points- looks better when displayed, more like an actual radio
101 /// </summary>
102 public float DisplayTune
103 {
104 get => Mathf.Round(tune * 10) / 10;
105 }
106
107 /// <summary>
108 /// Called when tune is modified in the inspector
109 /// </summary>
110 private void ExecOnTune()
111 {
112 // we invoke the callback here too so that it reacts when the tune is changed in the inspector
113 OnTune(tune);
114 }
115
116
117 /// <summary>
118 /// Updates \ref cachedPos
119 /// </summary>
120 protected virtual void Update()
121 {
122 // cache the position of this output
123 cachedPos = transform.position;
124 }
125
126#if !SKIP_IN_DOXYGEN
127 // starts the radio system- this component basically serves as a manager
128 private void Start()
129 {
130 #if UNITY_WEBGL
131 // if this is a webgl build, wait for clips to load before initializing
132 StartCoroutine(AwaitLoadingThenInit());
133 #else
134 // if this is not webgl, init right away
135 LocalInit();
136 #endif
137 }
138#endif
139
140 /// <summary>
141 /// Waits for all AudioClips used in this radio to be loaded before initializing the radio. This is used as WebGL will cause errors if a clip is used before it's fully loaded.
142 /// </summary>
143 private IEnumerator AwaitLoadingThenInit()
144 {
145 // store all child audioclips
146 List<AudioClip> unloadedClips = new List<AudioClip>();
147
148 // populate list of child audioclips
149 foreach (RadioTrackWrapper wrapper in data.TrackWrappers)
150 {
151 if (wrapper.TryGetClip(out AudioClip clip))
152 unloadedClips.Add(clip);
153 }
154
155 // while clips aren't yet loaded...
156 while (unloadedClips.Count > 0)
157 {
158 // iterate through all the clips
159 for (int i = 0; i < unloadedClips.Count; i++)
160 {
161 // and find those which are now loaded
162 if (unloadedClips[i].loadState == AudioDataLoadState.Loaded)
163 {
164 // and remove them
165 unloadedClips.Remove(unloadedClips[i]);
166 i--;
167 }
168 }
169
170 yield return null;
171 }
172 }
173
174 // we have to separate this and Init as otherwise data.Init() would call Init(), which calls data.Init(), which calls Init()......
175 // this is just a consequence of how the RadioComponent class works really
176 /// <summary>
177 /// Initializes the RadioData- this needs to be separated from \ref Init() as it would be recursive otherwise
178 /// </summary>
179 protected void LocalInit()
180 {
181 // initialize the associated RadioData
182 // note: this should be the only component that calls this- just for safety
183 data.Init();
184
185 }
186
187 /// <summary>
188 /// Initializes the output itself, and creates all required every required \ref RadioTrackPlayer
189 /// </summary>
190 public override void Init()
191 {
192 // save the sample rate of the whole Unity player
193 baseSampleRate = AudioSettings.outputSampleRate;
194
195 // create and start all track players
196 StartPlayers();
197
198 OnTune(tune);
199 }
200
201 /// <summary>
202 /// Sets up and stores a new \ref RadioTrackPlayer and alerts any \ref RadioObserver of its creation
203 /// </summary>
204 /// <param name="_player">The new player to set up</param>
205 protected void PlayerCreation(RadioTrackPlayer _player)
206 {
207 foreach (RadioObserver observer in Observers)
208 {
209 // if an observer is watching this track
210 if (observer.AffectedTracks.Contains(_player.TrackW.name))
211 {
212 observer.AssignEvents(_player); // point it towards this new player
213 }
214 }
215
216 // below here is a section where we insert the new player into the list while preserving the order of the tracks in the Data inspector.
217 // we do this so that attenuation is preserved. if we just added players to the list one by one, the order would change, and so tracks
218 // that are supposed to get quieter when another is played (attenuate) will not do so if the other player was created afterwards.
219
220 // the index of the track in the RadioData track list- the order of the Data in the inspector
221 int indexInData = Data.TrackIDs.IndexOf(_player.TrackW.id);
222
223 // the index at which to put the newly created player
224 int indexForNewPlayer = players.Count;
225
226 // search through the currently existent players to find one with a higher index in Data
227 for (int i = 0; i < players.Count; i++)
228 {
229 if (Data.TrackIDs.IndexOf(players[i].TrackW.id) > indexInData)
230 {
231 indexForNewPlayer = i;
232 break;
233 }
234 }
235
236 // store the player
237 if (players.Count == 0)
238 players.Add(_player);
239 else
240 players.Insert(indexForNewPlayer, _player);
241 }
242
243 /// <summary>
244 /// Creates every \ref RadioTrackPlayer that this output needs for playback
245 /// </summary>
246 private void StartPlayers()
247 {
248 // for every track in the RadioData
249 foreach (RadioTrackWrapper trackW in Data.TrackWrappers)
250 {
251 // if the track is supposed to play on game start
252 if (trackW.playOnInit)
253 {
254 // create a looping player for it
256
257 // and store the player
258 PlayerCreation(player);
259 }
260 }
261 }
262
263 /// <summary>
264 /// Plays a track as a one-shot. A one-shot destroys itself when its track ends.
265 /// </summary>
266 /// <param name="_id"></param>
267 /// <returns></returns>
268 public RadioTrackPlayer PlayOneShot(string _id)
269 {
270 // get the track with the given id
271 if (Data.TryGetTrack(_id, out RadioTrackWrapper track))
272 {
273 // create a new player for the track, set to one-shot
274 RadioTrackPlayer player = new(track, RadioTrackPlayer.PlayerType.OneShot, baseSampleRate);
275
276 // store the player
277 lock (playEvents)
278 playEvents += () => PlayerCreation(player);
279
280 // ensure that the new player is cleaned up when it's destroyed
281 player.DoDestroy += player =>
282 {
283 lock (playEvents)
284 playEvents += () => players.Remove(player);
285 };
286
287 // return the player
288 return player;
289 }
290 // if it can't find a player with the id, warn the user
291 else
292 {
293 Debug.LogWarning($"Can't find track with id {_id} to play as a one-shot!");
294 return null;
295 }
296 }
297
298 /// <summary>
299 /// Plays a track as a loop. A loop restarts when the track ends, then continues to play.
300 /// </summary>
301 /// <param name="_id"></param>
302 /// <returns></returns>
303 public RadioTrackPlayer PlayLoop(string _id)
304 {
305 // get the track with the given id
306 if (Data.TryGetTrack(_id, out RadioTrackWrapper track))
307 {
308 // create a new player for the track, set to loop
310
311 // store the player
312 lock (playEvents)
313 playEvents += () => PlayerCreation(player);
314
315 // return the player
316 return player;
317 }
318 // if it can't find a player with the id, warn the user
319 else
320 {
321 Debug.LogWarning($"Can't find track with id {_id} to play as a loop!");
322 return null;
323 }
324
325 }
326
327 // try to find an active RadioTrackPlayer on this output
328 /// <summary>
329 /// Gets an active \ref RadioTrackPlayer from this output.
330 /// </summary>
331 /// <param name="_trackID">The ID of the track used by the player</param>
332 /// <param name="_player">Output parameter containing the found player</param>
333 /// <param name="_createNew">Whether or not a new player should be created if one can't be found. Players created this way are always one-shots</param>
334 /// <param name="_multiplePlayerSelector">How a player is selected when multiple are present for the same track</param>
335 /// <returns>True if a player was found or created, false if not</returns>
336 public bool TryGetPlayer(
337 string _trackID,
338 out RadioTrackPlayer _player,
339 bool _createNew = false,
340 MultiplePlayersSelector _multiplePlayerSelector = MultiplePlayersSelector.Youngest
341 )
342 {
343 // find any players associated with the track with the given id
344 var found = players.Where(p => p.TrackW.id == _trackID);
345
346 // if there are multiple players,
347 if (found.Count() > 1)
348 {
349 // select one based off the given selector
350 switch (_multiplePlayerSelector)
351 {
352 default: // choose the youngest player
353 case MultiplePlayersSelector.Youngest:
354 _player = found.Last();
355 break;
356
357 // choose the oldest player
358 case MultiplePlayersSelector.Oldest:
359 _player = found.First();
360 break;
361
362 // choose a random player
363 case MultiplePlayersSelector.Random:
364 int index = Random.Range(0, found.Count());
365 _player = found.ElementAt(index);
366
367 break;
368 }
369
370 // a player was found, so return true
371 return true;
372 }
373 // if there is one player,
374 else if (found.Count() > 0)
375 {
376 // choose it and return true
377 _player = found.First();
378 return true;
379 }
380
381 // if there aren't any players for this track,
382 // and the method is set to make a new one,
383 else if (_createNew)
384 {
385 // create a new player for this track
386 // if you want it to be a loop, play it manually- this is more of an error catch than an actual method of creation
387 _player = PlayOneShot(_trackID);
388 return true; // and return true
389 }
390 // and we aren't creating a new one,
391 else
392 {
393 _player = null; // there is no player
394 return false; // so we return false
395 }
396 }
397
398 /// <summary>
399 /// Gets a set of samples from the radio to play from the AudioSource- this preserves the settings on the Source, e.g: volume, 3D. This is the main driving method for the radio's playback.
400 ///
401 /// The method itself appears to have been initially introduced so devs could create custom audio filters, but it just so happens we can use it for direct output of samples too!
402 /// </summary>
403 /// <param name="_data">Whatever other audio is playing from the AudioSource- preferably nothing</param>
404 /// <param name="_channels">The number of channels the AudioSource is using- the radio itself is limited to one channel, but still outputs as two- they'll just be identical.</param>
405 protected virtual void OnAudioFilterRead(float[] _data, int _channels)
406 {
407 // the output only plays back one channel, so we have to account for this when the radio is using
408 // tracks with more than one channel
409
410 // the number of samples for, total- the _data array has an entry for each channel by default. e.g if _data was 2048
411 // entries long, and there were two channels- there would actually only be 1024 samples
412 int monoSampleCount = _data.Length / _channels;
413
414 // for every sample in the data array
415 for (int index = 0; index < monoSampleCount; index++)
416 {
417 // we want to store the sample itself, and the added volume of all previous tracks
418 float sample = 0;
419 float combinedVolume = 0;
420
421 // for every active track,
422 foreach (RadioTrackPlayer player in players)
423 {
424 // get the audio in this sample
425 sample += player.GetSample(Tune, cachedPos, combinedVolume, out float outVolume, true);
426
427 // then move along to the next sample
428 player.IncrementSample();
429
430 // store the volume so far so that tracks with attenuation can adjust accordingly- see RadioTrackPlayer.GetSample()
431 combinedVolume += outVolume;
432 }
433
434 // this function uses _data for each sample packed into one big list- each channel is joined end to end
435 // that means if there are multiple _channels, we need to apply the audio to each of them before jumping to the next sample
436 // therefore we iterate through every channel, for every sample
437 int indexWithChannels = index * _channels;
438
439 // for each channel,
440 for (int channel = indexWithChannels; channel < indexWithChannels + _channels; channel++)
441 _data[channel] += sample; // apply the sample
442 }
443
444 // execute all events waiting for this read sequence to end
445 lock (playEvents)
446 {
447 playEvents(); // execute the delegate
448 playEvents = () => { }; // clear it
449 }
450 }
451
452 }
453
454}
A scene component that holds a reference to a RadioData.
RadioData Data
Read-only accessor for data.
RadioData data
The RadioData (aka just radio) that this component is linked to.
A component used to watch for specific happenings on a RadioOutput, e.g: a clip being a certain volum...
string[] AffectedTracks
The tracks selected on this Observer.
void AssignEvents(RadioTrackPlayer _player)
Assigns each event to a RadioTrackPlayer for one of our AffectedTracks. This is called when a new Rad...
The main scene component for a radio that plays it through an AudioSource.
RadioTrackPlayer PlayLoop(string _id)
Plays a track as a loop. A loop restarts when the track ends, then continues to play.
Action< float > OnTune
Event called whenever Tune is changed.
List< RadioObserver > Observers
Every RadioObserver associated with this output.
float Tune01
Tune scaled to [0 - 1], useful for UI
void LocalInit()
Initializes the RadioData- this needs to be separated from Init() as it would be recursive otherwise.
virtual void OnAudioFilterRead(float[] _data, int _channels)
Gets a set of samples from the radio to play from the AudioSource- this preserves the settings on the...
IEnumerator AwaitLoadingThenInit()
Waits for all AudioClips used in this radio to be loaded before initializing the radio....
float Tune
The tune clamped to the full range.
void StartPlayers()
Creates every RadioTrackPlayer that this output needs for playback.
Vector3 cachedPos
The position of this object as of the last frame update. We can't access transform....
float DisplayTune
Tune with limited decimal points- looks better when displayed, more like an actual radio
Action playEvents
Called at the end of every audio cycle so that we don't interrupt threads when manipulating RadioTrac...
override void Init()
Initializes the output itself, and creates all required every required RadioTrackPlayer.
float tune
The current tune value of this output- akin to the frequency of a real radio. Controls what tracks ca...
List< RadioTrackPlayer > players
The players used by this output.
Action OnInit
Event called when this output is finished initializing- mainly useful for WebGL when we use AwaitLoad...
MultiplePlayersSelector
The method by which a RadioTrackPlayer is chosen from this output. Really only matters when you're pl...
@ Random
Selects a random player (probably useless but funny to have)
void PlayerCreation(RadioTrackPlayer _player)
Sets up and stores a new RadioTrackPlayer and alerts any RadioObserver of its creation.
float baseSampleRate
The normal sample rate of this output, applied to each RadioTrackPlayer.
void ExecOnTune()
Called when tune is modified in the inspector.
bool TryGetPlayer(string _trackID, out RadioTrackPlayer _player, bool _createNew=false, MultiplePlayersSelector _multiplePlayerSelector=MultiplePlayersSelector.Youngest)
Gets an active RadioTrackPlayer from this output.
virtual void Update()
Updates cachedPos.
RadioTrackPlayer PlayOneShot(string _id)
Plays a track as a one-shot. A one-shot destroys itself when its track ends.
The central data object defining the radio. Contains the tracks and information required to play the ...
Definition RadioData.cs:17
const float LOW_TUNE
The lower limit for tune on this radio. This may become non-const at some point.
Definition RadioData.cs:20
const float HIGH_TUNE
The upper limit for tune on this radio. This may become non-const at some point.
Definition RadioData.cs:22
A class that plays a certain RadioTrack at runtime. It's created newly for each track on each RadioOu...
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.
void IncrementSample()
Move this player to the next sample.
A wrapper class for RadioTrack so that track types can be switched between in the inspector!...
bool playOnInit
If true, this track plays on RadioData.Init() - usually on game start.
Base interfaces and classes for components, e.g: track accessors, output accessors.
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