From 843cda222da33b7eb8f7dab563227e599bb268da Mon Sep 17 00:00:00 2001 From: Mark Heath Date: Sat, 12 Dec 2020 15:25:16 +0000 Subject: [PATCH] strong naming, and splitting Mp3FileReader --- NAudio.Asio/NAudio.Asio.csproj | 2 + NAudio.Core/NAudio.Core.csproj | 3 + .../Wave/WaveStreams/Mp3FileReaderBase.cs | 461 ++++++++++++++++++ NAudio.Midi/NAudio.Midi.csproj | 3 + NAudio.Uap/NAudio.Uap.csproj | 3 + NAudio.Wasapi/NAudio.Wasapi.csproj | 3 + NAudio.WinForms/NAudio.WinForms.csproj | 3 + NAudio.WinMM/NAudio.WinMM.csproj | 3 + NAudio/Mp3FileReader.cs | 454 +---------------- NAudio/NAudio.csproj | 6 +- NAudio/Properties/AssemblyInfo.cs | 6 - NAudioStrongNameKey.snk | Bin 0 -> 596 bytes 12 files changed, 487 insertions(+), 460 deletions(-) create mode 100644 NAudio.Core/Wave/WaveStreams/Mp3FileReaderBase.cs delete mode 100644 NAudio/Properties/AssemblyInfo.cs create mode 100644 NAudioStrongNameKey.snk diff --git a/NAudio.Asio/NAudio.Asio.csproj b/NAudio.Asio/NAudio.Asio.csproj index f631525..1f9b4c5 100644 --- a/NAudio.Asio/NAudio.Asio.csproj +++ b/NAudio.Asio/NAudio.Asio.csproj @@ -6,6 +6,8 @@ 2.0.0-beta1 true Mark Heath + true + ..\NAudioStrongNameKey.snk diff --git a/NAudio.Core/NAudio.Core.csproj b/NAudio.Core/NAudio.Core.csproj index 8099877..a34846c 100644 --- a/NAudio.Core/NAudio.Core.csproj +++ b/NAudio.Core/NAudio.Core.csproj @@ -5,6 +5,9 @@ Mark Heath 2.0.0-beta1 true + true + ..\NAudioStrongNameKey.snk + diff --git a/NAudio.Core/Wave/WaveStreams/Mp3FileReaderBase.cs b/NAudio.Core/Wave/WaveStreams/Mp3FileReaderBase.cs new file mode 100644 index 0000000..3d6bc4a --- /dev/null +++ b/NAudio.Core/Wave/WaveStreams/Mp3FileReaderBase.cs @@ -0,0 +1,461 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; + +// ReSharper disable once CheckNamespace +namespace NAudio.Wave +{ + class Mp3Index + { + public long FilePosition { get; set; } + public long SamplePosition { get; set; } + public int SampleCount { get; set; } + public int ByteCount { get; set; } + } + + /// + /// Class for reading from MP3 files + /// + public class Mp3FileReaderBase : WaveStream + { + private readonly WaveFormat waveFormat; + private Stream mp3Stream; + private readonly long mp3DataLength; + private readonly long dataStartPosition; + + /// + /// The MP3 wave format (n.b. NOT the output format of this stream - see the WaveFormat property) + /// + public Mp3WaveFormat Mp3WaveFormat { get; private set; } + + private readonly XingHeader xingHeader; + private readonly bool ownInputStream; + + private List tableOfContents; + private int tocIndex; + + private long totalSamples; + private readonly int bytesPerSample; + private readonly int bytesPerDecodedFrame; + + private IMp3FrameDecompressor decompressor; + + private readonly byte[] decompressBuffer; + private int decompressBufferOffset; + private int decompressLeftovers; + private bool repositionedFlag; + + private long position; // decompressed data position tracker + + private readonly object repositionLock = new object(); + + + /// Supports opening a MP3 file + /// MP3 File name + /// Factory method to build a frame decompressor + public Mp3FileReaderBase(string mp3FileName, FrameDecompressorBuilder frameDecompressorBuilder) + : this(File.OpenRead(mp3FileName), frameDecompressorBuilder, true) + { + } + + + + /// + /// Opens MP3 from a stream rather than a file + /// Will not dispose of this stream itself + /// + /// The incoming stream containing MP3 data + /// Factory method to build a frame decompressor + public Mp3FileReaderBase(Stream inputStream, FrameDecompressorBuilder frameDecompressorBuilder) + : this(inputStream, frameDecompressorBuilder, false) + { + + } + + protected Mp3FileReaderBase(Stream inputStream, FrameDecompressorBuilder frameDecompressorBuilder, bool ownInputStream) + { + if (inputStream == null) throw new ArgumentNullException(nameof(inputStream)); + if (frameDecompressorBuilder == null) throw new ArgumentNullException(nameof(frameDecompressorBuilder)); + this.ownInputStream = ownInputStream; + try + { + mp3Stream = inputStream; + Id3v2Tag = Id3v2Tag.ReadTag(mp3Stream); + + dataStartPosition = mp3Stream.Position; + var firstFrame = Mp3Frame.LoadFromStream(mp3Stream); + if (firstFrame == null) + throw new InvalidDataException("Invalid MP3 file - no MP3 Frames Detected"); + double bitRate = firstFrame.BitRate; + xingHeader = XingHeader.LoadXingHeader(firstFrame); + // If the header exists, we can skip over it when decoding the rest of the file + if (xingHeader != null) dataStartPosition = mp3Stream.Position; + + // workaround for a longstanding issue with some files failing to load + // because they report a spurious sample rate change + var secondFrame = Mp3Frame.LoadFromStream(mp3Stream); + if (secondFrame != null && + (secondFrame.SampleRate != firstFrame.SampleRate || + secondFrame.ChannelMode != firstFrame.ChannelMode)) + { + // assume that the first frame was some kind of VBR/LAME header that we failed to recognise properly + dataStartPosition = secondFrame.FileOffset; + // forget about the first frame, the second one is the first one we really care about + firstFrame = secondFrame; + } + + mp3DataLength = mp3Stream.Length - dataStartPosition; + + // try for an ID3v1 tag as well + mp3Stream.Position = mp3Stream.Length - 128; + byte[] tag = new byte[128]; + mp3Stream.Read(tag, 0, 128); + if (tag[0] == 'T' && tag[1] == 'A' && tag[2] == 'G') + { + Id3v1Tag = tag; + mp3DataLength -= 128; + } + + mp3Stream.Position = dataStartPosition; + + // create a temporary MP3 format before we know the real bitrate + Mp3WaveFormat = new Mp3WaveFormat(firstFrame.SampleRate, + firstFrame.ChannelMode == ChannelMode.Mono ? 1 : 2, firstFrame.FrameLength, (int) bitRate); + + CreateTableOfContents(); + tocIndex = 0; + + // [Bit rate in Kilobits/sec] = [Length in kbits] / [time in seconds] + // = [Length in bits ] / [time in milliseconds] + + // Note: in audio, 1 kilobit = 1000 bits. + // Calculated as a double to minimize rounding errors + bitRate = (mp3DataLength*8.0/TotalSeconds()); + + mp3Stream.Position = dataStartPosition; + + // now we know the real bitrate we can create an accurate MP3 WaveFormat + Mp3WaveFormat = new Mp3WaveFormat(firstFrame.SampleRate, + firstFrame.ChannelMode == ChannelMode.Mono ? 1 : 2, firstFrame.FrameLength, (int) bitRate); + decompressor = frameDecompressorBuilder(Mp3WaveFormat); + waveFormat = decompressor.OutputFormat; + bytesPerSample = (decompressor.OutputFormat.BitsPerSample)/8*decompressor.OutputFormat.Channels; + // no MP3 frames have more than 1152 samples in them + bytesPerDecodedFrame = 1152 * bytesPerSample; + // some MP3s I seem to get double + decompressBuffer = new byte[bytesPerDecodedFrame * 2]; + } + catch (Exception) + { + if (ownInputStream) inputStream.Dispose(); + throw; + } + } + + /// + /// Function that can create an MP3 Frame decompressor + /// + /// A WaveFormat object describing the MP3 file format + /// An MP3 Frame decompressor + public delegate IMp3FrameDecompressor FrameDecompressorBuilder(WaveFormat mp3Format); + + private void CreateTableOfContents() + { + try + { + // Just a guess at how many entries we'll need so the internal array need not resize very much + // 400 bytes per frame is probably a good enough approximation. + tableOfContents = new List((int)(mp3DataLength / 400)); + Mp3Frame frame; + do + { + var index = new Mp3Index(); + index.FilePosition = mp3Stream.Position; + index.SamplePosition = totalSamples; + frame = ReadNextFrame(false); + if (frame != null) + { + ValidateFrameFormat(frame); + + totalSamples += frame.SampleCount; + index.SampleCount = frame.SampleCount; + index.ByteCount = (int)(mp3Stream.Position - index.FilePosition); + tableOfContents.Add(index); + } + } while (frame != null); + } + catch (EndOfStreamException) + { + // not necessarily a problem + } + } + + private void ValidateFrameFormat(Mp3Frame frame) + { + if (frame.SampleRate != Mp3WaveFormat.SampleRate) + { + string message = + String.Format( + "Got a frame at sample rate {0}, in an MP3 with sample rate {1}. Mp3FileReader does not support sample rate changes.", + frame.SampleRate, Mp3WaveFormat.SampleRate); + throw new InvalidOperationException(message); + } + int channels = frame.ChannelMode == ChannelMode.Mono ? 1 : 2; + if (channels != Mp3WaveFormat.Channels) + { + string message = + String.Format( + "Got a frame with channel mode {0}, in an MP3 with {1} channels. Mp3FileReader does not support changes to channel count.", + frame.ChannelMode, Mp3WaveFormat.Channels); + throw new InvalidOperationException(message); + } + } + + /// + /// Gets the total length of this file in milliseconds. + /// + private double TotalSeconds() + { + return (double)totalSamples / Mp3WaveFormat.SampleRate; + } + + /// + /// ID3v2 tag if present + /// + // ReSharper disable once InconsistentNaming + public Id3v2Tag Id3v2Tag { get; } + + /// + /// ID3v1 tag if present + /// + // ReSharper disable once InconsistentNaming + public byte[] Id3v1Tag { get; } + + /// + /// Reads the next mp3 frame + /// + /// Next mp3 frame, or null if EOF + public Mp3Frame ReadNextFrame() + { + var frame = ReadNextFrame(true); + if (frame != null) position += frame.SampleCount*bytesPerSample; + return frame; + } + + /// + /// Reads the next mp3 frame + /// + /// Next mp3 frame, or null if EOF + private Mp3Frame ReadNextFrame(bool readData) + { + Mp3Frame frame = null; + try + { + frame = Mp3Frame.LoadFromStream(mp3Stream, readData); + if (frame != null) + { + tocIndex++; + } + } + catch (EndOfStreamException) + { + // suppress for now - it means we unexpectedly got to the end of the stream + // half way through + } + return frame; + } + + /// + /// This is the length in bytes of data available to be read out from the Read method + /// (i.e. the decompressed MP3 length) + /// n.b. this may return 0 for files whose length is unknown + /// + public override long Length => totalSamples * bytesPerSample; + + /// + /// + /// + public override WaveFormat WaveFormat => waveFormat; + + /// + /// + /// + public override long Position + { + get + { + return position; + } + set + { + lock (repositionLock) + { + value = Math.Max(Math.Min(value, Length), 0); + var samplePosition = value / bytesPerSample; + Mp3Index mp3Index = null; + for (int index = 0; index < tableOfContents.Count; index++) + { + if (tableOfContents[index].SamplePosition + tableOfContents[index].SampleCount > samplePosition) + { + mp3Index = tableOfContents[index]; + tocIndex = index; + break; + } + } + + decompressBufferOffset = 0; + decompressLeftovers = 0; + repositionedFlag = true; + + if (mp3Index != null) + { + // perform the reposition + mp3Stream.Position = mp3Index.FilePosition; + + // set the offset into the buffer (that is yet to be populated in Read()) + var frameOffset = samplePosition - mp3Index.SamplePosition; + if (frameOffset > 0) + { + decompressBufferOffset = (int)frameOffset * bytesPerSample; + } + } + else + { + // we are repositioning to the end of the data + mp3Stream.Position = mp3DataLength + dataStartPosition; + } + + position = value; + } + } + } + + /// + /// Reads decompressed PCM data from our MP3 file. + /// + public override int Read(byte[] sampleBuffer, int offset, int numBytes) + { + int bytesRead = 0; + lock (repositionLock) + { + if (decompressLeftovers != 0) + { + int toCopy = Math.Min(decompressLeftovers, numBytes); + Array.Copy(decompressBuffer, decompressBufferOffset, sampleBuffer, offset, toCopy); + decompressLeftovers -= toCopy; + if (decompressLeftovers == 0) + { + decompressBufferOffset = 0; + } + else + { + decompressBufferOffset += toCopy; + } + bytesRead += toCopy; + offset += toCopy; + } + + int targetTocIndex = tocIndex; // the frame index that contains the requested data + + if (repositionedFlag) + { + decompressor.Reset(); + + // Seek back a few frames of the stream to get the reset decoder decode a few + // warm-up frames before reading the requested data. Without the warm-up phase, + // the first half of the frame after the reset is attenuated and does not resemble + // the data as it would be when reading sequentially from the beginning, because + // the decoder is missing the required overlap from the previous frame. + tocIndex = Math.Max(0, tocIndex - 3); // no warm-up at the beginning of the stream + mp3Stream.Position = tableOfContents[tocIndex].FilePosition; + + repositionedFlag = false; + } + + while (bytesRead < numBytes) + { + Mp3Frame frame = ReadNextFrame(true); // internal read - should not advance position + if (frame != null) + { + int decompressed = decompressor.DecompressFrame(frame, decompressBuffer, 0); + + if (tocIndex <= targetTocIndex || decompressed == 0) + { + // The first frame after a reset usually does not immediately yield decoded samples. + // Because the next instructions will fail if a buffer offset is set and the frame + // decoding didn't return data, we skip the part. + // We skip the following instructions also after decoding a warm-up frame. + continue; + } + // Two special cases can happen here: + // 1. We are interested in the first frame of the stream, but need to read the second frame too + // for the decoder to return decoded data + // 2. We are interested in the second frame of the stream, but because reading the first frame + // as warm-up didn't yield any data (because the decoder needs two frames to return data), we + // get data from the first and second frame. + // This case needs special handling, and we have to purge the data of the first frame. + else if (tocIndex == targetTocIndex + 1 && decompressed == bytesPerDecodedFrame * 2) + { + // Purge the first frame's data + Array.Copy(decompressBuffer, bytesPerDecodedFrame, decompressBuffer, 0, bytesPerDecodedFrame); + decompressed = bytesPerDecodedFrame; + } + + int toCopy = Math.Min(decompressed - decompressBufferOffset, numBytes - bytesRead); + Array.Copy(decompressBuffer, decompressBufferOffset, sampleBuffer, offset, toCopy); + if ((toCopy + decompressBufferOffset) < decompressed) + { + decompressBufferOffset = toCopy + decompressBufferOffset; + decompressLeftovers = decompressed - decompressBufferOffset; + } + else + { + // no lefovers + decompressBufferOffset = 0; + decompressLeftovers = 0; + } + offset += toCopy; + bytesRead += toCopy; + } + else + { + break; + } + } + } + Debug.Assert(bytesRead <= numBytes, "MP3 File Reader read too much"); + position += bytesRead; + return bytesRead; + } + + /// + /// Xing header if present + /// + public XingHeader XingHeader => xingHeader; + + /// + /// Disposes this WaveStream + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (mp3Stream != null) + { + if (ownInputStream) + { + mp3Stream.Dispose(); + } + mp3Stream = null; + } + if (decompressor != null) + { + decompressor.Dispose(); + decompressor = null; + } + } + base.Dispose(disposing); + } + } +} diff --git a/NAudio.Midi/NAudio.Midi.csproj b/NAudio.Midi/NAudio.Midi.csproj index 3750502..9e38a05 100644 --- a/NAudio.Midi/NAudio.Midi.csproj +++ b/NAudio.Midi/NAudio.Midi.csproj @@ -5,6 +5,9 @@ 2.0.0-beta1 true Mark Heath + true + ..\NAudioStrongNameKey.snk + diff --git a/NAudio.Uap/NAudio.Uap.csproj b/NAudio.Uap/NAudio.Uap.csproj index a5a1c66..921f3ee 100644 --- a/NAudio.Uap/NAudio.Uap.csproj +++ b/NAudio.Uap/NAudio.Uap.csproj @@ -5,6 +5,9 @@ 2.0.0-beta1 true Mark Heath + true + ..\NAudioStrongNameKey.snk + diff --git a/NAudio.Wasapi/NAudio.Wasapi.csproj b/NAudio.Wasapi/NAudio.Wasapi.csproj index 1745d38..779a417 100644 --- a/NAudio.Wasapi/NAudio.Wasapi.csproj +++ b/NAudio.Wasapi/NAudio.Wasapi.csproj @@ -5,6 +5,9 @@ 2.0.0-beta1 true Mark Heath + true + ..\NAudioStrongNameKey.snk + diff --git a/NAudio.WinForms/NAudio.WinForms.csproj b/NAudio.WinForms/NAudio.WinForms.csproj index bcf0b9d..c5bb3b4 100644 --- a/NAudio.WinForms/NAudio.WinForms.csproj +++ b/NAudio.WinForms/NAudio.WinForms.csproj @@ -6,6 +6,9 @@ 2.0.0-beta1 true Mark Heath + true + ..\NAudioStrongNameKey.snk + diff --git a/NAudio.WinMM/NAudio.WinMM.csproj b/NAudio.WinMM/NAudio.WinMM.csproj index 5520ca9..74838e0 100644 --- a/NAudio.WinMM/NAudio.WinMM.csproj +++ b/NAudio.WinMM/NAudio.WinMM.csproj @@ -4,6 +4,9 @@ netstandard2.0 2.0.0-beta1 true + true + ..\NAudioStrongNameKey.snk + diff --git a/NAudio/Mp3FileReader.cs b/NAudio/Mp3FileReader.cs index b8ff30f..4f5b77d 100644 --- a/NAudio/Mp3FileReader.cs +++ b/NAudio/Mp3FileReader.cs @@ -1,66 +1,17 @@ -using System; using System.IO; -using System.Collections.Generic; -using System.Diagnostics; // ReSharper disable once CheckNamespace namespace NAudio.Wave { - class Mp3Index - { - public long FilePosition { get; set; } - public long SamplePosition { get; set; } - public int SampleCount { get; set; } - public int ByteCount { get; set; } - } /// /// Class for reading from MP3 files /// - public class Mp3FileReader : WaveStream + public class Mp3FileReader : Mp3FileReaderBase { - private readonly WaveFormat waveFormat; - private Stream mp3Stream; - private readonly long mp3DataLength; - private readonly long dataStartPosition; - - /// - /// The MP3 wave format (n.b. NOT the output format of this stream - see the WaveFormat property) - /// - public Mp3WaveFormat Mp3WaveFormat { get; private set; } - - private readonly XingHeader xingHeader; - private readonly bool ownInputStream; - - private List tableOfContents; - private int tocIndex; - - private long totalSamples; - private readonly int bytesPerSample; - private readonly int bytesPerDecodedFrame; - - private IMp3FrameDecompressor decompressor; - - private readonly byte[] decompressBuffer; - private int decompressBufferOffset; - private int decompressLeftovers; - private bool repositionedFlag; - - private long position; // decompressed data position tracker - - private readonly object repositionLock = new object(); - - /// Supports opening a MP3 file - public Mp3FileReader(string mp3FileName) - : this(File.OpenRead(mp3FileName), CreateAcmFrameDecompressor, true) - { - } - /// Supports opening a MP3 file - /// MP3 File name - /// Factory method to build a frame decompressor - public Mp3FileReader(string mp3FileName, FrameDecompressorBuilder frameDecompressorBuilder) - : this(File.OpenRead(mp3FileName), frameDecompressorBuilder, true) + public Mp3FileReader(string mp3FileName) + : base(File.OpenRead(mp3FileName), CreateAcmFrameDecompressor, true) { } @@ -70,110 +21,11 @@ namespace NAudio.Wave /// /// The incoming stream containing MP3 data public Mp3FileReader(Stream inputStream) - : this (inputStream, CreateAcmFrameDecompressor, false) + : base(inputStream, CreateAcmFrameDecompressor, false) { - - } - /// - /// Opens MP3 from a stream rather than a file - /// Will not dispose of this stream itself - /// - /// The incoming stream containing MP3 data - /// Factory method to build a frame decompressor - public Mp3FileReader(Stream inputStream, FrameDecompressorBuilder frameDecompressorBuilder) - : this(inputStream, frameDecompressorBuilder, false) - { - } - private Mp3FileReader(Stream inputStream, FrameDecompressorBuilder frameDecompressorBuilder, bool ownInputStream) - { - if (inputStream == null) throw new ArgumentNullException(nameof(inputStream)); - if (frameDecompressorBuilder == null) throw new ArgumentNullException(nameof(frameDecompressorBuilder)); - this.ownInputStream = ownInputStream; - try - { - mp3Stream = inputStream; - Id3v2Tag = Id3v2Tag.ReadTag(mp3Stream); - - dataStartPosition = mp3Stream.Position; - var firstFrame = Mp3Frame.LoadFromStream(mp3Stream); - if (firstFrame == null) - throw new InvalidDataException("Invalid MP3 file - no MP3 Frames Detected"); - double bitRate = firstFrame.BitRate; - xingHeader = XingHeader.LoadXingHeader(firstFrame); - // If the header exists, we can skip over it when decoding the rest of the file - if (xingHeader != null) dataStartPosition = mp3Stream.Position; - - // workaround for a longstanding issue with some files failing to load - // because they report a spurious sample rate change - var secondFrame = Mp3Frame.LoadFromStream(mp3Stream); - if (secondFrame != null && - (secondFrame.SampleRate != firstFrame.SampleRate || - secondFrame.ChannelMode != firstFrame.ChannelMode)) - { - // assume that the first frame was some kind of VBR/LAME header that we failed to recognise properly - dataStartPosition = secondFrame.FileOffset; - // forget about the first frame, the second one is the first one we really care about - firstFrame = secondFrame; - } - - mp3DataLength = mp3Stream.Length - dataStartPosition; - - // try for an ID3v1 tag as well - mp3Stream.Position = mp3Stream.Length - 128; - byte[] tag = new byte[128]; - mp3Stream.Read(tag, 0, 128); - if (tag[0] == 'T' && tag[1] == 'A' && tag[2] == 'G') - { - Id3v1Tag = tag; - mp3DataLength -= 128; - } - - mp3Stream.Position = dataStartPosition; - - // create a temporary MP3 format before we know the real bitrate - Mp3WaveFormat = new Mp3WaveFormat(firstFrame.SampleRate, - firstFrame.ChannelMode == ChannelMode.Mono ? 1 : 2, firstFrame.FrameLength, (int) bitRate); - - CreateTableOfContents(); - tocIndex = 0; - - // [Bit rate in Kilobits/sec] = [Length in kbits] / [time in seconds] - // = [Length in bits ] / [time in milliseconds] - - // Note: in audio, 1 kilobit = 1000 bits. - // Calculated as a double to minimize rounding errors - bitRate = (mp3DataLength*8.0/TotalSeconds()); - - mp3Stream.Position = dataStartPosition; - - // now we know the real bitrate we can create an accurate MP3 WaveFormat - Mp3WaveFormat = new Mp3WaveFormat(firstFrame.SampleRate, - firstFrame.ChannelMode == ChannelMode.Mono ? 1 : 2, firstFrame.FrameLength, (int) bitRate); - decompressor = frameDecompressorBuilder(Mp3WaveFormat); - waveFormat = decompressor.OutputFormat; - bytesPerSample = (decompressor.OutputFormat.BitsPerSample)/8*decompressor.OutputFormat.Channels; - // no MP3 frames have more than 1152 samples in them - bytesPerDecodedFrame = 1152 * bytesPerSample; - // some MP3s I seem to get double - decompressBuffer = new byte[bytesPerDecodedFrame * 2]; - } - catch (Exception) - { - if (ownInputStream) inputStream.Dispose(); - throw; - } - } - - /// - /// Function that can create an MP3 Frame decompressor - /// - /// A WaveFormat object describing the MP3 file format - /// An MP3 Frame decompressor - public delegate IMp3FrameDecompressor FrameDecompressorBuilder(WaveFormat mp3Format); - /// /// Creates an ACM MP3 Frame decompressor. This is the default with NAudio /// @@ -184,303 +36,5 @@ namespace NAudio.Wave // new DmoMp3FrameDecompressor(this.Mp3WaveFormat); return new AcmMp3FrameDecompressor(mp3Format); } - - private void CreateTableOfContents() - { - try - { - // Just a guess at how many entries we'll need so the internal array need not resize very much - // 400 bytes per frame is probably a good enough approximation. - tableOfContents = new List((int)(mp3DataLength / 400)); - Mp3Frame frame; - do - { - var index = new Mp3Index(); - index.FilePosition = mp3Stream.Position; - index.SamplePosition = totalSamples; - frame = ReadNextFrame(false); - if (frame != null) - { - ValidateFrameFormat(frame); - - totalSamples += frame.SampleCount; - index.SampleCount = frame.SampleCount; - index.ByteCount = (int)(mp3Stream.Position - index.FilePosition); - tableOfContents.Add(index); - } - } while (frame != null); - } - catch (EndOfStreamException) - { - // not necessarily a problem - } - } - - private void ValidateFrameFormat(Mp3Frame frame) - { - if (frame.SampleRate != Mp3WaveFormat.SampleRate) - { - string message = - String.Format( - "Got a frame at sample rate {0}, in an MP3 with sample rate {1}. Mp3FileReader does not support sample rate changes.", - frame.SampleRate, Mp3WaveFormat.SampleRate); - throw new InvalidOperationException(message); - } - int channels = frame.ChannelMode == ChannelMode.Mono ? 1 : 2; - if (channels != Mp3WaveFormat.Channels) - { - string message = - String.Format( - "Got a frame with channel mode {0}, in an MP3 with {1} channels. Mp3FileReader does not support changes to channel count.", - frame.ChannelMode, Mp3WaveFormat.Channels); - throw new InvalidOperationException(message); - } - } - - /// - /// Gets the total length of this file in milliseconds. - /// - private double TotalSeconds() - { - return (double)totalSamples / Mp3WaveFormat.SampleRate; - } - - /// - /// ID3v2 tag if present - /// - // ReSharper disable once InconsistentNaming - public Id3v2Tag Id3v2Tag { get; } - - /// - /// ID3v1 tag if present - /// - // ReSharper disable once InconsistentNaming - public byte[] Id3v1Tag { get; } - - /// - /// Reads the next mp3 frame - /// - /// Next mp3 frame, or null if EOF - public Mp3Frame ReadNextFrame() - { - var frame = ReadNextFrame(true); - if (frame != null) position += frame.SampleCount*bytesPerSample; - return frame; - } - - /// - /// Reads the next mp3 frame - /// - /// Next mp3 frame, or null if EOF - private Mp3Frame ReadNextFrame(bool readData) - { - Mp3Frame frame = null; - try - { - frame = Mp3Frame.LoadFromStream(mp3Stream, readData); - if (frame != null) - { - tocIndex++; - } - } - catch (EndOfStreamException) - { - // suppress for now - it means we unexpectedly got to the end of the stream - // half way through - } - return frame; - } - - /// - /// This is the length in bytes of data available to be read out from the Read method - /// (i.e. the decompressed MP3 length) - /// n.b. this may return 0 for files whose length is unknown - /// - public override long Length => totalSamples * bytesPerSample; - - /// - /// - /// - public override WaveFormat WaveFormat => waveFormat; - - /// - /// - /// - public override long Position - { - get - { - return position; - } - set - { - lock (repositionLock) - { - value = Math.Max(Math.Min(value, Length), 0); - var samplePosition = value / bytesPerSample; - Mp3Index mp3Index = null; - for (int index = 0; index < tableOfContents.Count; index++) - { - if (tableOfContents[index].SamplePosition + tableOfContents[index].SampleCount > samplePosition) - { - mp3Index = tableOfContents[index]; - tocIndex = index; - break; - } - } - - decompressBufferOffset = 0; - decompressLeftovers = 0; - repositionedFlag = true; - - if (mp3Index != null) - { - // perform the reposition - mp3Stream.Position = mp3Index.FilePosition; - - // set the offset into the buffer (that is yet to be populated in Read()) - var frameOffset = samplePosition - mp3Index.SamplePosition; - if (frameOffset > 0) - { - decompressBufferOffset = (int)frameOffset * bytesPerSample; - } - } - else - { - // we are repositioning to the end of the data - mp3Stream.Position = mp3DataLength + dataStartPosition; - } - - position = value; - } - } - } - - /// - /// Reads decompressed PCM data from our MP3 file. - /// - public override int Read(byte[] sampleBuffer, int offset, int numBytes) - { - int bytesRead = 0; - lock (repositionLock) - { - if (decompressLeftovers != 0) - { - int toCopy = Math.Min(decompressLeftovers, numBytes); - Array.Copy(decompressBuffer, decompressBufferOffset, sampleBuffer, offset, toCopy); - decompressLeftovers -= toCopy; - if (decompressLeftovers == 0) - { - decompressBufferOffset = 0; - } - else - { - decompressBufferOffset += toCopy; - } - bytesRead += toCopy; - offset += toCopy; - } - - int targetTocIndex = tocIndex; // the frame index that contains the requested data - - if (repositionedFlag) - { - decompressor.Reset(); - - // Seek back a few frames of the stream to get the reset decoder decode a few - // warm-up frames before reading the requested data. Without the warm-up phase, - // the first half of the frame after the reset is attenuated and does not resemble - // the data as it would be when reading sequentially from the beginning, because - // the decoder is missing the required overlap from the previous frame. - tocIndex = Math.Max(0, tocIndex - 3); // no warm-up at the beginning of the stream - mp3Stream.Position = tableOfContents[tocIndex].FilePosition; - - repositionedFlag = false; - } - - while (bytesRead < numBytes) - { - Mp3Frame frame = ReadNextFrame(true); // internal read - should not advance position - if (frame != null) - { - int decompressed = decompressor.DecompressFrame(frame, decompressBuffer, 0); - - if (tocIndex <= targetTocIndex || decompressed == 0) - { - // The first frame after a reset usually does not immediately yield decoded samples. - // Because the next instructions will fail if a buffer offset is set and the frame - // decoding didn't return data, we skip the part. - // We skip the following instructions also after decoding a warm-up frame. - continue; - } - // Two special cases can happen here: - // 1. We are interested in the first frame of the stream, but need to read the second frame too - // for the decoder to return decoded data - // 2. We are interested in the second frame of the stream, but because reading the first frame - // as warm-up didn't yield any data (because the decoder needs two frames to return data), we - // get data from the first and second frame. - // This case needs special handling, and we have to purge the data of the first frame. - else if (tocIndex == targetTocIndex + 1 && decompressed == bytesPerDecodedFrame * 2) - { - // Purge the first frame's data - Array.Copy(decompressBuffer, bytesPerDecodedFrame, decompressBuffer, 0, bytesPerDecodedFrame); - decompressed = bytesPerDecodedFrame; - } - - int toCopy = Math.Min(decompressed - decompressBufferOffset, numBytes - bytesRead); - Array.Copy(decompressBuffer, decompressBufferOffset, sampleBuffer, offset, toCopy); - if ((toCopy + decompressBufferOffset) < decompressed) - { - decompressBufferOffset = toCopy + decompressBufferOffset; - decompressLeftovers = decompressed - decompressBufferOffset; - } - else - { - // no lefovers - decompressBufferOffset = 0; - decompressLeftovers = 0; - } - offset += toCopy; - bytesRead += toCopy; - } - else - { - break; - } - } - } - Debug.Assert(bytesRead <= numBytes, "MP3 File Reader read too much"); - position += bytesRead; - return bytesRead; - } - - /// - /// Xing header if present - /// - public XingHeader XingHeader => xingHeader; - - /// - /// Disposes this WaveStream - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (mp3Stream != null) - { - if (ownInputStream) - { - mp3Stream.Dispose(); - } - mp3Stream = null; - } - if (decompressor != null) - { - decompressor.Dispose(); - decompressor = null; - } - } - base.Dispose(disposing); - } } } diff --git a/NAudio/NAudio.csproj b/NAudio/NAudio.csproj index e497672..34107e2 100644 --- a/NAudio/NAudio.csproj +++ b/NAudio/NAudio.csproj @@ -12,12 +12,10 @@ true true license.txt + true + ..\NAudioStrongNameKey.snk - - - - diff --git a/NAudio/Properties/AssemblyInfo.cs b/NAudio/Properties/AssemblyInfo.cs deleted file mode 100644 index 0832b8d..0000000 --- a/NAudio/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - - -[assembly: InternalsVisibleTo("NAudioTests")] diff --git a/NAudioStrongNameKey.snk b/NAudioStrongNameKey.snk new file mode 100644 index 0000000000000000000000000000000000000000..df3a3dfa15038b157283d5e16a4c88698fa8f2d4 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097nyu645mgq)T`xkHMh#A!E8n_||f8Zbj zAa#U~YvXMvRE^~bC*};%29aIz12cfS6|N`&F{ZuAnquXH1&)Vd@fRVQxxpauqrt?P zco4w@?oP3D4y1SP;=NrxHgAX9+(}!3mKOO3Jr1E6?g0tWMNb~E5LJXGwC7)f1oWy; zU65J$Id?Bj4k6JlkHa+=0Fm&;OaEuhmE#Eh<4Y zs#?xS#MBhv0>a|!TQoFOd%W!MASJ*zLE_vt5bgx64QyP1m1tEXqQ&>qxX_{_^8>~i zgxX~i4$)MhV4I@^wfusCi-_MLxc|U~4spKMZg1bks7&3|qh_r)m6)meG9Xm<4WWmG zY6^2A2JwFgcCd`=&5t&AB&?cq{QQiwm5aiBjJU&&>J%3J&6Xw<_fu-(@IIG_z94g6 zMW?&j=mCg%Z&W`7rAqj$=O`)!^YSJRHn5KZzK;|bYRmH&tJhgeDbxp&h4L=#P>%@y zvn>-vR`mgfu~uDhRmXzxFMDAvzG9_C!SuexO*EUKeFz(sK;bZ~y)OE5+0`CD?6|cn zce`}h7L&*w0@;QrdjNRkWI)#4j!3v|6YI0`U+iey6OP5*j!r7$H$Yxw+|}Ld>)6{6 zbqH5dP5PT7UBUvG$kJmgR-?2-$17#Tx)z