Author Topic: Wrong FLAC length unless plugin is loaded but it still works without doing so?  (Read 521 times)

aybe

  • Posts: 162
There seems to be an issue with FLAC files,

I've been wondering why I can read more data than stream length...
Well, I checked the file in two audio editors and indeed it's longer than what BASS says.
That is, unless you explicitly load the FLAC plugin, length is then right.

But then, how come is BASS able to read, albeit incorrectly, the FLAC file without loading the plugin?

Length in samples without loading the plugin: 16758000
Length in samples with plugin loaded: 16772112

I've uploaded the file to your FTP should you want to take a look at it.

If you could clarify what's going on, that'd be great, thanks!

(test below will fail with 16, expected for last block, i.e. it works when loading the plugin)

Code: [Select]
using System.Diagnostics.CodeAnalysis;
using Un4seen.Bass;
using Un4seen.Bass.AddOn.Mix;

// ReSharper disable CommentTypo
// ReSharper disable StringLiteralTypo

namespace Tests;

[TestClass]
public class UnitTest1
{
    [ClassInitialize]
    [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "<Pending>")]
    public static void ClassInit(TestContext context)
    {
        if (!Bass.BASS_Init(0, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero))
        {
            throw new BassException();
        }
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        if (!Bass.BASS_Free())
        {
            throw new BassException();
        }
    }

    [TestMethod]
    public void TestMethod1()
    {
        // samples with no plugin load: 16758000 % 1024 == 240

        // samples with plugin load: 16772112 % 1024 == 16

        if (Bass.BASS_PluginLoad("bassflac") == 0)
        {
            throw new BassException();
        }

        const string path =
            """D:\Music\Everything but the Girl - Corcovado (Knee Deep remix) [Ben Watt vocal re-edit].flac""";

        const BASSFlag streamFlags = BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_SAMPLE_FLOAT;

        var streamHandle = Bass.BASS_StreamCreateFile(path, 0, 0, streamFlags);

        if (streamHandle == 0)
        {
            throw new BassException();
        }

        using var stream = new AudioStream(streamHandle);

        const BASSFlag mixerFlags = BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_SAMPLE_FLOAT | BASSFlag.BASS_MIXER_END;

        var mixerHandle = BassMix.BASS_Mixer_StreamCreate(stream.Rate, 1, mixerFlags);

        if (mixerHandle == 0)
        {
            throw new BassException();
        }

        const BASSFlag channelFlags = BASSFlag.BASS_MIXER_CHAN_DOWNMIX | BASSFlag.BASS_MIXER_CHAN_LIMIT |
                                      BASSFlag.BASS_MIXER_NORAMPIN;

        if (!BassMix.BASS_Mixer_StreamAddChannel(mixerHandle, stream.Handle, channelFlags))
        {
            throw new BassException();
        }

        using var mixer = new AudioStream(mixerHandle);

        const int ratio = 1024;

        var blocks = mixer.Length / ratio + (mixer.Length % ratio == 0 ? 0 : 1);

        var buffer = new float[ratio * mixer.Channels];

        var bufferBytes = mixer.SamplesToBytes(ratio);

        Console.WriteLine(stream);
        Console.WriteLine(mixer);
        Console.WriteLine(blocks);
        Console.WriteLine(ratio);
        Console.WriteLine(bufferBytes);

        for (var i = 0; i < blocks; i++)
        {
            var readBytes = Bass.BASS_ChannelGetData(mixer.Handle, buffer, (int)BASSData.BASS_DATA_FLOAT | bufferBytes);

            if (readBytes == -1)
            {
                throw new BassException();
            }

            var readSamples = mixer.BytesToSamples(readBytes);
            Assert.AreEqual(ratio, readSamples, i.ToString());
        }
    }
}

public sealed class BassException() : Exception($"{Bass.BASS_ErrorGetCode()}");

