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