Dynamic Music System

Project Information

  1. Development Environment & Team
  2. Idea
  3. Usage
  4. Showcase
  5. Post Mortem

Code Snippets

  1. Track.cs
  2. Trackplayer.cs
  3. MusicSystem

Project Information

The project we worked on is called Operation Starfall (working title), and its a 2.5D couch co-op metroidvania game with a 80's cartoons theme. this is a massive project that multiple developers have and will work on in the future. This game will eventually be a commercial product. on this feature/user story i worked with 2 other devs to make this Here are their websites if you want to explore more about their work: Mikey Clarke Martijn van der Meer

The idea was to have a track which is just audio files put in layers so we can turn them on and off independantly for verticle mixing, but the first idea we had is to implement a tick system where it would keep track on what beat it currently is and would only add a sound on the next beat. so on a 4/4 time signature if the beat is between 2 and 3 it will wait till the song is at the 3rd beat and only then add a layer. we thought that this would be too complex for the game so we scratched the tick idea but kept the rest. altough its something i want to experiment with in my own freetime on a personal project. Here is a diagram/visual sheet so you can see what we were aiming for:

The idea is to have a Music manager that manages tracks, it stores a list of all the tracks so you can choose if you want to change to a specific track at all times. a track has layers which consists of a audio clip which is the file to play and a boolean whether it should be disabled on start or not, so when you play a song the base layer always plays and the other layers are turned off. then you also have the trackplayers which manage the actual track data and does all the stuff to make it play. we use Unity's built-in audio mixer and we can use the independant mixer groups for volume control. we never turn off the layers we only mute the layers so that the audio clips stay in sync at all times.

Eventually we made everything super optimized with the help of some code reviews and reworks, i was very happy about it but after looking at it i realized it could be optimized futher or reworked to be better in certain aspects

Code Snippets

This is the script that just holds all the data you can change in the inspector. The track has a name, and has layers. each layer as a audioclip you can assign and a boolean whether it should be muted or not when starting a track

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; [Serializable] public class Track { [SerializeField] private string trackName; public bool IsPlaying { get; set; } [Serializable] public struct TrackLayer { [field: SerializeField] public bool Disabled { get; private set; } [field: SerializeField] public AudioClip Clip { get; private set; } } [field: SerializeField] public TrackLayer[] layers { get; private set; } }

This is the script which plays/stops a song, handles which layer to turn on and off. and which audioMixerGroups to use since they are being re-used. we decided to use a fade function made by John Leonard French Instead of using Unity's built-in snapshot (I have linked an article of his if you are curious)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; public class TrackPlayer : MonoBehaviour { [HideInInspector] public Track currentTrack; private static AudioMixer Mixer { get; set; } [SerializeField] private float minVolume = 0.0001f; [SerializeField] private float maxVolume = 1f; [SerializeField] private int decibelMultiplier = 20; [SerializeField] private float fadeDuration; [SerializeField] private float fadeMultiplier = 10f; [SerializeField] private int layerAmount = 5; private bool _isFading; private string _songName; private AudioMixerGroup _parentMixerGroup; private string[] _layerName; private AudioMixerGroup[] _assignedMixerGroups; private List<AudioSource> _trackSources; //kan ook [HideInInspector] hebben private void Start() { _layerName = new string[layerAmount]; _assignedMixerGroups = new AudioMixerGroup[layerAmount]; _trackSources = new List<AudioSource>(layerAmount); } [Serializable] public struct MixerGroup { public AudioMixerGroup group; public string exposedParam; } [SerializeField] private MixerGroup[] mixerGroups; public static void SetMixer(AudioMixer targetMixer) => Mixer = targetMixer; private void MuteLayers() { var l = currentTrack.layers.Length; for (var i = 0; i < l; i++) { if (!currentTrack.layers[i].Disabled) continue; SetVolume(i, minVolume); } } private void AssignMixerGroups() { _parentMixerGroup = mixerGroups[0].group; _songName = mixerGroups[0].exposedParam; var l = currentTrack.layers.Length; for (int i = 0; i < l; i++) { _assignedMixerGroups[i] = mixerGroups[i+1].group; _layerName[i] = mixerGroups[i+1].exposedParam; } } private void CreateSources() { var l = currentTrack.layers.Length; for (int i = 0; i < l; i++) { var newSource = gameObject.AddComponent<AudioSource>(); _trackSources.Add(newSource); } } private void InitializeSources() { var l = _trackSources.Count; for (int i = 0; i < l; i++) { var source = _trackSources[i]; source.clip = currentTrack.layers[i].Clip; // zet clip van audiosource source.loop = true; source.outputAudioMixerGroup = _assignedMixerGroups[i]; // set audiosource output naar de audiomixergroup } } private void ClearSources() { var l = _trackSources.Count; for (int i = 0; i < l; i++) { Destroy(_trackSources[i]); } _trackSources.Clear(); } private void SetVolume(int index, float value) => Mixer.SetFloat(_layerName[index], Mathf.Log10(value) * decibelMultiplier); public void ToggleFadeLayer(params (int[] indexes, bool fadeToggle)[] targetValues) { if (_isFading) return; var targetValuesLength = targetValues.Length; for (var i = 0; i < targetValuesLength; i++) { var indexesLength = targetValues[i].indexes.Length; for (int j = 0; j < indexesLength; j++) { if (_layerName[targetValues[i].indexes[j]] == null || _layerName == null) return; StartCoroutine(Fade((_layerName[targetValues[i].indexes[j]], targetValues[i].fadeToggle))); } } } public void StartFadeIn() => StartCoroutine(FadeIn()); public void StartFadeOut() => StartCoroutine(FadeOut()); private IEnumerator FadeOut() { if (_songName != null) StartCoroutine(Fade((_songName, false))); currentTrack.IsPlaying = false; yield return new WaitForSeconds(fadeDuration); if (_trackSources == null || _trackSources.IsEmpty()) yield return null; var l = _trackSources.Count; for (var i = 0; i < l; i++) { _trackSources[i].Stop(); } ClearSources(); } private IEnumerator FadeIn() { if (_trackSources == null || _trackSources.IsEmpty()) { CreateSources(); AssignMixerGroups(); InitializeSources(); MuteLayers(); } var l = _trackSources.Count; for (int i = 0; i < l; i++) { _trackSources[i].Play(); } currentTrack.IsPlaying = true; StartCoroutine(Fade((_songName, true))); yield return new WaitForSeconds(fadeDuration); } private IEnumerator Fade(params (string exposedParam,bool fadeToggle)[] targetValues) { _isFading = true; var currentVolumes = new float[targetValues.Length]; var l = targetValues.Length; for (var i = 0; i < l; i++) { Mixer.GetFloat(targetValues[i].exposedParam, out currentVolumes[i]); currentVolumes[i] = Mathf.Pow(fadeMultiplier, currentVolumes[i] / decibelMultiplier); StartCoroutine(FadeLayer(targetValues[i].exposedParam, currentVolumes[i], targetValues[i].fadeToggle)); } yield return null; _isFading = false; } private IEnumerator FadeLayer(string exposedParam, float currentVolume, bool fadeIn) { var currentTime = 0f; if (currentVolume == minVolume) yield return null; var targetValue = fadeIn ? maxVolume : minVolume; while (currentTime < fadeDuration) { currentTime += Time.deltaTime; var newVol = Mathf.Lerp(currentVolume, targetValue, currentTime / fadeDuration); Mixer.SetFloat(exposedParam, Mathf.Log10(newVol) * decibelMultiplier); yield return null; } } }