public abstract class Disposable : IDisposable
{
    private bool Disposed { get; set; }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Disposable()
    {
        Dispose(false);
    }

    [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
    protected virtual void Dispose(bool disposing)
    {
        if (Disposed)
        {
            return;
        }

        DisposeNative();

        if (disposing)
        {
            DisposeManaged();
        }

        Disposed = true;
    }

    [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
    protected virtual void DisposeManaged()
    {
    }

    [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
    protected virtual void DisposeNative()
    {
    }


    protected void ThrowIfDisposed()
    {
        if (Disposed)
        {
            throw new ObjectDisposedException(GetType().Name);
        }
    }
}

public sealed class AudioStream : Disposable
{
    internal AudioStream(int handle)
    {
        ArgumentOutOfRangeException.ThrowIfZero(handle);

        Handle = handle;

        Info = Bass.BASS_ChannelGetInfo(Handle) ?? throw new BassException();
    }

    internal int Handle { get; }

    private BASS_CHANNELINFO Info { get; }

    public int Bits => Info.Is32bit ? 32 : Info.Is8bit ? 8 : 16;

    public int Channels => Info.chans;

    public long Length
    {
        get
        {
            ThrowIfDisposed();

            var length = Bass.BASS_ChannelGetLength(Handle);

            if (length == -1)
            {
                throw new BassException();
            }

            length = BytesToSamples(length);

            return length;
        }
    }

    public long Position // TODO setter
    {
        get
        {
            ThrowIfDisposed();

            var position = Bass.BASS_ChannelGetPosition(Handle);

            if (position == -1)
            {
                throw new BassException();
            }

            position = BytesToSamples(position);

            return position;
        }
    }

    public int Rate => Info.freq;

    public override string ToString()
    {
        return
            $"{nameof(Bits)}: {Bits}, {nameof(Channels)}: {Channels}, {nameof(Length)}: {Length}, {nameof(Position)}: {Position}, {nameof(Rate)}: {Rate}";
    }

    protected override void DisposeNative()
    {
        if (!Bass.BASS_StreamFree(Handle))
        {
            throw new BassException();
        }
    }

    #region Helpers

/*
   ms = samples * 1000 / samplerate.
   samples = ms * samplerate / 1000.
   samplerate = samples * 1000 / ms.
   bytes = samples * bits * channels / 8.
   samples = bytes * 8 / bits / channels.
 */

    public static int BytesToSamples(int bytes, int bits, int channels)
    {
        return bytes * 8 / bits / channels;
    }

    public static long BytesToSamples(long bytes, int bits, int channels)
    {
        return bytes * 8 / bits / channels;
    }

    public int BytesToSamples(int bytes)
    {
        ThrowIfDisposed();

        return BytesToSamples(bytes, Bits, Channels);
    }

    public long BytesToSamples(long bytes)
    {
        ThrowIfDisposed();

        return BytesToSamples(bytes, Bits, Channels);
    }

    public static int SamplesToBytes(int samples, int bits, int channels)
    {
        return samples * bits * channels / 8;
    }

    public static long SamplesToBytes(long samples, int bits, int channels)
    {
        return samples * bits * channels / 8;
    }

    public int SamplesToBytes(int samples)
    {
        ThrowIfDisposed();

        return SamplesToBytes(samples, Bits, Channels);
    }

    public long SamplesToBytes(long samples)
    {
        ThrowIfDisposed();

        return SamplesToBytes(samples, Bits, Channels);
    }

    #endregion
}
« Last Edit: 15 May '24 - 18:25 by aybe »

Chris

  • Posts: 2217
Quote
Length in samples without loading the plugin: 16758000
Length in samples with plugin loaded: 16772112

with plugin Bass will use bassflac,
without Plugin will use Media Foundation Codec.
« Last Edit: 16 May '24 - 11:07 by Chris »

aybe

  • Posts: 162
Oh yes, I understand now... I remember using MF and it was pretty inaccurate, seems like it's still is...

Thanks!