<commit_before>// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.IO;
using System.Threading;
using ManagedBass;
using ManagedBass.Fx;
using osuTK;
using osu.Framework.IO;
using System.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Audio.Callbacks;

namespace osu.Framework.Audio.Track
{
    public sealed class TrackBass : Track, IBassAudio, IHasPitchAdjust
    {
        public const int BYTES_PER_SAMPLE = 4;

        private AsyncBufferStream dataStream;

        /// <summary>
        /// Should this track only be used for preview purposes? This suggests it has not yet been fully loaded.
        /// </summary>
        public bool Preview { get; private set; }

        /// <summary>
        /// The handle for this track, if there is one.
        /// </summary>
        private int activeStream;

        /// <summary>
        /// The handle for adjusting tempo.
        /// </summary>
        private int tempoAdjustStream;

        /// <summary>
        /// This marks if the track is paused, or stopped to the end.
        /// </summary>
        private bool isPlayed;

        private long byteLength;

        /// <summary>
        /// The last position that a seek will succeed for.
        /// </summary>
        private double lastSeekablePosition;

        private FileCallbacks fileCallbacks;
        private SyncCallback stopCallback;
        private SyncCallback endCallback;

        private volatile bool isLoaded;

        public override bool IsLoaded => isLoaded;

        /// <summary>
        /// Constructs a new <see cref="TrackBass"/> from provided audio data.
        /// </summary>
        /// <param name="data">The sample data stream.</param>
        /// <param name="quick">If true, the track will not be fully loaded, and should only be used for preview purposes.  Defaults to false.</param>
        public TrackBass(Stream data, bool quick = false)
        {
            if (data == null)
                throw new ArgumentNullException(nameof(data));

            // todo: support this internally to match the underlying Track implementation (which can support this).
            const float tempo_minimum_supported = 0.05f;

            Tempo.ValueChanged += t =>
            {
                if (t.NewValue < tempo_minimum_supported)
                    throw new ArgumentException($"{nameof(TrackBass)} does not support {nameof(Tempo)} specifications below {tempo_minimum_supported}. Use {nameof(Frequency)} instead.");
            };

            EnqueueAction(() =>
            {
                Preview = quick;

                //encapsulate incoming stream with async buffer if it isn't already.
                dataStream = data as AsyncBufferStream ?? new AsyncBufferStream(data, quick ? 8 : -1);

                fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(dataStream));

                BassFlags flags = Preview ? 0 : BassFlags.Decode | BassFlags.Prescan;
                activeStream = Bass.CreateStream(StreamSystem.NoBuffer, flags, fileCallbacks.Callbacks, fileCallbacks.Handle);

                if (!Preview)
                {
                    // We assign the BassFlags.Decode streams to the device "bass_nodevice" to prevent them from getting
                    // cleaned up during a Bass.Free call. This is necessary for seamless switching between audio devices.
                    // Further, we provide the flag BassFlags.FxFreeSource such that freeing the activeStream also frees
                    // all parent decoding streams.
                    const int bass_nodevice = 0x20000;

                    Bass.ChannelSetDevice(activeStream, bass_nodevice);
                    tempoAdjustStream = BassFx.TempoCreate(activeStream, BassFlags.Decode | BassFlags.FxFreeSource);
                    Bass.ChannelSetDevice(tempoAdjustStream, bass_nodevice);
                    activeStream = BassFx.ReverseCreate(tempoAdjustStream, 5f, BassFlags.Default | BassFlags.FxFreeSource);

                    Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoUseQuickAlgorithm, 1);
                    Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoOverlapMilliseconds, 4);
                    Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoSequenceMilliseconds, 30);
                }

                // will be -1 in case of an error
                double seconds = Bass.ChannelBytes2Seconds(activeStream, byteLength = Bass.ChannelGetLength(activeStream));

                bool success = seconds >= 0;