This is the script that has 2 trackplayers and manages them both. where you can play/stop a track and transition between 2 tracks

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

using System; using System.Collections; using UnityEngine; using UnityEngine.Audio; public class MusicSystem : MonoBehaviour { [SerializeField] private AudioMixer mixer; public float Volume { get { mixer.GetFloat("MasterVolume", out float value); return Mathf.Pow(10, value/decibelMultiplier); } set => mixer.SetFloat("MasterVolume", Mathf.Log10(value) * decibelMultiplier); } [SerializeField] private float transitionDelay = 1f; [SerializeField] private int decibelMultiplier = 20; [SerializeField] private Track[] tracks; [SerializeField] private TrackPlayer[] trackPlayers; private TrackPlayer _currentTrackPlayer; private int _currentTrackId; private void Start() { SetCurrentTrackPlayer(0); TrackPlayer.SetMixer(mixer); SetCurrentTrack(tracks[0]); } public void Play() { if (_currentTrackPlayer.currentTrack.IsPlaying) return; _currentTrackPlayer.StartFadeIn(); } public void Stop() => _currentTrackPlayer.StartFadeOut(); public void PlayTrack(int trackIndex) { if (_currentTrackPlayer.currentTrack == tracks[trackIndex]) return; StartCoroutine(Transition(trackIndex)); } private IEnumerator Transition(int index) { _currentTrackPlayer.StartFadeOut(); yield return new WaitForSeconds(transitionDelay); SwapTrackPlayers(); SetCurrentTrack(tracks[index]); _currentTrackPlayer.StartFadeIn(); } //deze FadeLayer in/out methods kunnen ook dry door in 1 method te zetten met nog een boolean als parameter public void FadeInLayer(params int[] index) => _currentTrackPlayer.ToggleFadeLayer((index,true)); public void FadeOutLayer(params int[] index) => _currentTrackPlayer.ToggleFadeLayer((index,false)); public void FadeInLayer(TrackPlayer targetPlayer, params int[] index) => targetPlayer.ToggleFadeLayer((index, true)); public void FadeOutLayer(TrackPlayer targetPlayer, params int[] index) => targetPlayer.ToggleFadeLayer((index, true)); private void SetCurrentTrack(Track newTrack) => _currentTrackPlayer.currentTrack = newTrack; private void SwapTrackPlayers() => SetCurrentTrackPlayer(trackPlayers[0] == _currentTrackPlayer ? 1 : 0); private void SetCurrentTrackPlayer(int index) { _currentTrackPlayer = trackPlayers[index]; _currentTrackId = Array.IndexOf(trackPlayers, _currentTrackPlayer); } }