A game framework written with osu! in mind.

Initial rework of sample + samplechannel

+188 -194
+6 -10
osu.Framework.Tests/Audio/SampleBassTest.cs
··· 16 16 public class SampleBassTest 17 17 { 18 18 private DllResourceStore resources; 19 - 20 - private SampleBass sample; 21 - 22 - private SampleChannelBass channel; 19 + private Sample sample; 20 + private SampleChannel channel; 23 21 24 22 [SetUp] 25 23 public void Setup() ··· 28 26 Bass.Init(0); 29 27 30 28 resources = new DllResourceStore(typeof(TrackBassTest).Assembly); 31 - 32 29 sample = new SampleBass(resources.Get("Resources.Tracks.sample-track.mp3")); 33 30 34 - channel = new SampleChannelBass(sample, channel => { }); 35 31 updateSample(); 36 32 } 37 33 ··· 44 40 [Test] 45 41 public void TestStart() 46 42 { 47 - channel.Play(); 43 + channel = sample.Play(); 48 44 updateSample(); 49 45 50 46 Thread.Sleep(50); ··· 57 53 [Test] 58 54 public void TestStop() 59 55 { 60 - channel.Play(); 56 + channel = sample.Play(); 61 57 updateSample(); 62 58 63 59 channel.Stop(); ··· 69 65 [Test] 70 66 public void TestStopBeforeLoadFinished() 71 67 { 72 - channel.Play(); 68 + channel = sample.Play(); 73 69 channel.Stop(); 74 70 75 71 updateSample(); ··· 80 76 private void updateSample() => runOnAudioThread(() => 81 77 { 82 78 sample.Update(); 83 - channel.Update(); 79 + channel?.Update(); 84 80 }); 85 81 86 82 /// <summary>
-1
osu.Framework.Tests/Audio/SampleChannelVirtualTest.cs
··· 28 28 Assert.IsFalse(channel.Played); 29 29 Assert.IsFalse(channel.HasCompleted); 30 30 31 - channel.Play(); 32 31 updateChannel(); 33 32 34 33 Thread.Sleep(50);
+18 -15
osu.Framework.Tests/Visual/Audio/TestSceneSampleAmplitudes.cs
··· 4 4 using System.Linq; 5 5 using osu.Framework.Allocation; 6 6 using osu.Framework.Audio.Sample; 7 + using osu.Framework.Audio.Track; 7 8 using osu.Framework.Graphics; 8 9 using osu.Framework.Graphics.Audio; 9 10 using osu.Framework.Graphics.Containers; ··· 13 14 { 14 15 public class TestSceneSampleAmplitudes : FrameworkTestScene 15 16 { 16 - private DrawableSample sample; 17 - 18 17 private Box leftChannel; 19 18 private Box rightChannel; 20 19 21 - private SampleChannelBass bassSample; 20 + private DrawableSample sample; 21 + private SampleChannel channel; 22 22 23 23 private Container amplitudeBoxes; 24 24 25 25 [BackgroundDependencyLoader] 26 26 private void load(ISampleStore samples) 27 27 { 28 - bassSample = (SampleChannelBass)samples.Get("long.mp3"); 29 - 30 - var length = bassSample.CurrentAmplitudes.FrequencyAmplitudes.Length; 31 - 32 28 Children = new Drawable[] 33 29 { 34 - sample = new DrawableSample(bassSample), 30 + sample = new DrawableSample(samples.Get("long.mp3")), 35 31 new GridContainer 36 32 { 37 33 RelativeSizeAxes = Axes.Both, ··· 65 61 { 66 62 RelativeSizeAxes = Axes.Both, 67 63 ChildrenEnumerable = 68 - Enumerable.Range(0, length) 64 + Enumerable.Range(0, ChannelAmplitudes.AMPLITUDES_SIZE) 69 65 .Select(i => new Box 70 66 { 71 67 RelativeSizeAxes = Axes.Both, 72 68 RelativePositionAxes = Axes.X, 73 69 Anchor = Anchor.BottomLeft, 74 70 Origin = Anchor.BottomLeft, 75 - Width = 1f / length, 76 - X = (float)i / length 71 + Width = 1f / ChannelAmplitudes.AMPLITUDES_SIZE, 72 + X = (float)i / ChannelAmplitudes.AMPLITUDES_SIZE 77 73 }) 78 74 }, 79 75 } ··· 86 82 { 87 83 base.LoadComplete(); 88 84 89 - sample.Looping = true; 90 - AddStep("start sample", () => sample.Play()); 91 - AddStep("stop sample", () => sample.Stop()); 85 + AddStep("start sample", () => 86 + { 87 + channel = sample.Play(); 88 + channel.Looping = true; 89 + }); 90 + 91 + AddStep("stop sample", () => channel.Stop()); 92 92 } 93 93 94 94 protected override void Update() 95 95 { 96 96 base.Update(); 97 97 98 - var amplitudes = bassSample.CurrentAmplitudes; 98 + if (channel == null) 99 + return; 100 + 101 + var amplitudes = channel.CurrentAmplitudes; 99 102 100 103 rightChannel.Width = amplitudes.RightChannel * 0.5f; 101 104 leftChannel.Width = amplitudes.LeftChannel * 0.5f;
+28 -26
osu.Framework.Tests/Visual/Audio/TestSceneSampleLooping.cs
··· 11 11 { 12 12 public class TestSceneSampleLooping : FrameworkTestScene 13 13 { 14 - private SampleChannel sampleChannel; 14 + private Sample sample; 15 + private SampleChannel channel; 15 16 16 17 [Resolved] 17 18 private AudioManager audioManager { get; set; } ··· 20 21 public void SetUpSteps() 21 22 { 22 23 AddUntilStep("audio device ready", () => audioManager.IsLoaded); 23 - AddStep("create looping sample", createLoopingSample); 24 + AddStep("create looping sample", () => 25 + { 26 + channel?.Dispose(); 27 + sample = audioManager.Samples.Get("tone.wav"); 28 + }); 24 29 } 25 30 26 31 [Test] ··· 28 33 { 29 34 playAndCheckSample(); 30 35 31 - AddStep("disable looping", () => sampleChannel.Looping = false); 32 - AddUntilStep("ensure stops", () => !sampleChannel.Playing); 36 + AddStep("disable looping", () => channel.Looping = false); 37 + AddUntilStep("ensure stops", () => !channel.Playing); 33 38 } 34 39 35 40 [Test] ··· 37 42 { 38 43 playAndCheckSample(); 39 44 40 - AddStep("set frequency to 0", () => sampleChannel.Frequency.Value = 0); 41 - AddAssert("is still playing", () => sampleChannel.Playing); 45 + AddStep("set frequency to 0", () => channel.Frequency.Value = 0); 46 + AddAssert("is still playing", () => channel.Playing); 42 47 } 43 48 44 49 [Test] 45 50 public void TestZeroFrequencyOnStart() 46 51 { 47 - AddStep("set frequency to 0", () => sampleChannel.Frequency.Value = 0); 52 + AddStep("set frequency to 0", () => channel.Frequency.Value = 0); 48 53 playAndCheckSample(); 49 54 50 - AddStep("set frequency to 1", () => sampleChannel.Frequency.Value = 1); 51 - AddAssert("is still playing", () => sampleChannel.Playing); 55 + AddStep("set frequency to 1", () => channel.Frequency.Value = 1); 56 + AddAssert("is still playing", () => channel.Playing); 52 57 } 53 58 54 59 [Test] ··· 56 61 { 57 62 stopAndCheckSample(); 58 63 59 - AddStep("set frequency to 0", () => sampleChannel.Frequency.Value = 0); 60 - AddAssert("still stopped", () => !sampleChannel.Playing); 64 + AddStep("set frequency to 0", () => channel.Frequency.Value = 0); 65 + AddAssert("still stopped", () => !channel.Playing); 61 66 } 62 67 63 68 [TearDownSteps] ··· 68 73 69 74 private void playAndCheckSample() 70 75 { 71 - AddStep("play sample", () => sampleChannel.Play()); 76 + AddStep("play sample", () => 77 + { 78 + channel = sample.Play(); 79 + 80 + // reduce volume of the tone due to how loud it normally is. 81 + channel.Volume.Value = 0.05; 82 + channel.Looping = true; 83 + }); 72 84 73 85 // ensures that it is in fact looping given that the loaded sample length is very short. 74 86 AddWaitStep("wait", 10); 75 - AddAssert("is playing", () => sampleChannel.Playing); 87 + AddAssert("is playing", () => channel.Playing); 76 88 } 77 89 78 90 private void stopAndCheckSample() 79 91 { 80 - AddStep("stop playing", () => sampleChannel.Stop()); 81 - AddUntilStep("stopped", () => !sampleChannel.Playing); 82 - } 83 - 84 - private void createLoopingSample() 85 - { 86 - sampleChannel?.Dispose(); 87 - sampleChannel = audioManager.Samples.Get("tone.wav"); 88 - 89 - // reduce volume of the tone due to how loud it normally is. 90 - sampleChannel.Volume.Value = 0.05; 91 - sampleChannel.Looping = true; 92 + AddStep("stop playing", () => channel.Stop()); 93 + AddUntilStep("stopped", () => !channel.Playing); 92 94 } 93 95 94 96 protected override void Dispose(bool isDisposing) 95 97 { 96 - sampleChannel?.Dispose(); 98 + channel?.Dispose(); 97 99 base.Dispose(isDisposing); 98 100 } 99 101 }
+2 -2
osu.Framework.Tests/Visual/Audio/TestSceneSamples.cs
··· 196 196 Played = true; 197 197 circle.ScaleTo(1.8f).ScaleTo(1, 600, Easing.OutQuint); 198 198 199 - sample.Frequency.Value = 1 + Y / notes; 200 - sample.Play(); 199 + var channel = sample.Play(); 200 + channel.Frequency.Value = 1 + Y / notes; 201 201 } 202 202 } 203 203 }
+3 -2
osu.Framework/Audio/AudioCollectionManager.cs
··· 10 10 /// A collection of audio components which need central property control. 11 11 /// </summary> 12 12 public class AudioCollectionManager<T> : AdjustableAudioComponent, IBassAudio 13 - where T : AdjustableAudioComponent 13 + where T : AudioComponent 14 14 { 15 15 internal List<T> Items = new List<T>(); 16 16 ··· 20 20 { 21 21 if (Items.Contains(item)) return; 22 22 23 - item.BindAdjustments(this); 23 + (item as AdjustableAudioComponent)?.BindAdjustments(this); 24 + 24 25 Items.Add(item); 25 26 }); 26 27 }
+12
osu.Framework/Audio/Sample/ISample.cs
··· 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 + namespace osu.Framework.Audio.Sample 5 + { 6 + public interface ISample 7 + { 8 + double Length { get; } 9 + 10 + SampleChannel Play(); 11 + } 12 + }
-11
osu.Framework/Audio/Sample/ISampleChannel.cs
··· 9 9 public interface ISampleChannel : IHasAmplitudes 10 10 { 11 11 /// <summary> 12 - /// Start a playback of this sample. 13 - /// </summary> 14 - /// <param name="restart">Whether to restart the sample from the beginning. If true, any existing playback of the channel will be stopped.</param> 15 - void Play(bool restart = true); 16 - 17 - /// <summary> 18 12 /// Stop playback and reset position to beginning of sample. 19 13 /// </summary> 20 14 void Stop(); ··· 33 27 /// States if this sample should repeat. 34 28 /// </summary> 35 29 bool Looping { get; set; } 36 - 37 - /// <summary> 38 - /// The length of the underlying sample, in milliseconds. 39 - /// </summary> 40 - double Length { get; } 41 30 } 42 31 }
+1 -1
osu.Framework/Audio/Sample/ISampleStore.cs
··· 3 3 4 4 namespace osu.Framework.Audio.Sample 5 5 { 6 - public interface ISampleStore : IAdjustableResourceStore<SampleChannel> 6 + public interface ISampleStore : IAdjustableResourceStore<Sample> 7 7 { 8 8 /// <summary> 9 9 /// How many instances of a single sample should be allowed to playback concurrently before stopping the longest playing.
+16
osu.Framework/Audio/Sample/Sample.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + using System; 5 + 4 6 namespace osu.Framework.Audio.Sample 5 7 { 6 8 public abstract class Sample : AudioComponent 7 9 { 8 10 public const int DEFAULT_CONCURRENCY = 2; 11 + 12 + internal Action<SampleChannel> AddChannel; 9 13 10 14 /// <summary> 11 15 /// The length in milliseconds of this <see cref="Sample"/>. ··· 22 26 { 23 27 PlaybackConcurrency = playbackConcurrency; 24 28 } 29 + 30 + public SampleChannel Play() 31 + { 32 + var channel = CreateChannel(); 33 + 34 + if (channel != null) 35 + AddChannel?.Invoke(channel); 36 + 37 + return channel; 38 + } 39 + 40 + protected abstract SampleChannel CreateChannel(); 25 41 } 26 42 }
+8 -13
osu.Framework/Audio/Sample/SampleBass.cs
··· 3 3 4 4 using ManagedBass; 5 5 using osu.Framework.Allocation; 6 - using System.Collections.Concurrent; 7 6 using System.Runtime.InteropServices; 8 - using System.Threading.Tasks; 9 7 using osu.Framework.Platform; 10 8 11 9 namespace osu.Framework.Audio.Sample 12 10 { 13 11 internal sealed class SampleBass : Sample, IBassAudio 14 12 { 15 - private volatile int sampleId; 13 + public int SampleId { get; private set; } 16 14 17 - public override bool IsLoaded => sampleId != 0; 15 + public override bool IsLoaded => SampleId != 0; 18 16 19 17 private NativeMemoryTracker.NativeMemoryLease memoryLease; 20 18 21 - internal SampleBass(byte[] data, ConcurrentQueue<Task> customPendingActions = null, int concurrency = DEFAULT_CONCURRENCY) 19 + internal SampleBass(byte[] data, int concurrency = DEFAULT_CONCURRENCY) 22 20 : base(concurrency) 23 21 { 24 - if (customPendingActions != null) 25 - PendingActions = customPendingActions; 26 - 27 22 if (data.Length > 0) 28 23 { 29 24 EnqueueAction(() => 30 25 { 31 - sampleId = loadSample(data); 26 + SampleId = loadSample(data); 32 27 memoryLease = NativeMemoryTracker.AddMemory(this, data.Length); 33 28 }); 34 29 } ··· 38 33 { 39 34 if (IsLoaded) 40 35 { 41 - Bass.SampleFree(sampleId); 36 + Bass.SampleFree(SampleId); 42 37 memoryLease?.Dispose(); 43 38 } 44 39 ··· 51 46 return; 52 47 53 48 // counter-intuitively, this is the correct API to use to migrate a sample to a new device. 54 - Bass.ChannelSetDevice(sampleId, deviceIndex); 49 + Bass.ChannelSetDevice(SampleId, deviceIndex); 55 50 BassUtils.CheckFaulted(true); 56 51 } 57 - 58 - public int CreateChannel() => Bass.SampleGetChannel(sampleId); 59 52 60 53 private int loadSample(byte[] data) 61 54 { ··· 74 67 using (var handle = new ObjectHandle<byte[]>(data, GCHandleType.Pinned)) 75 68 return Bass.SampleLoad(handle.Address, 0, data.Length, PlaybackConcurrency, flags); 76 69 } 70 + 71 + protected override SampleChannel CreateChannel() => new SampleChannelBass(this); 77 72 } 78 73 }
+1 -27
osu.Framework/Audio/Sample/SampleChannel.cs
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 using osu.Framework.Statistics; 5 - using System; 6 5 using osu.Framework.Audio.Track; 7 6 8 7 namespace osu.Framework.Audio.Sample 9 8 { 10 9 public abstract class SampleChannel : AdjustableAudioComponent, ISampleChannel 11 10 { 12 - protected bool WasStarted; 13 - 14 - protected Sample Sample { get; set; } 15 - 16 - private readonly Action<SampleChannel> onPlay; 17 - 18 - protected SampleChannel(Sample sample, Action<SampleChannel> onPlay) 19 - { 20 - Sample = sample ?? throw new ArgumentNullException(nameof(sample)); 21 - this.onPlay = onPlay; 22 - } 23 - 24 - public virtual void Play(bool restart = true) 25 - { 26 - if (IsDisposed) 27 - throw new ObjectDisposedException(ToString(), "Can not play disposed samples."); 28 - 29 - onPlay(this); 30 - WasStarted = true; 31 - } 32 - 33 11 public virtual void Stop() 34 12 { 35 - if (IsDisposed) 36 - throw new ObjectDisposedException(ToString(), "Can not stop disposed samples."); 37 13 } 38 14 39 15 protected override void Dispose(bool disposing) ··· 52 28 53 29 public abstract bool Playing { get; } 54 30 55 - public virtual bool Played => WasStarted && !Playing; 56 - 57 - public double Length => Sample.Length; 31 + public virtual bool Played => !Playing; 58 32 59 33 public override bool IsAlive => base.IsAlive && !Played; 60 34
+26 -49
osu.Framework/Audio/Sample/SampleChannelBass.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 - using System; 5 4 using ManagedBass; 6 5 using osu.Framework.Audio.Track; 7 6 8 7 namespace osu.Framework.Audio.Sample 9 8 { 10 - public sealed class SampleChannelBass : SampleChannel, IBassAudio 9 + internal sealed class SampleChannelBass : SampleChannel, IBassAudio 11 10 { 12 11 private volatile int channel; 13 12 private volatile bool playing; 14 - 15 - public override bool IsLoaded => Sample.IsLoaded; 16 13 17 14 private readonly BassRelativeFrequencyHandler relativeFrequencyHandler; 18 15 private BassAmplitudeProcessor bassAmplitudeProcessor; 19 16 20 - public SampleChannelBass(Sample sample, Action<SampleChannel> onPlay) 21 - : base(sample, onPlay) 17 + public SampleChannelBass(SampleBass sample) 22 18 { 23 19 relativeFrequencyHandler = new BassRelativeFrequencyHandler 24 20 { 25 21 FrequencyChangedToZero = () => Bass.ChannelPause(channel), 26 22 FrequencyChangedFromZero = () => Bass.ChannelPlay(channel), 27 23 }; 24 + 25 + EnqueueAction(() => 26 + { 27 + channel = Bass.SampleGetChannel(sample.SampleId); 28 + if (channel == 0) 29 + return; 30 + 31 + Bass.ChannelSetAttribute(channel, ChannelAttribute.NoRamp, 1); 32 + setLoopFlag(Looping); 33 + 34 + relativeFrequencyHandler.SetChannel(channel); 35 + bassAmplitudeProcessor?.SetChannel(channel); 36 + 37 + // ensure state is correct before starting. 38 + InvalidateState(); 39 + 40 + if (channel != 0 && !relativeFrequencyHandler.IsFrequencyZero) 41 + Bass.ChannelPlay(channel, true); 42 + }); 43 + 44 + // Needs to happen on the main thread such that 45 + // Played does not become true for a short moment. 46 + playing = true; 28 47 } 29 48 30 49 void IBassAudio.UpdateDevice(int deviceIndex) ··· 58 77 } 59 78 } 60 79 61 - public override void Play(bool restart = true) 62 - { 63 - base.Play(restart); 64 - 65 - EnqueueAction(() => 66 - { 67 - if (!IsLoaded) 68 - { 69 - channel = 0; 70 - return; 71 - } 72 - 73 - bool existingChannelAvailable = Bass.ChannelIsActive(channel) != PlaybackState.Stopped; 74 - 75 - if (existingChannelAvailable) 76 - { 77 - // if restart is not requested and the sample is currently playing, nothing needs to be done. 78 - if (!restart) 79 - return; 80 - 81 - Stop(); 82 - } 83 - 84 - channel = ((SampleBass)Sample).CreateChannel(); 85 - 86 - Bass.ChannelSetAttribute(channel, ChannelAttribute.NoRamp, 1); 87 - setLoopFlag(Looping); 88 - 89 - relativeFrequencyHandler.SetChannel(channel); 90 - bassAmplitudeProcessor?.SetChannel(channel); 91 - 92 - // ensure state is correct before starting. 93 - InvalidateState(); 94 - 95 - if (channel != 0 && !relativeFrequencyHandler.IsFrequencyZero) 96 - Bass.ChannelPlay(channel, restart); 97 - }); 98 - 99 - // Needs to happen on the main thread such that 100 - // Played does not become true for a short moment. 101 - playing = true; 102 - } 103 - 104 80 protected override void UpdateState() 105 81 { 106 82 playing = channel != 0 && Bass.ChannelIsActive(channel) != 0; 83 + 107 84 base.UpdateState(); 108 85 109 86 bassAmplitudeProcessor?.Update();
+14 -5
osu.Framework/Audio/Sample/SampleChannelVirtual.cs
··· 9 9 /// </summary> 10 10 public sealed class SampleChannelVirtual : SampleChannel 11 11 { 12 - public SampleChannelVirtual() 13 - : base(new SampleVirtual(), _ => { }) 12 + private volatile bool playing = true; 13 + 14 + public override bool Playing => playing; 15 + 16 + public override void Stop() 14 17 { 18 + base.Stop(); 19 + playing = false; 15 20 } 21 + } 16 22 17 - public override bool Playing => false; 18 - 19 - private class SampleVirtual : Sample 23 + public sealed class SampleVirtual : Sample 24 + { 25 + public SampleVirtual(int playbackConcurrency = DEFAULT_CONCURRENCY) 26 + : base(playbackConcurrency) 20 27 { 21 28 } 29 + 30 + protected override SampleChannel CreateChannel() => new SampleChannelVirtual(); 22 31 } 23 32 }
+13 -14
osu.Framework/Audio/Sample/SampleStore.cs
··· 12 12 13 13 namespace osu.Framework.Audio.Sample 14 14 { 15 - internal class SampleStore : AudioCollectionManager<AdjustableAudioComponent>, ISampleStore 15 + internal class SampleStore : AudioCollectionManager<AudioComponent>, ISampleStore 16 16 { 17 17 private readonly IResourceStore<byte[]> store; 18 18 ··· 28 28 (store as ResourceStore<byte[]>)?.AddExtension(@"mp3"); 29 29 } 30 30 31 - public SampleChannel Get(string name) 31 + public Sample Get(string name) 32 32 { 33 33 if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(SampleStore)}"); 34 34 ··· 36 36 37 37 lock (sampleCache) 38 38 { 39 - SampleChannel channel = null; 39 + if (sampleCache.TryGetValue(name, out Sample sample)) 40 + return sample; 40 41 41 - if (!sampleCache.TryGetValue(name, out Sample sample)) 42 - { 43 - this.LogIfNonBackgroundThread(name); 42 + this.LogIfNonBackgroundThread(name); 43 + 44 + byte[] data = store.Get(name); 44 45 45 - byte[] data = store.Get(name); 46 - sample = sampleCache[name] = data == null ? null : new SampleBass(data, PendingActions, PlaybackConcurrency); 47 - } 46 + sample = sampleCache[name] = data == null 47 + ? null 48 + : new SampleBass(data, PlaybackConcurrency) { AddChannel = AddItem }; 48 49 49 50 if (sample != null) 50 - { 51 - channel = new SampleChannelBass(sample, AddItem); 52 - } 51 + AddItem(sample); 53 52 54 - return channel; 53 + return sample; 55 54 } 56 55 } 57 56 58 - public Task<SampleChannel> GetAsync(string name) => Task.Run(() => Get(name)); 57 + public Task<Sample> GetAsync(string name) => Task.Run(() => Get(name)); 59 58 60 59 internal override void UpdateDevice(int deviceIndex) 61 60 {
+40 -18
osu.Framework/Graphics/Audio/DrawableSample.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + using JetBrains.Annotations; 4 5 using osu.Framework.Audio.Sample; 5 6 using osu.Framework.Audio.Track; 6 7 ··· 9 10 /// <summary> 10 11 /// A <see cref="SampleChannel"/> wrapper to allow insertion in the draw hierarchy to allow transforms, lifetime management etc. 11 12 /// </summary> 12 - public class DrawableSample : DrawableAudioWrapper, ISampleChannel 13 + public class DrawableSample : DrawableAudioWrapper, ISample 13 14 { 14 - private readonly SampleChannel channel; 15 + private readonly Sample sample; 16 + private readonly bool disposeChannelsOnDisposal; 15 17 16 18 /// <summary> 17 19 /// Construct a new drawable sample instance. 18 20 /// </summary> 19 - /// <param name="channel">The audio sample to wrap.</param> 20 - /// <param name="disposeChannelOnDisposal">Whether the sample should be automatically disposed on drawable disposal/expiry.</param> 21 - public DrawableSample(SampleChannel channel, bool disposeChannelOnDisposal = true) 22 - : base(channel, disposeChannelOnDisposal) 21 + /// <param name="sample">The audio sample to wrap.</param> 22 + /// <param name="disposeChannelsOnDisposal">Whether the sample channels should be automatically disposed on drawable disposal/expiry.</param> 23 + public DrawableSample(Sample sample, bool disposeChannelsOnDisposal = true) 24 + : base(Empty()) 23 25 { 24 - this.channel = channel; 26 + this.sample = sample; 27 + this.disposeChannelsOnDisposal = disposeChannelsOnDisposal; 25 28 } 26 29 27 - public void Play(bool restart = true) => channel.Play(restart); 30 + public SampleChannel Play() 31 + { 32 + var channel = sample.Play(); 33 + AddInternal(new DrawableSampleChannel(channel, disposeChannelsOnDisposal)); 34 + return channel; 35 + } 28 36 29 - public void Stop() => channel.Stop(); 37 + public double Length => sample.Length; 30 38 31 - public bool Playing => channel.Playing; 39 + private class DrawableSampleChannel : DrawableAudioWrapper, ISampleChannel 40 + { 41 + [NotNull] 42 + private readonly SampleChannel channel; 32 43 33 - public bool Played => channel.Played; 44 + /// <param name="channel">The sample channel to wrap.</param> 45 + /// <param name="disposeChannelOnDisposal">Whether the channel should be automatically disposed on drawable disposal/expiry.</param> 46 + public DrawableSampleChannel([NotNull] SampleChannel channel, bool disposeChannelOnDisposal = true) 47 + : base(channel, disposeChannelOnDisposal) 48 + { 49 + this.channel = channel; 50 + } 51 + 52 + public ChannelAmplitudes CurrentAmplitudes => channel.CurrentAmplitudes; 53 + 54 + public void Stop() => channel.Stop(); 34 55 35 - public bool Looping 36 - { 37 - get => channel.Looping; 38 - set => channel.Looping = value; 39 - } 56 + public bool Playing => channel.Playing; 40 57 41 - public double Length => channel.Length; 58 + public bool Played => channel.Played; 42 59 43 - public ChannelAmplitudes CurrentAmplitudes => channel.CurrentAmplitudes; 60 + public bool Looping 61 + { 62 + get => channel.Looping; 63 + set => channel.Looping = value; 64 + } 65 + } 44 66 } 45 67 }