A game framework written with osu! in mind.
at master 121 lines 3.9 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 System.Diagnostics; 7using System.Runtime.InteropServices; 8using ManagedBass; 9using osu.Framework.Allocation; 10using osu.Framework.Audio.Mixing.Bass; 11using osu.Framework.Bindables; 12using osu.Framework.Platform; 13 14namespace osu.Framework.Audio.Sample 15{ 16 /// <summary> 17 /// A factory for <see cref="SampleBass"/> objects sharing a common sample ID (and thus playback concurrency). 18 /// </summary> 19 internal class SampleBassFactory : AudioCollectionManager<AdjustableAudioComponent> 20 { 21 public int SampleId { get; private set; } 22 23 public override bool IsLoaded => SampleId != 0; 24 25 public double Length { get; private set; } 26 27 /// <summary> 28 /// Todo: Expose this to support per-sample playback concurrency once ManagedBass has been updated (https://github.com/ManagedBass/ManagedBass/pull/85). 29 /// </summary> 30 internal readonly Bindable<int> PlaybackConcurrency = new Bindable<int>(Sample.DEFAULT_CONCURRENCY); 31 32 private readonly BassAudioMixer mixer; 33 34 private NativeMemoryTracker.NativeMemoryLease? memoryLease; 35 private byte[]? data; 36 37 public SampleBassFactory(byte[] data, BassAudioMixer mixer) 38 { 39 this.data = data; 40 this.mixer = mixer; 41 42 EnqueueAction(loadSample); 43 44 PlaybackConcurrency.BindValueChanged(updatePlaybackConcurrency); 45 } 46 47 private void updatePlaybackConcurrency(ValueChangedEvent<int> concurrency) 48 { 49 EnqueueAction(() => 50 { 51 // Broken in ManagedBass (https://github.com/ManagedBass/ManagedBass/pull/85). 52 // if (!IsLoaded) 53 // return; 54 // 55 // var sampleInfo = Bass.SampleGetInfo(SampleId); 56 // sampleInfo.Max = concurrency.NewValue; 57 // Bass.SampleSetInfo(SampleId, sampleInfo); 58 }); 59 } 60 61 internal override void UpdateDevice(int deviceIndex) 62 { 63 // The sample may not have already loaded if a device wasn't present in a previous load attempt. 64 if (!IsLoaded) 65 loadSample(); 66 } 67 68 private void loadSample() 69 { 70 Debug.Assert(CanPerformInline); 71 Debug.Assert(!IsLoaded); 72 73 if (data == null) 74 return; 75 76 int dataLength = data.Length; 77 78 const BassFlags flags = BassFlags.Default | BassFlags.SampleOverrideLongestPlaying; 79 using (var handle = new ObjectHandle<byte[]>(data, GCHandleType.Pinned)) 80 SampleId = Bass.SampleLoad(handle.Address, 0, dataLength, PlaybackConcurrency.Value, flags); 81 82 if (Bass.LastError == Errors.Init) 83 return; 84 85 // We've done as best as we could to init the sample. It may still have failed by some other cause (such as malformed data), but allow the GC to now clean up the locally-stored data. 86 data = null; 87 88 if (!IsLoaded) 89 return; 90 91 Length = Bass.ChannelBytes2Seconds(SampleId, dataLength) * 1000; 92 memoryLease = NativeMemoryTracker.AddMemory(this, dataLength); 93 } 94 95 public Sample CreateSample() => new SampleBass(this, mixer) { OnPlay = onPlay }; 96 97 private void onPlay(Sample sample) 98 { 99 AddItem(sample); 100 } 101 102 ~SampleBassFactory() 103 { 104 Dispose(false); 105 } 106 107 protected override void Dispose(bool disposing) 108 { 109 if (IsDisposed) 110 return; 111 112 if (IsLoaded) 113 { 114 Bass.SampleFree(SampleId); 115 memoryLease?.Dispose(); 116 } 117 118 base.Dispose(disposing); 119 } 120 } 121}