From 5fd89ba63946d9de6ed3a23048908c627b7eeccb Mon Sep 17 00:00:00 2001 From: hwsmm <9151706+hwsmm@users.noreply.github.com> Date: Sat, 14 Sep 2024 11:14:26 +0900 Subject: [PATCH] Satisfy CodeFactor and simplify mixing --- .../Audio/SDL3AudioTestComponents.cs | 4 +- .../Audio/Mixing/SDL3/SDL3AudioMixer.cs | 48 +- .../Audio/SDL3AudioDecoderManager.cs | 504 +++++++++--------- osu.Framework/Audio/SDL3AudioManager.cs | 292 +++++----- .../Audio/Sample/SampleSDL3Factory.cs | 6 +- .../Audio/Track/TempoSDL3AudioPlayer.cs | 4 +- osu.Framework/Audio/Track/TrackSDL3.cs | 6 +- osu.Framework/Audio/Track/Waveform.cs | 2 +- osu.Framework/Extensions/ExtensionMethods.cs | 2 +- 9 files changed, 424 insertions(+), 444 deletions(-) diff --git a/osu.Framework.Tests/Audio/SDL3AudioTestComponents.cs b/osu.Framework.Tests/Audio/SDL3AudioTestComponents.cs index 051e034e3c..041ee1f9f7 100644 --- a/osu.Framework.Tests/Audio/SDL3AudioTestComponents.cs +++ b/osu.Framework.Tests/Audio/SDL3AudioTestComponents.cs @@ -19,7 +19,7 @@ namespace osu.Framework.Tests.Audio /// public class SDL3AudioTestComponents : AudioTestComponents { - private SDL3BaseAudioManager baseManager = null!; + private SDL3AudioManager.SDL3BaseAudioManager baseManager = null!; public SDL3AudioTestComponents(bool init = true) : base(init) @@ -31,7 +31,7 @@ protected override void Prepare() base.Prepare(); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"u8); - baseManager = new SDL3BaseAudioManager(MixerComponents.Items.OfType); + baseManager = new SDL3AudioManager.SDL3BaseAudioManager(MixerComponents.Items.OfType); } public override void Init() diff --git a/osu.Framework/Audio/Mixing/SDL3/SDL3AudioMixer.cs b/osu.Framework/Audio/Mixing/SDL3/SDL3AudioMixer.cs index a5da4d17c8..f36c908dc1 100644 --- a/osu.Framework/Audio/Mixing/SDL3/SDL3AudioMixer.cs +++ b/osu.Framework/Audio/Mixing/SDL3/SDL3AudioMixer.cs @@ -7,6 +7,7 @@ using ManagedBass.Fx; using osu.Framework.Statistics; using NAudio.Dsp; +using System; namespace osu.Framework.Audio.Mixing.SDL3 { @@ -56,27 +57,16 @@ protected override void UpdateState() base.UpdateState(); } - private void mixAudio(float[] dst, float[] src, ref int filled, int samples, float left, float right) + private void mixAudio(float[] dst, float[] src, int samples, float left, float right) { if (left <= 0 && right <= 0) return; for (int e = 0; e < samples; e += 2) { - if (e < filled) - { - dst[e] += src[e] * left; - dst[e + 1] += src[e + 1] * right; - } - else - { - dst[e] = src[e] * left; - dst[e + 1] = src[e + 1] * right; - } + dst[e] += src[e] * left; + dst[e + 1] += src[e + 1] * right; } - - if (samples > filled) - filled = samples; } private float[]? ret; @@ -90,8 +80,7 @@ private void mixAudio(float[] dst, float[] src, ref int filled, int samples, flo /// /// A float array that audio will be mixed into. /// Size of data - /// Count of usable audio samples in data - public void MixChannelsInto(float[] data, int sampleCount, ref int filledSamples) + public void MixChannelsInto(float[] data, int sampleCount) { lock (syncRoot) { @@ -100,10 +89,13 @@ public void MixChannelsInto(float[] data, int sampleCount, ref int filledSamples bool useFilters = activeEffects.Count > 0; - if (useFilters && (filterArray == null || filterArray.Length != sampleCount)) - filterArray = new float[sampleCount]; + if (useFilters) + { + if (filterArray == null || filterArray.Length != sampleCount) + filterArray = new float[sampleCount]; - int filterArrayFilled = 0; + Array.Fill(filterArray, 0); + } var node = activeChannels.First; @@ -123,11 +115,7 @@ public void MixChannelsInto(float[] data, int sampleCount, ref int filledSamples if (size > 0) { var (left, right) = channel.Volume; - - if (!useFilters) - mixAudio(data, ret, ref filledSamples, size, left, right); - else - mixAudio(filterArray!, ret, ref filterArrayFilled, size, left, right); + mixAudio(useFilters ? filterArray! : data, ret, size, left, right); } } @@ -140,21 +128,13 @@ public void MixChannelsInto(float[] data, int sampleCount, ref int filledSamples { foreach (var filter in activeEffects.Values) { - for (int i = 0; i < filterArrayFilled; i++) + for (int i = 0; i < sampleCount; i++) filterArray![i] = filter.Transform(filterArray[i]); } - mixAudio(data, filterArray!, ref filledSamples, filterArrayFilled, 1, 1); + mixAudio(data, filterArray!, sampleCount, 1, 1); } } - - for (int i = 0; i < filledSamples; i++) - { - if (data[i] > 1.0f) - data[i] = 1.0f; - else if (data[i] < -1.0f) - data[i] = -1.0f; - } } private static BiQuadFilter updateFilter(BiQuadFilter? filter, float freq, BQFParameters bqfp) diff --git a/osu.Framework/Audio/SDL3AudioDecoderManager.cs b/osu.Framework/Audio/SDL3AudioDecoderManager.cs index e5d48ce8a7..20f88dee88 100644 --- a/osu.Framework/Audio/SDL3AudioDecoderManager.cs +++ b/osu.Framework/Audio/SDL3AudioDecoderManager.cs @@ -14,27 +14,27 @@ namespace osu.Framework.Audio { - public interface ISDL3AudioDataReceiver - { - /// - /// Interface to get decoded audio data from the decoder. - /// - /// Decoded audio. The format depends on you specified, - /// so you may need to actual data format. - /// This may be used by decoder later to reduce allocation, so you need to copy the data before exiting from this delegate, otherwise you may end up with wrong data. - /// Length in byte of decoded audio. Use this instead of data.Length - /// Whether if this is the last data or not. - void GetData(byte[] data, int length, bool done); - - void GetMetaData(int bitrate, double length, long byteLength); - } - /// /// Decodes audio from , and convert it to appropriate format. /// It needs a lot of polishing... /// public class SDL3AudioDecoderManager : IDisposable { + public interface ISDL3AudioDataReceiver + { + /// + /// Interface to get decoded audio data from the decoder. + /// + /// Decoded audio. The format depends on you specified, + /// so you may need to actual data format. + /// This may be used by decoder later to reduce allocation, so you need to copy the data before exiting from this delegate, otherwise you may end up with wrong data. + /// Length in byte of decoded audio. Use this instead of data.Length + /// Whether if this is the last data or not. + void GetData(byte[] data, int length, bool done); + + void GetMetaData(int bitrate, double length, long byteLength); + } + private readonly LinkedList jobs = new LinkedList(); private readonly Thread decoderThread; @@ -231,316 +231,316 @@ private static int decodeAudio(SDL3AudioDecoder decoder, out byte[] decoded) return (int)memoryStream.Length; } } - } - - /// - /// Contains decoder information, and perform the actual decoding. - /// - public abstract class SDL3AudioDecoder - { - /// - /// Decoder will decode audio data from this. - /// It accepts most formats. (e.g. MP3, OGG, WAV and so on...) - /// - internal readonly Stream Stream; - - /// - /// Decoder will convert audio data according to this spec if needed. - /// - internal readonly SDL_AudioSpec AudioSpec; - - /// - /// Decoder will call multiple times with partial data if true. - /// It's a receiver's job to combine the data in this case. Otherwise, It will call only once with the entirely decoded data if false. - /// - internal readonly bool IsTrack; - - /// - /// It will automatically dispose once decoding is done/failed. - /// - internal readonly bool AutoDisposeStream; - - /// - /// Decoder will call once or more to pass the decoded audio data. - /// - internal readonly ISDL3AudioDataReceiver? Pass; - - private int bitrate; - - /// - /// Audio bitrate. Decoder may fill this in after the first call of . - /// - public int Bitrate - { - get => bitrate; - set => Interlocked.Exchange(ref bitrate, value); - } - - private double length; /// - /// Audio length in milliseconds. Decoder may fill this in after the first call of . + /// Contains decoder information, and perform the actual decoding. /// - public double Length + public abstract class SDL3AudioDecoder { - get => length; - set => Interlocked.Exchange(ref length, value); - } - - private long byteLength; - - /// - /// Audio length in byte. Note that this may not be accurate. You cannot depend on this value entirely. - /// You can find out the actual byte length by summing up byte counts you received once decoding is done. - /// Decoder may fill this in after the first call of . - /// - public long ByteLength - { - get => byteLength; - set => Interlocked.Exchange(ref byteLength, value); - } - - internal bool MetadataSended; + /// + /// Decoder will decode audio data from this. + /// It accepts most formats. (e.g. MP3, OGG, WAV and so on...) + /// + internal readonly Stream Stream; + + /// + /// Decoder will convert audio data according to this spec if needed. + /// + internal readonly SDL_AudioSpec AudioSpec; + + /// + /// Decoder will call multiple times with partial data if true. + /// It's a receiver's job to combine the data in this case. Otherwise, It will call only once with the entirely decoded data if false. + /// + internal readonly bool IsTrack; + + /// + /// It will automatically dispose once decoding is done/failed. + /// + internal readonly bool AutoDisposeStream; + + /// + /// Decoder will call once or more to pass the decoded audio data. + /// + internal readonly ISDL3AudioDataReceiver? Pass; + + private int bitrate; + + /// + /// Audio bitrate. Decoder may fill this in after the first call of . + /// + public int Bitrate + { + get => bitrate; + set => Interlocked.Exchange(ref bitrate, value); + } - internal volatile bool StopJob; + private double length; - private volatile bool loading; + /// + /// Audio length in milliseconds. Decoder may fill this in after the first call of . + /// + public double Length + { + get => length; + set => Interlocked.Exchange(ref length, value); + } - /// - /// Whether it is decoding or not. - /// - public bool Loading { get => loading; protected set => loading = value; } + private long byteLength; - protected SDL3AudioDecoder(Stream stream, SDL_AudioSpec audioSpec, bool isTrack, bool autoDisposeStream, ISDL3AudioDataReceiver? pass) - { - Stream = stream; - AudioSpec = audioSpec; - IsTrack = isTrack; - AutoDisposeStream = autoDisposeStream; - Pass = pass; - } + /// + /// Audio length in byte. Note that this may not be accurate. You cannot depend on this value entirely. + /// You can find out the actual byte length by summing up byte counts you received once decoding is done. + /// Decoder may fill this in after the first call of . + /// + public long ByteLength + { + get => byteLength; + set => Interlocked.Exchange(ref byteLength, value); + } - /// - /// Add a flag to stop decoding in the next loop of decoder thread. - /// - public void Stop() - { - StopJob = true; - } + internal bool MetadataSended; - // Not using IDisposable since things must be handled in a decoder thread - internal virtual void Dispose() - { - if (AutoDisposeStream) - Stream.Dispose(); - } + internal volatile bool StopJob; - protected abstract int LoadFromStreamInternal(out byte[] decoded); + private volatile bool loading; - /// - /// Decodes and resamples audio from job.Stream, and pass it to decoded. - /// You may need to run this multiple times. - /// Don't call this yourself if this decoder is in the decoder thread job list. - /// - /// Decoded audio - public int LoadFromStream(out byte[] decoded) - { - int read = 0; + /// + /// Whether it is decoding or not. + /// + public bool Loading { get => loading; protected set => loading = value; } - try + protected SDL3AudioDecoder(Stream stream, SDL_AudioSpec audioSpec, bool isTrack, bool autoDisposeStream, ISDL3AudioDataReceiver? pass) { - read = LoadFromStreamInternal(out decoded); + Stream = stream; + AudioSpec = audioSpec; + IsTrack = isTrack; + AutoDisposeStream = autoDisposeStream; + Pass = pass; } - catch (Exception e) + + /// + /// Add a flag to stop decoding in the next loop of decoder thread. + /// + public void Stop() { - Logger.Log(e.Message, level: LogLevel.Important); - Loading = false; - decoded = Array.Empty(); + StopJob = true; } - finally + + // Not using IDisposable since things must be handled in a decoder thread + internal virtual void Dispose() { - if (!Loading) - Dispose(); + if (AutoDisposeStream) + Stream.Dispose(); } - return read; - } + protected abstract int LoadFromStreamInternal(out byte[] decoded); - /// - /// This is only for using BASS as a decoder for SDL3 backend! - /// - internal class BassAudioDecoder : SDL3AudioDecoder - { - private int decodeStream; - private FileCallbacks? fileCallbacks; - - private int resampler; - - private byte[]? decodeData; - - private Resolution resolution + /// + /// Decodes and resamples audio from job.Stream, and pass it to decoded. + /// You may need to run this multiple times. + /// Don't call this yourself if this decoder is in the decoder thread job list. + /// + /// Decoded audio + public int LoadFromStream(out byte[] decoded) { - get + int read = 0; + + try { - if (AudioSpec.format == SDL_AudioFormat.SDL_AUDIO_S8) - return Resolution.Byte; - else if (AudioSpec.format == SDL3.SDL_AUDIO_S16) // uses constant due to endian - return Resolution.Short; - else - return Resolution.Float; + read = LoadFromStreamInternal(out decoded); + } + catch (Exception e) + { + Logger.Log(e.Message, level: LogLevel.Important); + Loading = false; + decoded = Array.Empty(); + } + finally + { + if (!Loading) + Dispose(); } - } - - private ushort bits => (ushort)SDL3.SDL_AUDIO_BITSIZE(AudioSpec.format); - public BassAudioDecoder(Stream stream, SDL_AudioSpec audioSpec, bool isTrack, bool autoDisposeStream, ISDL3AudioDataReceiver? pass) - : base(stream, audioSpec, isTrack, autoDisposeStream, pass) - { + return read; } - internal override void Dispose() + /// + /// This is only for using BASS as a decoder for SDL3 backend! + /// + internal class BassAudioDecoder : SDL3AudioDecoder { - fileCallbacks?.Dispose(); - fileCallbacks = null; + private int decodeStream; + private FileCallbacks? fileCallbacks; - decodeData = null; + private int resampler; - if (resampler != 0) + private byte[]? decodeData; + + private Resolution resolution { - Bass.StreamFree(resampler); - resampler = 0; + get + { + if (AudioSpec.format == SDL_AudioFormat.SDL_AUDIO_S8) + return Resolution.Byte; + else if (AudioSpec.format == SDL3.SDL_AUDIO_S16) // uses constant due to endian + return Resolution.Short; + else + return Resolution.Float; + } } - if (decodeStream != 0) + private ushort bits => (ushort)SDL3.SDL_AUDIO_BITSIZE(AudioSpec.format); + + public BassAudioDecoder(Stream stream, SDL_AudioSpec audioSpec, bool isTrack, bool autoDisposeStream, ISDL3AudioDataReceiver? pass) + : base(stream, audioSpec, isTrack, autoDisposeStream, pass) { - Bass.StreamFree(decodeStream); - decodeStream = 0; } - base.Dispose(); - } + internal override void Dispose() + { + fileCallbacks?.Dispose(); + fileCallbacks = null; - protected override int LoadFromStreamInternal(out byte[] decoded) - { - if (Bass.CurrentDevice < 0) - throw new InvalidOperationException($"Initialize a BASS device to decode audio: {Bass.LastError}"); + decodeData = null; - if (!Loading) - { - fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(Stream)); + if (resampler != 0) + { + Bass.StreamFree(resampler); + resampler = 0; + } - BassFlags bassFlags = BassFlags.Decode | resolution.ToBassFlag(); - if (IsTrack) bassFlags |= BassFlags.Prescan; + if (decodeStream != 0) + { + Bass.StreamFree(decodeStream); + decodeStream = 0; + } - decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, bassFlags, fileCallbacks.Callbacks); + base.Dispose(); + } - if (decodeStream == 0) - throw new FormatException($"Couldn't create stream: {Bass.LastError}"); + protected override int LoadFromStreamInternal(out byte[] decoded) + { + if (Bass.CurrentDevice < 0) + throw new InvalidOperationException($"Initialize a BASS device to decode audio: {Bass.LastError}"); - if (Bass.ChannelGetInfo(decodeStream, out var info)) + if (!Loading) { - ByteLength = Bass.ChannelGetLength(decodeStream); - Length = Bass.ChannelBytes2Seconds(decodeStream, ByteLength) * 1000.0d; - Bitrate = (int)Math.Round(Bass.ChannelGetAttribute(decodeStream, ChannelAttribute.Bitrate)); + fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(Stream)); + + BassFlags bassFlags = BassFlags.Decode | resolution.ToBassFlag(); + if (IsTrack) bassFlags |= BassFlags.Prescan; + + decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, bassFlags, fileCallbacks.Callbacks); - if (info.Channels != AudioSpec.channels || info.Frequency != AudioSpec.freq) + if (decodeStream == 0) + throw new FormatException($"Couldn't create stream: {Bass.LastError}"); + + if (Bass.ChannelGetInfo(decodeStream, out var info)) { - resampler = BassMix.CreateMixerStream(AudioSpec.freq, AudioSpec.channels, BassFlags.MixerEnd | BassFlags.Decode | resolution.ToBassFlag()); + ByteLength = Bass.ChannelGetLength(decodeStream); + Length = Bass.ChannelBytes2Seconds(decodeStream, ByteLength) * 1000.0d; + Bitrate = (int)Math.Round(Bass.ChannelGetAttribute(decodeStream, ChannelAttribute.Bitrate)); + + if (info.Channels != AudioSpec.channels || info.Frequency != AudioSpec.freq) + { + resampler = BassMix.CreateMixerStream(AudioSpec.freq, AudioSpec.channels, BassFlags.MixerEnd | BassFlags.Decode | resolution.ToBassFlag()); - if (resampler == 0) - throw new FormatException($"Failed to create BASS Mixer: {Bass.LastError}"); + if (resampler == 0) + throw new FormatException($"Failed to create BASS Mixer: {Bass.LastError}"); - if (!BassMix.MixerAddChannel(resampler, decodeStream, BassFlags.MixerChanNoRampin | BassFlags.MixerChanLimit)) - throw new FormatException($"Failed to add a channel to BASS Mixer: {Bass.LastError}"); + if (!BassMix.MixerAddChannel(resampler, decodeStream, BassFlags.MixerChanNoRampin | BassFlags.MixerChanLimit)) + throw new FormatException($"Failed to add a channel to BASS Mixer: {Bass.LastError}"); - ByteLength /= info.Channels * (bits / 8); - ByteLength = (long)Math.Ceiling((decimal)ByteLength / info.Frequency * AudioSpec.freq); - ByteLength *= AudioSpec.channels * (bits / 8); + ByteLength /= info.Channels * (bits / 8); + ByteLength = (long)Math.Ceiling((decimal)ByteLength / info.Frequency * AudioSpec.freq); + ByteLength *= AudioSpec.channels * (bits / 8); + } } - } - else - { - if (IsTrack) - throw new FormatException($"Couldn't get channel info: {Bass.LastError}"); + else + { + if (IsTrack) + throw new FormatException($"Couldn't get channel info: {Bass.LastError}"); + } + + Loading = true; } - Loading = true; - } + int handle = resampler == 0 ? decodeStream : resampler; - int handle = resampler == 0 ? decodeStream : resampler; + int bufferLen = (int)Bass.ChannelSeconds2Bytes(handle, 1); - int bufferLen = (int)Bass.ChannelSeconds2Bytes(handle, 1); + if (bufferLen <= 0) + bufferLen = 44100 * 2 * 4 * 1; - if (bufferLen <= 0) - bufferLen = 44100 * 2 * 4 * 1; + if (decodeData == null || decodeData.Length < bufferLen) + decodeData = new byte[bufferLen]; - if (decodeData == null || decodeData.Length < bufferLen) - decodeData = new byte[bufferLen]; + int got = Bass.ChannelGetData(handle, decodeData, bufferLen); - int got = Bass.ChannelGetData(handle, decodeData, bufferLen); + if (got == -1) + { + Loading = false; - if (got == -1) - { - Loading = false; + if (Bass.LastError != Errors.Ended) + throw new FormatException($"Couldn't decode: {Bass.LastError}"); + } + else if (got < bufferLen) + { + // originally used synchandle to detect end, but it somehow created strong handle + Loading = false; + } - if (Bass.LastError != Errors.Ended) - throw new FormatException($"Couldn't decode: {Bass.LastError}"); - } - else if (got < bufferLen) - { - // originally used synchandle to detect end, but it somehow created strong handle - Loading = false; + decoded = decodeData; + return Math.Max(0, got); } - - decoded = decodeData; - return Math.Max(0, got); } - } - internal class FFmpegAudioDecoder : SDL3AudioDecoder - { - private VideoDecoder? ffmpeg; - private byte[]? decodeData; - - public FFmpegAudioDecoder(Stream stream, SDL_AudioSpec audioSpec, bool isTrack, bool autoDisposeStream, ISDL3AudioDataReceiver? pass) - : base(stream, audioSpec, isTrack, autoDisposeStream, pass) + internal class FFmpegAudioDecoder : SDL3AudioDecoder { - } + private VideoDecoder? ffmpeg; + private byte[]? decodeData; - internal override void Dispose() - { - decodeData = null; + public FFmpegAudioDecoder(Stream stream, SDL_AudioSpec audioSpec, bool isTrack, bool autoDisposeStream, ISDL3AudioDataReceiver? pass) + : base(stream, audioSpec, isTrack, autoDisposeStream, pass) + { + } - ffmpeg?.Dispose(); - ffmpeg = null; + internal override void Dispose() + { + decodeData = null; - base.Dispose(); - } + ffmpeg?.Dispose(); + ffmpeg = null; - protected override int LoadFromStreamInternal(out byte[] decoded) - { - if (ffmpeg == null) + base.Dispose(); + } + + protected override int LoadFromStreamInternal(out byte[] decoded) { - ffmpeg = new VideoDecoder(Stream, AudioSpec.freq, AudioSpec.channels, - SDL3.SDL_AUDIO_ISFLOAT(AudioSpec.format), SDL3.SDL_AUDIO_BITSIZE(AudioSpec.format), SDL3.SDL_AUDIO_ISSIGNED(AudioSpec.format)); + if (ffmpeg == null) + { + ffmpeg = new VideoDecoder(Stream, AudioSpec.freq, AudioSpec.channels, + SDL3.SDL_AUDIO_ISFLOAT(AudioSpec.format), SDL3.SDL_AUDIO_BITSIZE(AudioSpec.format), SDL3.SDL_AUDIO_ISSIGNED(AudioSpec.format)); - ffmpeg.PrepareDecoding(); - ffmpeg.RecreateCodecContext(); + ffmpeg.PrepareDecoding(); + ffmpeg.RecreateCodecContext(); - Bitrate = (int)ffmpeg.Bitrate; - Length = ffmpeg.Duration; - ByteLength = (long)Math.Ceiling(ffmpeg.Duration / 1000.0d * AudioSpec.freq) * AudioSpec.channels * (SDL3.SDL_AUDIO_BITSIZE(AudioSpec.format) / 8); // FIXME + Bitrate = (int)ffmpeg.Bitrate; + Length = ffmpeg.Duration; + ByteLength = (long)Math.Ceiling(ffmpeg.Duration / 1000.0d * AudioSpec.freq) * AudioSpec.channels * (SDL3.SDL_AUDIO_BITSIZE(AudioSpec.format) / 8); // FIXME - Loading = true; - } + Loading = true; + } - int got = ffmpeg.DecodeNextAudioFrame(32, ref decodeData, !IsTrack); + int got = ffmpeg.DecodeNextAudioFrame(32, ref decodeData, !IsTrack); - if (ffmpeg.State != VideoDecoder.DecoderState.Running) - Loading = false; + if (ffmpeg.State != VideoDecoder.DecoderState.Running) + Loading = false; - decoded = decodeData; - return got; + decoded = decodeData; + return got; + } } } } diff --git a/osu.Framework/Audio/SDL3AudioManager.cs b/osu.Framework/Audio/SDL3AudioManager.cs index 1cc044c7ca..d7eafa1c42 100644 --- a/osu.Framework/Audio/SDL3AudioManager.cs +++ b/osu.Framework/Audio/SDL3AudioManager.cs @@ -190,202 +190,202 @@ protected override void Dispose(bool disposing) baseManager.Dispose(); } - } - - /// - /// To share basic playback logic with audio tests. - /// - internal unsafe class SDL3BaseAudioManager : IDisposable - { - internal SDL_AudioSpec AudioSpec { get; private set; } - internal SDL_AudioDeviceID DeviceId { get; private set; } - internal SDL_AudioStream* DeviceStream { get; private set; } - - internal int BufferSize { get; private set; } = (int)(SDL3AudioManager.AUDIO_FREQ * 0.01); + /// + /// To share basic playback logic with audio tests. + /// + internal unsafe class SDL3BaseAudioManager : IDisposable + { + internal SDL_AudioSpec AudioSpec { get; private set; } - internal string DeviceName { get; private set; } = "Not loaded"; + internal SDL_AudioDeviceID DeviceId { get; private set; } + internal SDL_AudioStream* DeviceStream { get; private set; } - private readonly Func> mixerIterator; + internal int BufferSize { get; private set; } = (int)(AUDIO_FREQ * 0.01); - private ObjectHandle objectHandle; + internal string DeviceName { get; private set; } = "Not loaded"; - private readonly SDL3AudioDecoderManager decoderManager = new SDL3AudioDecoderManager(); + private readonly Func> mixerIterator; - internal SDL3BaseAudioManager(Func> mixerIterator) - { - if (SDL_InitSubSystem(SDL_InitFlags.SDL_INIT_AUDIO) < 0) - { - throw new InvalidOperationException($"Failed to initialise SDL Audio: {SDL_GetError()}"); - } + private ObjectHandle objectHandle; - this.mixerIterator = mixerIterator; + private readonly SDL3AudioDecoderManager decoderManager = new SDL3AudioDecoderManager(); - objectHandle = new ObjectHandle(this, GCHandleType.Normal); - AudioSpec = new SDL_AudioSpec + internal SDL3BaseAudioManager(Func> mixerIterator) { - freq = SDL3AudioManager.AUDIO_FREQ, - channels = SDL3AudioManager.AUDIO_CHANNELS, - format = SDL3AudioManager.AUDIO_FORMAT - }; - } - - internal void RunWhileLockingAudioStream(Action action) - { - SDL_AudioStream* stream = DeviceStream; + if (SDL_InitSubSystem(SDL_InitFlags.SDL_INIT_AUDIO) < 0) + { + throw new InvalidOperationException($"Failed to initialise SDL Audio: {SDL_GetError()}"); + } - if (stream != null) - SDL_LockAudioStream(stream); + this.mixerIterator = mixerIterator; - try - { - action(); + objectHandle = new ObjectHandle(this, GCHandleType.Normal); + AudioSpec = new SDL_AudioSpec + { + freq = AUDIO_FREQ, + channels = AUDIO_CHANNELS, + format = AUDIO_FORMAT + }; } - finally + + internal void RunWhileLockingAudioStream(Action action) { + SDL_AudioStream* stream = DeviceStream; + if (stream != null) - SDL_UnlockAudioStream(stream); + SDL_LockAudioStream(stream); + + try + { + action(); + } + finally + { + if (stream != null) + SDL_UnlockAudioStream(stream); + } } - } - internal bool SetAudioDevice(SDL_AudioDeviceID targetId) - { - if (DeviceStream != null) + internal bool SetAudioDevice(SDL_AudioDeviceID targetId) { - SDL_DestroyAudioStream(DeviceStream); - DeviceStream = null; - } + if (DeviceStream != null) + { + SDL_DestroyAudioStream(DeviceStream); + DeviceStream = null; + } - SDL_AudioSpec spec = AudioSpec; + SDL_AudioSpec spec = AudioSpec; - SDL_AudioStream* deviceStream = SDL_OpenAudioDeviceStream(targetId, &spec, &audioCallback, objectHandle.Handle); + SDL_AudioStream* deviceStream = SDL_OpenAudioDeviceStream(targetId, &spec, &audioCallback, objectHandle.Handle); - if (deviceStream != null) - { - SDL_DestroyAudioStream(DeviceStream); - DeviceStream = deviceStream; - AudioSpec = spec; + if (deviceStream != null) + { + SDL_DestroyAudioStream(DeviceStream); + DeviceStream = deviceStream; + AudioSpec = spec; - DeviceId = SDL_GetAudioStreamDevice(deviceStream); + DeviceId = SDL_GetAudioStreamDevice(deviceStream); - int sampleFrameSize = 0; - SDL_AudioSpec temp; // this has 'real' device info which is useless since SDL converts audio according to the spec we provided - if (SDL_GetAudioDeviceFormat(DeviceId, &temp, &sampleFrameSize) == 0) - BufferSize = sampleFrameSize * (int)Math.Ceiling((double)spec.freq / temp.freq); - } + int sampleFrameSize = 0; + SDL_AudioSpec temp; // this has 'real' device info which is useless since SDL converts audio according to the spec we provided + if (SDL_GetAudioDeviceFormat(DeviceId, &temp, &sampleFrameSize) == 0) + BufferSize = sampleFrameSize * (int)Math.Ceiling((double)spec.freq / temp.freq); + } - if (deviceStream == null) - { - if (targetId == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) - return false; + if (deviceStream == null) + { + if (targetId == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) + return false; - return SetAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK); - } + return SetAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK); + } - SDL_ResumeAudioDevice(DeviceId); + SDL_ResumeAudioDevice(DeviceId); - DeviceName = SDL_GetAudioDeviceName(targetId); + DeviceName = SDL_GetAudioDeviceName(targetId); - return true; - } + return true; + } - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void audioCallback(IntPtr userdata, SDL_AudioStream* stream, int additionalAmount, int totalAmount) - { - var handle = new ObjectHandle(userdata); - if (handle.GetTarget(out SDL3BaseAudioManager audioManager)) - audioManager.internalAudioCallback(stream, additionalAmount); - } + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void audioCallback(IntPtr userdata, SDL_AudioStream* stream, int additionalAmount, int totalAmount) + { + var handle = new ObjectHandle(userdata); + if (handle.GetTarget(out SDL3BaseAudioManager audioManager)) + audioManager.internalAudioCallback(stream, additionalAmount); + } - private float[] audioBuffer; + private float[] audioBuffer; - private void internalAudioCallback(SDL_AudioStream* stream, int additionalAmount) - { - additionalAmount /= 4; + private void internalAudioCallback(SDL_AudioStream* stream, int additionalAmount) + { + additionalAmount /= 4; - if (audioBuffer == null || audioBuffer.Length < additionalAmount) - audioBuffer = new float[additionalAmount]; + if (audioBuffer == null || audioBuffer.Length < additionalAmount) + audioBuffer = new float[additionalAmount]; - try - { - int filled = 0; + Array.Fill(audioBuffer, 0); - foreach (var mixer in mixerIterator()) + try { - if (mixer.IsAlive) - mixer.MixChannelsInto(audioBuffer, additionalAmount, ref filled); + foreach (var mixer in mixerIterator()) + { + if (mixer.IsAlive) + mixer.MixChannelsInto(audioBuffer, additionalAmount); + } + + fixed (float* ptr = audioBuffer) + SDL_PutAudioStreamData(stream, (IntPtr)ptr, additionalAmount * 4); + } + catch (Exception e) + { + Logger.Error(e, "Error while pushing audio to SDL"); } - - fixed (float* ptr = audioBuffer) - SDL_PutAudioStreamData(stream, (IntPtr)ptr, filled * 4); } - catch (Exception e) + + /// + /// With how decoders work, we need this to get test passed + /// I don't want this either... otherwise we have to dispose decoder in tests + /// + private class ReceiverGCWrapper : SDL3AudioDecoderManager.ISDL3AudioDataReceiver { - Logger.Error(e, "Error while pushing audio to SDL"); - } - } + private readonly WeakReference channelWeakReference; - /// - /// With how decoders work, we need this to get test passed - /// I don't want this either... otherwise we have to dispose decoder in tests - /// - private class ReceiverGCWrapper : ISDL3AudioDataReceiver - { - private readonly WeakReference channelWeakReference; + internal ReceiverGCWrapper(WeakReference channel) + { + channelWeakReference = channel; + } - internal ReceiverGCWrapper(WeakReference channel) - { - channelWeakReference = channel; + void SDL3AudioDecoderManager.ISDL3AudioDataReceiver.GetData(byte[] data, int length, bool done) + { + if (channelWeakReference.TryGetTarget(out SDL3AudioDecoderManager.ISDL3AudioDataReceiver r)) + r.GetData(data, length, done); + else + throw new ObjectDisposedException("channel is already disposed"); + } + + void SDL3AudioDecoderManager.ISDL3AudioDataReceiver.GetMetaData(int bitrate, double length, long byteLength) + { + if (channelWeakReference.TryGetTarget(out SDL3AudioDecoderManager.ISDL3AudioDataReceiver r)) + r.GetMetaData(bitrate, length, byteLength); + else + throw new ObjectDisposedException("channel is already disposed"); + } } - void ISDL3AudioDataReceiver.GetData(byte[] data, int length, bool done) + internal Track.Track GetNewTrack(Stream data, string name) { - if (channelWeakReference.TryGetTarget(out ISDL3AudioDataReceiver r)) - r.GetData(data, length, done); - else - throw new ObjectDisposedException("channel is already disposed"); + TrackSDL3 track = new TrackSDL3(name, AudioSpec, BufferSize); + ReceiverGCWrapper receiverGC = new ReceiverGCWrapper(new WeakReference(track)); + decoderManager.StartDecodingAsync(data, AudioSpec, true, receiverGC); + return track; } - void ISDL3AudioDataReceiver.GetMetaData(int bitrate, double length, long byteLength) + internal SampleFactory GetSampleFactory(Stream data, string name, AudioMixer mixer, int playbackConcurrency) { - if (channelWeakReference.TryGetTarget(out ISDL3AudioDataReceiver r)) - r.GetMetaData(bitrate, length, byteLength); - else - throw new ObjectDisposedException("channel is already disposed"); + SampleSDL3Factory sampleFactory = new SampleSDL3Factory(name, (SDL3AudioMixer)mixer, playbackConcurrency, AudioSpec); + ReceiverGCWrapper receiverGC = new ReceiverGCWrapper(new WeakReference(sampleFactory)); + decoderManager.StartDecodingAsync(data, AudioSpec, false, receiverGC); + return sampleFactory; } - } - internal Track.Track GetNewTrack(Stream data, string name) - { - TrackSDL3 track = new TrackSDL3(name, AudioSpec, BufferSize); - ReceiverGCWrapper receiverGC = new ReceiverGCWrapper(new WeakReference(track)); - decoderManager.StartDecodingAsync(data, AudioSpec, true, receiverGC); - return track; - } - - internal SampleFactory GetSampleFactory(Stream data, string name, AudioMixer mixer, int playbackConcurrency) - { - SampleSDL3Factory sampleFactory = new SampleSDL3Factory(name, (SDL3AudioMixer)mixer, playbackConcurrency, AudioSpec); - ReceiverGCWrapper receiverGC = new ReceiverGCWrapper(new WeakReference(sampleFactory)); - decoderManager.StartDecodingAsync(data, AudioSpec, false, receiverGC); - return sampleFactory; - } - - public void Dispose() - { - if (DeviceStream != null) + public void Dispose() { - SDL_DestroyAudioStream(DeviceStream); - DeviceStream = null; - DeviceId = 0; - // Destroying audio stream will close audio device because we use SDL3 OpenAudioDeviceStream - // won't use multiple AudioStream for now since it's barely useful - } + if (DeviceStream != null) + { + SDL_DestroyAudioStream(DeviceStream); + DeviceStream = null; + DeviceId = 0; + // Destroying audio stream will close audio device because we use SDL3 OpenAudioDeviceStream + // won't use multiple AudioStream for now since it's barely useful + } - objectHandle.Dispose(); - decoderManager.Dispose(); + objectHandle.Dispose(); + decoderManager.Dispose(); - SDL_QuitSubSystem(SDL_InitFlags.SDL_INIT_AUDIO); + SDL_QuitSubSystem(SDL_InitFlags.SDL_INIT_AUDIO); + } } } } diff --git a/osu.Framework/Audio/Sample/SampleSDL3Factory.cs b/osu.Framework/Audio/Sample/SampleSDL3Factory.cs index 670713cae9..0d1d59341b 100644 --- a/osu.Framework/Audio/Sample/SampleSDL3Factory.cs +++ b/osu.Framework/Audio/Sample/SampleSDL3Factory.cs @@ -9,7 +9,7 @@ namespace osu.Framework.Audio.Sample { - internal class SampleSDL3Factory : SampleFactory, ISDL3AudioDataReceiver + internal class SampleSDL3Factory : SampleFactory, SDL3AudioDecoderManager.ISDL3AudioDataReceiver { private volatile bool isLoaded; public override bool IsLoaded => isLoaded; @@ -28,7 +28,7 @@ public SampleSDL3Factory(string name, SDL3AudioMixer mixer, int playbackConcurre this.spec = spec; } - void ISDL3AudioDataReceiver.GetData(byte[] audio, int byteLen, bool done) + void SDL3AudioDecoderManager.ISDL3AudioDataReceiver.GetData(byte[] audio, int byteLen, bool done) { if (IsDisposed) return; @@ -75,7 +75,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - void ISDL3AudioDataReceiver.GetMetaData(int bitrate, double length, long byteLength) + void SDL3AudioDecoderManager.ISDL3AudioDataReceiver.GetMetaData(int bitrate, double length, long byteLength) { } // not needed } diff --git a/osu.Framework/Audio/Track/TempoSDL3AudioPlayer.cs b/osu.Framework/Audio/Track/TempoSDL3AudioPlayer.cs index 80fb49d094..6472cac0f0 100644 --- a/osu.Framework/Audio/Track/TempoSDL3AudioPlayer.cs +++ b/osu.Framework/Audio/Track/TempoSDL3AudioPlayer.cs @@ -155,10 +155,10 @@ protected int GetTempoLatencyInSamples() if (soundTouch == null) return 0; - return (int)(soundTouch.UnprocessedSampleCount + soundTouch.AvailableSamples * Tempo); + return (int)(soundTouch.UnprocessedSampleCount + (soundTouch.AvailableSamples * Tempo)); } - protected override double GetProcessingLatency() => base.GetProcessingLatency() + (double)GetTempoLatencyInSamples() / SrcRate * 1000.0d; + protected override double GetProcessingLatency() => base.GetProcessingLatency() + (GetTempoLatencyInSamples() * 1000.0 / SrcRate); public override void Clear() { diff --git a/osu.Framework/Audio/Track/TrackSDL3.cs b/osu.Framework/Audio/Track/TrackSDL3.cs index 0391f61692..3f5b233a3a 100644 --- a/osu.Framework/Audio/Track/TrackSDL3.cs +++ b/osu.Framework/Audio/Track/TrackSDL3.cs @@ -11,7 +11,7 @@ namespace osu.Framework.Audio.Track { - public sealed class TrackSDL3 : Track, ISDL3AudioChannel, ISDL3AudioDataReceiver + public sealed class TrackSDL3 : Track, ISDL3AudioChannel, SDL3AudioDecoderManager.ISDL3AudioDataReceiver { private readonly TempoSDL3AudioPlayer player; @@ -55,7 +55,7 @@ public TrackSDL3(string name, SDL_AudioSpec spec, int samples) private readonly object syncRoot = new object(); - void ISDL3AudioDataReceiver.GetData(byte[] audio, int length, bool done) + void SDL3AudioDecoderManager.ISDL3AudioDataReceiver.GetData(byte[] audio, int length, bool done) { if (IsDisposed) return; @@ -82,7 +82,7 @@ void ISDL3AudioDataReceiver.GetData(byte[] audio, int length, bool done) } } - void ISDL3AudioDataReceiver.GetMetaData(int bitrate, double length, long byteLength) + void SDL3AudioDecoderManager.ISDL3AudioDataReceiver.GetMetaData(int bitrate, double length, long byteLength) { if (!isLoaded) { diff --git a/osu.Framework/Audio/Track/Waveform.cs b/osu.Framework/Audio/Track/Waveform.cs index b2a3436cd3..428ec5ae36 100644 --- a/osu.Framework/Audio/Track/Waveform.cs +++ b/osu.Framework/Audio/Track/Waveform.cs @@ -96,7 +96,7 @@ public Waveform(Stream? data) }; // AudioDecoder will resample data into specified sample rate and channels (44100hz 2ch float) - SDL3AudioDecoder decoder = SDL3AudioDecoderManager.CreateDecoder(data, spec, true, false); + SDL3AudioDecoderManager.SDL3AudioDecoder decoder = SDL3AudioDecoderManager.CreateDecoder(data, spec, true, false); Complex[] complexBuffer = ArrayPool.Shared.Rent(fft_samples); diff --git a/osu.Framework/Extensions/ExtensionMethods.cs b/osu.Framework/Extensions/ExtensionMethods.cs index cb8b00f209..4add4d1e61 100644 --- a/osu.Framework/Extensions/ExtensionMethods.cs +++ b/osu.Framework/Extensions/ExtensionMethods.cs @@ -343,6 +343,6 @@ public static bool CheckIsValidUrl(this string url) /// /// NAudio Complex number /// Magnitude (Absolute number) of a given complex. - public static float ComputeMagnitude(this Complex complex) => (float)Math.Sqrt(complex.X * complex.X + complex.Y * complex.Y); + public static float ComputeMagnitude(this Complex complex) => (float)Math.Sqrt((complex.X * complex.X) + (complex.Y * complex.Y)); } }