                if (success)
                {
                    Length = seconds * 1000;

                    // Bass does not allow seeking to the end of the track, so the last available position is 1 sample before.
                    lastSeekablePosition = Bass.ChannelBytes2Seconds(activeStream, byteLength - BYTES_PER_SAMPLE) * 1000;

                    Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Frequency, out float frequency);
                    initialFrequency = frequency;
                    bitrate = (int)Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Bitrate);

                    stopCallback = new SyncCallback((a, b, c, d) => RaiseFailed());
                    endCallback = new SyncCallback((a, b, c, d) =>
                    {
                        if (!Looping)
                            RaiseCompleted();
                    });

                    Bass.ChannelSetSync(activeStream, SyncFlags.Stop, 0, stopCallback.Callback, stopCallback.Handle);
                    Bass.ChannelSetSync(activeStream, SyncFlags.End, 0, endCallback.Callback, endCallback.Handle);

                    isLoaded = true;
                }
            });

            InvalidateState();
        }

        /// <summary>
        /// Returns whether the playback state is considered to be running or not.
        /// This will only return true for <see cref="PlaybackState.Playing"/> and <see cref="PlaybackState.Stalled"/>.
        /// </summary>
        private static bool isRunningState(PlaybackState state) => state == PlaybackState.Playing || state == PlaybackState.Stalled;

        void IBassAudio.UpdateDevice(int deviceIndex)
        {
            Bass.ChannelSetDevice(activeStream, deviceIndex);
            Trace.Assert(Bass.LastError == Errors.OK);

            // Bass may leave us in an invalid state after the output device changes (this is true for "No sound" device)
            // if the observed state was playing before change, we should force things into a good state.
            if (isPlayed)
            {
                // While on windows, changing to "No sound" changes the playback state correctly,
                // on macOS it is left in a playing-but-stalled state. Forcefully stopping first fixes this.
                stopInternal();
                startInternal();
            }
        }

        protected override void UpdateState()
        {
            isRunning = isRunningState(Bass.ChannelIsActive(activeStream));

            Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, Bass.ChannelGetPosition(activeStream)) * 1000);

            var leftChannel = isPlayed ? Bass.ChannelGetLevelLeft(activeStream) / 32768f : -1;
            var rightChannel = isPlayed ? Bass.ChannelGetLevelRight(activeStream) / 32768f : -1;

            if (leftChannel >= 0 && rightChannel >= 0)
            {
                currentAmplitudes.LeftChannel = leftChannel;
                currentAmplitudes.RightChannel = rightChannel;

                float[] tempFrequencyData = new float[256];
                Bass.ChannelGetData(activeStream, tempFrequencyData, (int)DataFlags.FFT512);
                currentAmplitudes.FrequencyAmplitudes = tempFrequencyData;
            }
            else
            {<commit_msg>Revert "Simplify TrackBass isRunning condition"<commit_after>// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.IO;
using System.Threading;
using ManagedBass;
using ManagedBass.Fx;
using osuTK;
using osu.Framework.IO;
using System.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Audio.Callbacks;

namespace osu.Framework.Audio.Track
{
    public sealed class TrackBass : Track, IBassAudio, IHasPitchAdjust
    {
        public const int BYTES_PER_SAMPLE = 4;

        private AsyncBufferStream dataStream;

        /// <summary>
        /// Should this track only be used for preview purposes? This suggests it has not yet been fully loaded.
        /// </summary>
        public bool Preview { get; private set; }

        /// <summary>
        /// The handle for this track, if there is one.
        /// </summary>
        private int activeStream;

        /// <summary>
        /// The handle for adjusting tempo.
        /// </summary>
        private int tempoAdjustStream;

        /// <summary>
        /// This marks if the track is paused, or stopped to the end.
        /// </summary>
        private bool isPlayed;

        private long byteLength;

        /// <summary>
        /// The last position that a seek will succeed for.
        /// </summary>
        private double lastSeekablePosition;

        private FileCallbacks fileCallbacks;
        private SyncCallback stopCallback;
        private SyncCallback endCallback;

        private volatile bool isLoaded;

        public override bool IsLoaded => isLoaded;

        /// <summary>
        /// Constructs a new <see cref="TrackBass"/> from provided audio data.
        /// </summary>
        /// <param name="data">The sample data stream.</param>
        /// <param name="quick">If true, the track will not be fully loaded, and should only be used for preview purposes.  Defaults to false.</param>
        public TrackBass(Stream data, bool quick = false)
        {
            if (data == null)
                throw new ArgumentNullException(nameof(data));

            // todo: support this internally to match the underlying Track implementation (which can support this).
            const float tempo_minimum_supported = 0.05f;

            Tempo.ValueChanged += t =>
            {
                if (t.NewValue < tempo_minimum_supported)
                    throw new ArgumentException($"{nameof(TrackBass)} does not support {nameof(Tempo)} specifications below {tempo_minimum_supported}. Use {nameof(Frequency)} instead.");
            };

            EnqueueAction(() =>
            {
                Preview = quick;

                //encapsulate incoming stream with async buffer if it isn't already.
                dataStream = data as AsyncBufferStream ?? new AsyncBufferStream(data, quick ? 8 : -1);

                fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(dataStream));

                BassFlags flags = Preview ? 0 : BassFlags.Decode | BassFlags.Prescan;
                activeStream = Bass.CreateStream(StreamSystem.NoBuffer, flags, fileCallbacks.Callbacks, fileCallbacks.Handle);

                if (!Preview)
                {
                    // We assign the BassFlags.Decode streams to the device "bass_nodevice" to prevent them from getting
                    // cleaned up during a Bass.Free call. This is necessary for seamless switching between audio devices.
                    // Further, we provide the flag BassFlags.FxFreeSource such that freeing the activeStream also frees
                    // all parent decoding streams.
                    const int bass_nodevice = 0x20000;

                    Bass.ChannelSetDevice(activeStream, bass_nodevice);
                    tempoAdjustStream = BassFx.TempoCreate(activeStream, BassFlags.Decode | BassFlags.FxFreeSource);
                    Bass.ChannelSetDevice(tempoAdjustStream, bass_nodevice);
                    activeStream = BassFx.ReverseCreate(tempoAdjustStream, 5f, BassFlags.Default | BassFlags.FxFreeSource);

                    Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoUseQuickAlgorithm, 1);
                    Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoOverlapMilliseconds, 4);
                    Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoSequenceMilliseconds, 30);
                }

                // will be -1 in case of an error
                double seconds = Bass.ChannelBytes2Seconds(activeStream, byteLength = Bass.ChannelGetLength(activeStream));

                bool success = seconds >= 0;

                if (success)
                {
                    Length = seconds * 1000;

                    // Bass does not allow seeking to the end of the track, so the last available position is 1 sample before.
                    lastSeekablePosition = Bass.ChannelBytes2Seconds(activeStream, byteLength - BYTES_PER_SAMPLE) * 1000;

                    Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Frequency, out float frequency);
                    initialFrequency = frequency;
                    bitrate = (int)Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Bitrate);

                    stopCallback = new SyncCallback((a, b, c, d) => RaiseFailed());
                    endCallback = new SyncCallback((a, b, c, d) =>
                    {
                        if (!Looping)
                            RaiseCompleted();
                    });

                    Bass.ChannelSetSync(activeStream, SyncFlags.Stop, 0, stopCallback.Callback, stopCallback.Handle);
                    Bass.ChannelSetSync(activeStream, SyncFlags.End, 0, endCallback.Callback, endCallback.Handle);

                    isLoaded = true;
                }
            });

            InvalidateState();
        }

        /// <summary>
        /// Returns whether the playback state is considered to be running or not.
        /// This will only return true for <see cref="PlaybackState.Playing"/> and <see cref="PlaybackState.Stalled"/>.
        /// </summary>
        private static bool isRunningState(PlaybackState state) => state == PlaybackState.Playing || state == PlaybackState.Stalled;

        void IBassAudio.UpdateDevice(int deviceIndex)
        {
            Bass.ChannelSetDevice(activeStream, deviceIndex);
            Trace.Assert(Bass.LastError == Errors.OK);

            // Bass may leave us in an invalid state after the output device changes (this is true for "No sound" device)
            // if the observed state was playing before change, we should force things into a good state.
            if (isPlayed)
            {
                // While on windows, changing to "No sound" changes the playback state correctly,
                // on macOS it is left in a playing-but-stalled state. Forcefully stopping first fixes this.
                stopInternal();
                startInternal();
            }
        }

        protected override void UpdateState()
        {
            var running = isRunningState(Bass.ChannelIsActive(activeStream));
            var bytePosition = Bass.ChannelGetPosition(activeStream);

            // because device validity check isn't done frequently, when switching to "No sound" device,
            // there will be a brief time where this track will be stopped, before we resume it manually (see comments in UpdateDevice(int).)
            // this makes us appear to be playing, even if we may not be.
            isRunning = running || (isPlayed && bytePosition != byteLength);

            Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, bytePosition) * 1000);

            var leftChannel = isPlayed ? Bass.ChannelGetLevelLeft(activeStream) / 32768f : -1;
            var rightChannel = isPlayed ? Bass.ChannelGetLevelRight(activeStream) / 32768f : -1;

            if (leftChannel >= 0 && rightChannel >= 0)
            {
                currentAmplitudes.LeftChannel = leftChannel;
                currentAmplitudes.RightChannel = rightChannel;

                float[] tempFrequencyData = new float[256];
                Bass.ChannelGetData(activeStream, tempFrequencyData, (int)DataFlags.FFT512);
                currentAmplitudes.FrequencyAmplitudes = tempFrequencyData;
            }
            else
            {
{'fraction_non_alphanumeric': 0.08195223007555447, 'fraction_numerical': 0.0070679990251035826, 'mean_word_length': 2.0137715754682337, 'pattern_counts': {'":': 0, '<': 51, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5145470', 'n_tokens_mistral': 4174, 'n_tokens_neox': 3853, 'n_words': 1248}