···196196 Played = true;
197197 circle.ScaleTo(1.8f).ScaleTo(1, 600, Easing.OutQuint);
198198199199- sample.Frequency.Value = 1 + Y / notes;
200200- sample.Play();
199199+ var channel = sample.Play();
200200+ channel.Frequency.Value = 1 + Y / notes;
201201 }
202202 }
203203 }
+3-2
osu.Framework/Audio/AudioCollectionManager.cs
···1010 /// A collection of audio components which need central property control.
1111 /// </summary>
1212 public class AudioCollectionManager<T> : AdjustableAudioComponent, IBassAudio
1313- where T : AdjustableAudioComponent
1313+ where T : AudioComponent
1414 {
1515 internal List<T> Items = new List<T>();
1616···2020 {
2121 if (Items.Contains(item)) return;
22222323- item.BindAdjustments(this);
2323+ (item as AdjustableAudioComponent)?.BindAdjustments(this);
2424+2425 Items.Add(item);
2526 });
2627 }
+12
osu.Framework/Audio/Sample/ISample.cs
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22+// See the LICENCE file in the repository root for full licence text.
33+44+namespace osu.Framework.Audio.Sample
55+{
66+ public interface ISample
77+ {
88+ double Length { get; }
99+1010+ SampleChannel Play();
1111+ }
1212+}
-11
osu.Framework/Audio/Sample/ISampleChannel.cs
···99 public interface ISampleChannel : IHasAmplitudes
1010 {
1111 /// <summary>
1212- /// Start a playback of this sample.
1313- /// </summary>
1414- /// <param name="restart">Whether to restart the sample from the beginning. If true, any existing playback of the channel will be stopped.</param>
1515- void Play(bool restart = true);
1616-1717- /// <summary>
1812 /// Stop playback and reset position to beginning of sample.
1913 /// </summary>
2014 void Stop();
···3327 /// States if this sample should repeat.
3428 /// </summary>
3529 bool Looping { get; set; }
3636-3737- /// <summary>
3838- /// The length of the underlying sample, in milliseconds.
3939- /// </summary>
4040- double Length { get; }
4130 }
4231}
+1-1
osu.Framework/Audio/Sample/ISampleStore.cs
···3344namespace osu.Framework.Audio.Sample
55{
66- public interface ISampleStore : IAdjustableResourceStore<SampleChannel>
66+ public interface ISampleStore : IAdjustableResourceStore<Sample>
77 {
88 /// <summary>
99 /// 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
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344+using System;
55+46namespace osu.Framework.Audio.Sample
57{
68 public abstract class Sample : AudioComponent
79 {
810 public const int DEFAULT_CONCURRENCY = 2;
1111+1212+ internal Action<SampleChannel> AddChannel;
9131014 /// <summary>
1115 /// The length in milliseconds of this <see cref="Sample"/>.
···2226 {
2327 PlaybackConcurrency = playbackConcurrency;
2428 }
2929+3030+ public SampleChannel Play()
3131+ {
3232+ var channel = CreateChannel();
3333+3434+ if (channel != null)
3535+ AddChannel?.Invoke(channel);
3636+3737+ return channel;
3838+ }
3939+4040+ protected abstract SampleChannel CreateChannel();
2541 }
2642}
+8-13
osu.Framework/Audio/Sample/SampleBass.cs
···3344using ManagedBass;
55using osu.Framework.Allocation;
66-using System.Collections.Concurrent;
76using System.Runtime.InteropServices;
88-using System.Threading.Tasks;
97using osu.Framework.Platform;
108119namespace osu.Framework.Audio.Sample
1210{
1311 internal sealed class SampleBass : Sample, IBassAudio
1412 {
1515- private volatile int sampleId;
1313+ public int SampleId { get; private set; }
16141717- public override bool IsLoaded => sampleId != 0;
1515+ public override bool IsLoaded => SampleId != 0;
18161917 private NativeMemoryTracker.NativeMemoryLease memoryLease;
20182121- internal SampleBass(byte[] data, ConcurrentQueue<Task> customPendingActions = null, int concurrency = DEFAULT_CONCURRENCY)
1919+ internal SampleBass(byte[] data, int concurrency = DEFAULT_CONCURRENCY)
2220 : base(concurrency)
2321 {
2424- if (customPendingActions != null)
2525- PendingActions = customPendingActions;
2626-2722 if (data.Length > 0)
2823 {
2924 EnqueueAction(() =>
3025 {
3131- sampleId = loadSample(data);
2626+ SampleId = loadSample(data);
3227 memoryLease = NativeMemoryTracker.AddMemory(this, data.Length);
3328 });
3429 }
···3833 {
3934 if (IsLoaded)
4035 {
4141- Bass.SampleFree(sampleId);
3636+ Bass.SampleFree(SampleId);
4237 memoryLease?.Dispose();
4338 }
4439···5146 return;
52475348 // counter-intuitively, this is the correct API to use to migrate a sample to a new device.
5454- Bass.ChannelSetDevice(sampleId, deviceIndex);
4949+ Bass.ChannelSetDevice(SampleId, deviceIndex);
5550 BassUtils.CheckFaulted(true);
5651 }
5757-5858- public int CreateChannel() => Bass.SampleGetChannel(sampleId);
59526053 private int loadSample(byte[] data)
6154 {
···7467 using (var handle = new ObjectHandle<byte[]>(data, GCHandleType.Pinned))
7568 return Bass.SampleLoad(handle.Address, 0, data.Length, PlaybackConcurrency, flags);
7669 }
7070+7171+ protected override SampleChannel CreateChannel() => new SampleChannelBass(this);
7772 }
7873}
+1-27
osu.Framework/Audio/Sample/SampleChannel.cs
···22// See the LICENCE file in the repository root for full licence text.
3344using osu.Framework.Statistics;
55-using System;
65using osu.Framework.Audio.Track;
7687namespace osu.Framework.Audio.Sample
98{
109 public abstract class SampleChannel : AdjustableAudioComponent, ISampleChannel
1110 {
1212- protected bool WasStarted;
1313-1414- protected Sample Sample { get; set; }
1515-1616- private readonly Action<SampleChannel> onPlay;
1717-1818- protected SampleChannel(Sample sample, Action<SampleChannel> onPlay)
1919- {
2020- Sample = sample ?? throw new ArgumentNullException(nameof(sample));
2121- this.onPlay = onPlay;
2222- }
2323-2424- public virtual void Play(bool restart = true)
2525- {
2626- if (IsDisposed)
2727- throw new ObjectDisposedException(ToString(), "Can not play disposed samples.");
2828-2929- onPlay(this);
3030- WasStarted = true;
3131- }
3232-3311 public virtual void Stop()
3412 {
3535- if (IsDisposed)
3636- throw new ObjectDisposedException(ToString(), "Can not stop disposed samples.");
3713 }
38143915 protected override void Dispose(bool disposing)
···52285329 public abstract bool Playing { get; }
54305555- public virtual bool Played => WasStarted && !Playing;
5656-5757- public double Length => Sample.Length;
3131+ public virtual bool Played => !Playing;
58325933 public override bool IsAlive => base.IsAlive && !Played;
6034
+26-49
osu.Framework/Audio/Sample/SampleChannelBass.cs
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344-using System;
54using ManagedBass;
65using osu.Framework.Audio.Track;
7687namespace osu.Framework.Audio.Sample
98{
1010- public sealed class SampleChannelBass : SampleChannel, IBassAudio
99+ internal sealed class SampleChannelBass : SampleChannel, IBassAudio
1110 {
1211 private volatile int channel;
1312 private volatile bool playing;
1414-1515- public override bool IsLoaded => Sample.IsLoaded;
16131714 private readonly BassRelativeFrequencyHandler relativeFrequencyHandler;
1815 private BassAmplitudeProcessor bassAmplitudeProcessor;
19162020- public SampleChannelBass(Sample sample, Action<SampleChannel> onPlay)
2121- : base(sample, onPlay)
1717+ public SampleChannelBass(SampleBass sample)
2218 {
2319 relativeFrequencyHandler = new BassRelativeFrequencyHandler
2420 {
2521 FrequencyChangedToZero = () => Bass.ChannelPause(channel),
2622 FrequencyChangedFromZero = () => Bass.ChannelPlay(channel),
2723 };
2424+2525+ EnqueueAction(() =>
2626+ {
2727+ channel = Bass.SampleGetChannel(sample.SampleId);
2828+ if (channel == 0)
2929+ return;
3030+3131+ Bass.ChannelSetAttribute(channel, ChannelAttribute.NoRamp, 1);
3232+ setLoopFlag(Looping);
3333+3434+ relativeFrequencyHandler.SetChannel(channel);
3535+ bassAmplitudeProcessor?.SetChannel(channel);
3636+3737+ // ensure state is correct before starting.
3838+ InvalidateState();
3939+4040+ if (channel != 0 && !relativeFrequencyHandler.IsFrequencyZero)
4141+ Bass.ChannelPlay(channel, true);
4242+ });
4343+4444+ // Needs to happen on the main thread such that
4545+ // Played does not become true for a short moment.
4646+ playing = true;
2847 }
29483049 void IBassAudio.UpdateDevice(int deviceIndex)
···5877 }
5978 }
60796161- public override void Play(bool restart = true)
6262- {
6363- base.Play(restart);
6464-6565- EnqueueAction(() =>
6666- {
6767- if (!IsLoaded)
6868- {
6969- channel = 0;
7070- return;
7171- }
7272-7373- bool existingChannelAvailable = Bass.ChannelIsActive(channel) != PlaybackState.Stopped;
7474-7575- if (existingChannelAvailable)
7676- {
7777- // if restart is not requested and the sample is currently playing, nothing needs to be done.
7878- if (!restart)
7979- return;
8080-8181- Stop();
8282- }
8383-8484- channel = ((SampleBass)Sample).CreateChannel();
8585-8686- Bass.ChannelSetAttribute(channel, ChannelAttribute.NoRamp, 1);
8787- setLoopFlag(Looping);
8888-8989- relativeFrequencyHandler.SetChannel(channel);
9090- bassAmplitudeProcessor?.SetChannel(channel);
9191-9292- // ensure state is correct before starting.
9393- InvalidateState();
9494-9595- if (channel != 0 && !relativeFrequencyHandler.IsFrequencyZero)
9696- Bass.ChannelPlay(channel, restart);
9797- });
9898-9999- // Needs to happen on the main thread such that
100100- // Played does not become true for a short moment.
101101- playing = true;
102102- }
103103-10480 protected override void UpdateState()
10581 {
10682 playing = channel != 0 && Bass.ChannelIsActive(channel) != 0;
8383+10784 base.UpdateState();
1088510986 bassAmplitudeProcessor?.Update();