A game framework written with osu! in mind.
at master 229 lines 7.2 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4#nullable enable 5 6using ManagedBass; 7using osu.Framework.Audio.Mixing.Bass; 8using osu.Framework.Audio.Track; 9using osu.Framework.Extensions.ObjectExtensions; 10 11namespace osu.Framework.Audio.Sample 12{ 13 internal sealed class SampleChannelBass : SampleChannel, IBassAudioChannel 14 { 15 private readonly SampleBass sample; 16 private volatile int channel; 17 18 /// <summary> 19 /// Whether the channel is currently playing. 20 /// </summary> 21 /// <remarks> 22 /// This is set to <c>true</c> immediately upon <see cref="Play"/>, but the channel may not be audibly playing yet. 23 /// </remarks> 24 public override bool Playing => playing || enqueuedPlaybackStart; 25 26 private volatile bool playing; 27 28 /// <summary> 29 /// <c>true</c> if the user last called <see cref="Play"/>. 30 /// <c>false</c> if the user last called <see cref="Stop"/>. 31 /// </summary> 32 private volatile bool userRequestedPlay; 33 34 /// <summary> 35 /// Whether the playback start has been enqueued. 36 /// </summary> 37 private volatile bool enqueuedPlaybackStart; 38 39 private readonly BassRelativeFrequencyHandler relativeFrequencyHandler; 40 private BassAmplitudeProcessor? bassAmplitudeProcessor; 41 42 /// <summary> 43 /// Creates a new <see cref="SampleChannelBass"/>. 44 /// </summary> 45 /// <param name="sample">The <see cref="SampleBass"/> to create the channel from.</param> 46 public SampleChannelBass(SampleBass sample) 47 { 48 this.sample = sample; 49 50 relativeFrequencyHandler = new BassRelativeFrequencyHandler 51 { 52 FrequencyChangedToZero = stopChannel, 53 FrequencyChangedFromZero = () => 54 { 55 // Only unpause if the channel has been played by the user. 56 if (userRequestedPlay) 57 playChannel(); 58 }, 59 }; 60 61 ensureChannel(); 62 } 63 64 public override void Play() 65 { 66 userRequestedPlay = true; 67 68 // Pin Playing and IsAlive to true so that the channel isn't killed by the next update. This is only reset after playback is started. 69 enqueuedPlaybackStart = true; 70 71 // Bring this channel alive, allowing it to receive updates. 72 base.Play(); 73 74 playChannel(); 75 } 76 77 internal override void OnStateChanged() 78 { 79 base.OnStateChanged(); 80 81 if (!hasChannel) 82 return; 83 84 Bass.ChannelSetAttribute(channel, ChannelAttribute.Volume, AggregateVolume.Value); 85 Bass.ChannelSetAttribute(channel, ChannelAttribute.Pan, AggregateBalance.Value); 86 relativeFrequencyHandler.SetFrequency(AggregateFrequency.Value); 87 } 88 89 public override bool Looping 90 { 91 get => base.Looping; 92 set 93 { 94 base.Looping = value; 95 setLoopFlag(Looping); 96 } 97 } 98 99 protected override void UpdateState() 100 { 101 if (hasChannel) 102 { 103 switch (bassMixer.ChannelIsActive(this)) 104 { 105 case PlaybackState.Playing: 106 // Stalled counts as playing, as playback will continue once more data has streamed in. 107 case PlaybackState.Stalled: 108 // The channel is in a "paused" state via zero-frequency. It should be marked as playing even if it's in a paused state internally. 109 case PlaybackState.Paused when userRequestedPlay: 110 playing = true; 111 break; 112 113 default: 114 playing = false; 115 break; 116 } 117 } 118 else 119 { 120 // Channel doesn't exist - a rare case occurring as a result of device updates. 121 playing = false; 122 } 123 124 base.UpdateState(); 125 126 bassAmplitudeProcessor?.Update(); 127 } 128 129 public override void Stop() 130 { 131 userRequestedPlay = false; 132 133 base.Stop(); 134 135 stopChannel(); 136 } 137 138 public override ChannelAmplitudes CurrentAmplitudes => (bassAmplitudeProcessor ??= new BassAmplitudeProcessor(this)).CurrentAmplitudes; 139 140 private bool hasChannel => channel != 0; 141 142 private void playChannel() => EnqueueAction(() => 143 { 144 try 145 { 146 // Channel may have been freed via UpdateDevice(). 147 ensureChannel(); 148 149 if (!hasChannel) 150 return; 151 152 // Ensure state is correct before starting. 153 InvalidateState(); 154 155 // Bass will restart the sample if it has reached its end. This behavior isn't desirable so block locally. 156 // Unlike TrackBass, sample channels can't have sync callbacks attached, so the stopped state is used instead 157 // to indicate the natural stoppage of a sample as a result of having reaching the end. 158 if (Played && bassMixer.ChannelIsActive(this) == PlaybackState.Stopped) 159 return; 160 161 playing = true; 162 163 if (!relativeFrequencyHandler.IsFrequencyZero) 164 bassMixer.ChannelPlay(this); 165 } 166 finally 167 { 168 enqueuedPlaybackStart = false; 169 } 170 }); 171 172 private void stopChannel() => EnqueueAction(() => 173 { 174 if (hasChannel) 175 bassMixer.ChannelPause(this); 176 }); 177 178 private void setLoopFlag(bool value) => EnqueueAction(() => 179 { 180 if (hasChannel) 181 Bass.ChannelFlags(channel, value ? BassFlags.Loop : BassFlags.Default, BassFlags.Loop); 182 }); 183 184 private void ensureChannel() => EnqueueAction(() => 185 { 186 if (hasChannel) 187 return; 188 189 channel = Bass.SampleGetChannel(sample.SampleId, BassFlags.SampleChannelStream | BassFlags.Decode); 190 191 if (!hasChannel) 192 return; 193 194 setLoopFlag(Looping); 195 196 relativeFrequencyHandler.SetChannel(channel); 197 }); 198 199 #region Mixing 200 201 private BassAudioMixer bassMixer => (BassAudioMixer)Mixer.AsNonNull(); 202 203 bool IBassAudioChannel.IsActive => IsAlive; 204 205 int IBassAudioChannel.Handle => channel; 206 207 bool IBassAudioChannel.MixerChannelPaused { get; set; } = true; 208 209 BassAudioMixer IBassAudioChannel.Mixer => bassMixer; 210 211 #endregion 212 213 protected override void Dispose(bool disposing) 214 { 215 if (IsDisposed) 216 return; 217 218 if (hasChannel) 219 { 220 bassMixer.StreamFree(this); 221 channel = 0; 222 } 223 224 playing = false; 225 226 base.Dispose(disposing); 227 } 228 } 229}