mirror of
https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git
synced 2024-11-24 14:36:13 +00:00
1056 lines
45 KiB
C#
1056 lines
45 KiB
C#
using ATL.Logging;
|
|
using Commons;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using static ATL.AudioData.FlacHelper;
|
|
using static ATL.AudioData.IO.MetaDataIO;
|
|
using static ATL.ChannelsArrangements;
|
|
|
|
namespace ATL.AudioData.IO
|
|
{
|
|
/// <summary>
|
|
/// Class for OGG files manipulation. Current implementation covers :
|
|
/// - Vorbis data (extensions : .OGG)
|
|
/// - Opus data (extensions : .OPUS)
|
|
/// - Embedded FLAC data (extensions : .OGG)
|
|
///
|
|
/// Implementation notes
|
|
///
|
|
/// 1. CRC's : Current implementation does not test OGG page header CRC's
|
|
/// 2. Page numbers : Current implementation does not test page numbers consistency
|
|
/// 3. When the file has multiple bitstreams, only those whose headers
|
|
/// are positioned at the beginning of the file are detected
|
|
///
|
|
/// </summary>
|
|
partial class Ogg : VorbisTagHolder, IMetaDataIO, IAudioDataIO
|
|
{
|
|
// Contents of the file
|
|
private const int CONTENTS_UNSUPPORTED = -1; // Unsupported
|
|
private const int CONTENTS_VORBIS = 0; // Vorbis
|
|
private const int CONTENTS_OPUS = 1; // Opus
|
|
private const int CONTENTS_FLAC = 2; // FLAC
|
|
|
|
private const int MAX_PAGE_SIZE = 255 * 255;
|
|
|
|
// Ogg page header ID
|
|
private static readonly byte[] OGG_PAGE_ID = Utils.Latin1Encoding.GetBytes("OggS");
|
|
|
|
// Vorbis identification packet (frame) ID
|
|
private static readonly byte[] VORBIS_HEADER_ID = { 1, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73 }; // 1 + "vorbis"
|
|
|
|
// Vorbis comment (tags) packet (frame) ID
|
|
private static readonly byte[] VORBIS_COMMENT_ID = { 3, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73 }; // 3 + "vorbis"
|
|
|
|
// Vorbis setup packet (frame) ID
|
|
private static readonly byte[] VORBIS_SETUP_ID = { 5, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73 }; // 5 + "vorbis"
|
|
|
|
|
|
// Theora identification packet (frame) ID
|
|
private static readonly byte[] THEORA_HEADER_ID = { 0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61 }; // 0x80 + "theora"
|
|
|
|
|
|
// Opus parameter frame ID
|
|
private static readonly byte[] OPUS_HEADER_ID = Utils.Latin1Encoding.GetBytes("OpusHead");
|
|
|
|
// Opus tag frame ID
|
|
private static readonly byte[] OPUS_TAG_ID = Utils.Latin1Encoding.GetBytes("OpusTags");
|
|
|
|
|
|
// FLAC identification packet (frame) ID
|
|
private static readonly byte[] FLAC_HEADER_ID = { 0x7F, 0x46, 0x4C, 0x41, 0x43 }; // 0x7f + "FLAC"
|
|
|
|
|
|
private readonly Format audioFormat;
|
|
|
|
private readonly FileInfo info = new FileInfo();
|
|
|
|
private int contents;
|
|
|
|
private ushort bitRateNominal;
|
|
private ulong samples;
|
|
|
|
private AudioDataManager.SizeInfo sizeInfo;
|
|
|
|
|
|
|
|
public int SampleRate { get; private set; }
|
|
|
|
public bool Valid => isValid();
|
|
public string FileName { get; }
|
|
|
|
public double BitRate => getBitRate();
|
|
public int BitDepth { get; private set; } // Only for embedded FLACs
|
|
|
|
public double Duration => getDuration();
|
|
public ChannelsArrangement ChannelsArrangement { get; private set; }
|
|
|
|
public bool IsVBR => contents != CONTENTS_FLAC;
|
|
public long AudioDataOffset { get; set; }
|
|
public long AudioDataSize { get; set; }
|
|
|
|
|
|
|
|
// Ogg page header
|
|
private sealed class OggPageHeader
|
|
{
|
|
public byte[] ID; // Always "OggS"
|
|
public byte StreamVersion; // Stream structure version
|
|
public byte TypeFlag; // Header type flag
|
|
public ulong AbsolutePosition; // Absolute granule position
|
|
public int StreamId; // Stream serial number
|
|
public int PageNumber; // Page sequence number
|
|
public uint Checksum; // Page CRC32
|
|
public byte Segments; // Number of page segments
|
|
public byte[] LacingValues; // Lacing values - segment sizes
|
|
public long Offset; // Header offset
|
|
|
|
public OggPageHeader(int streamId = 0)
|
|
{
|
|
ID = OGG_PAGE_ID;
|
|
StreamVersion = 0; // Constant
|
|
TypeFlag = 0;
|
|
AbsolutePosition = ulong.MaxValue;
|
|
StreamId = streamId;
|
|
PageNumber = 1;
|
|
Checksum = 0;
|
|
Offset = 0;
|
|
}
|
|
|
|
public void ReadFromStream(Stream r)
|
|
{
|
|
Offset = r.Position;
|
|
byte[] buffer = new byte[8];
|
|
r.Read(buffer, 0, 4);
|
|
ID = new byte[4];
|
|
Array.Copy(buffer, ID, 4);
|
|
r.Read(buffer, 0, 2);
|
|
StreamVersion = buffer[0];
|
|
TypeFlag = buffer[1];
|
|
r.Read(buffer, 0, 8);
|
|
AbsolutePosition = StreamUtils.DecodeUInt64(buffer);
|
|
r.Read(buffer, 0, 4);
|
|
StreamId = StreamUtils.DecodeInt32(buffer);
|
|
r.Read(buffer, 0, 4);
|
|
PageNumber = StreamUtils.DecodeInt32(buffer);
|
|
r.Read(buffer, 0, 4);
|
|
Checksum = StreamUtils.DecodeUInt32(buffer);
|
|
r.Read(buffer, 0, 1);
|
|
Segments = buffer[0];
|
|
|
|
LacingValues = new byte[Segments];
|
|
r.Read(LacingValues, 0, Segments);
|
|
}
|
|
|
|
public static OggPageHeader ReadFromStream(BufferedBinaryReader r)
|
|
{
|
|
OggPageHeader result = new OggPageHeader();
|
|
result.Offset = r.Position;
|
|
result.ID = r.ReadBytes(4);
|
|
result.StreamVersion = r.ReadByte();
|
|
result.TypeFlag = r.ReadByte();
|
|
result.AbsolutePosition = r.ReadUInt64();
|
|
result.StreamId = r.ReadInt32();
|
|
result.PageNumber = r.ReadInt32();
|
|
result.Checksum = r.ReadUInt32();
|
|
result.Segments = r.ReadByte();
|
|
result.LacingValues = r.ReadBytes(result.Segments);
|
|
return result;
|
|
}
|
|
|
|
public void WriteToStream(Stream w)
|
|
{
|
|
var buffer = new Span<byte>(new byte[8]);
|
|
StreamUtils.WriteBytes(w, ID);
|
|
w.WriteByte(StreamVersion);
|
|
w.WriteByte(TypeFlag);
|
|
StreamUtils.WriteUInt64(w, AbsolutePosition, buffer);
|
|
StreamUtils.WriteInt32(w, StreamId, buffer);
|
|
StreamUtils.WriteInt32(w, PageNumber, buffer);
|
|
StreamUtils.WriteUInt32(w, Checksum, buffer);
|
|
w.WriteByte(Segments);
|
|
StreamUtils.WriteBytes(w, LacingValues);
|
|
}
|
|
|
|
public int GetPageSize()
|
|
{
|
|
int result = 0;
|
|
for (int i = 0; i < Segments; i++)
|
|
{
|
|
result += LacingValues[i];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public int GetHeaderSize()
|
|
{
|
|
return 27 + LacingValues.Length;
|
|
}
|
|
|
|
public bool IsValid()
|
|
{
|
|
return ID != null && ID.SequenceEqual(OGG_PAGE_ID);
|
|
}
|
|
|
|
public bool IsFirstPage()
|
|
{
|
|
return 0 == (TypeFlag & 1);
|
|
}
|
|
}
|
|
|
|
#pragma warning disable S4487 // Unread "private" fields should be removed
|
|
// Vorbis parameter header
|
|
private sealed class VorbisHeader
|
|
{
|
|
public byte[] ID;
|
|
public byte[] BitstreamVersion = new byte[4]; // Bitstream version number
|
|
public byte ChannelMode; // Number of channels
|
|
public int SampleRate; // Sample rate (hz)
|
|
public int BitRateMaximal; // Bit rate upper limit
|
|
public int BitRateNominal; // Nominal bit rate
|
|
public int BitRateMinimal; // Bit rate lower limit
|
|
public byte BlockSize; // Coded size for small and long blocks
|
|
public byte StopFlag; // Always 1
|
|
|
|
public void Reset()
|
|
{
|
|
ID = Array.Empty<byte>();
|
|
Array.Clear(BitstreamVersion, 0, BitstreamVersion.Length);
|
|
ChannelMode = 0;
|
|
SampleRate = 0;
|
|
BitRateMaximal = 0;
|
|
BitRateNominal = 0;
|
|
BitRateMinimal = 0;
|
|
BlockSize = 0;
|
|
StopFlag = 0;
|
|
}
|
|
}
|
|
|
|
// Opus parameter header
|
|
private sealed class OpusHeader
|
|
{
|
|
public byte[] ID;
|
|
public byte Version;
|
|
public byte OutputChannelCount;
|
|
public UInt16 PreSkip;
|
|
public UInt32 InputSampleRate;
|
|
public Int16 OutputGain;
|
|
public byte ChannelMappingFamily;
|
|
|
|
public byte StreamCount;
|
|
public byte CoupledStreamCount;
|
|
public byte[] ChannelMapping;
|
|
|
|
public void Reset()
|
|
{
|
|
ID = Array.Empty<byte>();
|
|
Version = 0;
|
|
OutputChannelCount = 0;
|
|
PreSkip = 0;
|
|
InputSampleRate = 0;
|
|
OutputGain = 0;
|
|
ChannelMappingFamily = 0;
|
|
StreamCount = 0;
|
|
CoupledStreamCount = 0;
|
|
}
|
|
}
|
|
#pragma warning restore S4487 // Unread "private" fields should be removed
|
|
|
|
|
|
// File data
|
|
private sealed class FileInfo
|
|
{
|
|
// First, second and third Vorbis packets
|
|
public int AudioStreamId;
|
|
|
|
// Following two properties are mutually exclusive
|
|
public readonly VorbisHeader VorbisParameters = new VorbisHeader(); // Vorbis parameter header
|
|
public readonly OpusHeader OpusParameters = new OpusHeader(); // Opus parameter header
|
|
public FlacHeader FlacParameters; // FLAC parameter header
|
|
|
|
// Total number of samples
|
|
public ulong Samples;
|
|
|
|
// Metrics to ease parsing
|
|
public long CommentHeaderStart; // Begin offset of comment header
|
|
public long CommentHeaderEnd; // End offset of comment header
|
|
public int CommentHeaderSpanPages; // Number of pages the Comment header spans over
|
|
|
|
public long SetupHeaderStart; // Begin offset of setup header
|
|
public long SetupHeaderEnd; // End offset of setup header
|
|
public int SetupHeaderSpanPages; // Number of pages the Setup header spans over
|
|
|
|
public void Reset()
|
|
{
|
|
AudioStreamId = 0;
|
|
|
|
VorbisParameters.Reset();
|
|
OpusParameters.Reset();
|
|
|
|
Samples = 0;
|
|
|
|
CommentHeaderStart = 0;
|
|
CommentHeaderEnd = 0;
|
|
CommentHeaderSpanPages = 0;
|
|
SetupHeaderStart = 0;
|
|
SetupHeaderEnd = 0;
|
|
SetupHeaderSpanPages = 0;
|
|
}
|
|
}
|
|
|
|
// ---------- CONSTRUCTORS & INITIALIZERS
|
|
|
|
protected void resetData()
|
|
{
|
|
SampleRate = 0;
|
|
bitRateNominal = 0;
|
|
BitDepth = -1;
|
|
samples = 0;
|
|
contents = -1;
|
|
AudioDataOffset = -1;
|
|
AudioDataSize = 0;
|
|
|
|
info.Reset();
|
|
}
|
|
|
|
public Ogg(string filePath, Format format) : base(true, true, true, true)
|
|
{
|
|
this.FileName = filePath;
|
|
audioFormat = format;
|
|
resetData();
|
|
}
|
|
|
|
|
|
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
|
|
|
|
public Format AudioFormat
|
|
{
|
|
get
|
|
{
|
|
Format f = new Format(audioFormat);
|
|
string subformat;
|
|
if (contents == CONTENTS_VORBIS) subformat = "Vorbis";
|
|
else if (contents == CONTENTS_OPUS) subformat = "Opus";
|
|
else if (contents == CONTENTS_FLAC) subformat = "FLAC";
|
|
else subformat = "Unsupported";
|
|
f.Name = f.Name + " (" + subformat + ")";
|
|
return f;
|
|
}
|
|
}
|
|
public int CodecFamily => contents == CONTENTS_FLAC ? AudioDataIOFactory.CF_LOSSLESS : AudioDataIOFactory.CF_LOSSY;
|
|
|
|
/// <inheritdoc/>
|
|
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
|
|
{
|
|
// According to id3.org (FAQ), ID3 is not compatible with OGG. Hence ATL does not allow ID3 tags to be written on OGG files; native is for VorbisTag
|
|
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE };
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override IList<Format> MetadataFormats
|
|
{
|
|
get
|
|
{
|
|
IList<Format> result = base.MetadataFormats;
|
|
result[0].Name += " (OGG)";
|
|
result[0].ID += AudioFormat.ID;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Read total samples of OGG file, which are located on the very last page of the file
|
|
private static ulong getSamples(BufferedBinaryReader source)
|
|
{
|
|
byte typeFlag;
|
|
byte[] lacingValues = new byte[255];
|
|
byte nbLacingValues = 0;
|
|
long nextPageOffset = 0;
|
|
double seekDistanceRatio = 0.5;
|
|
|
|
int seekDistance = (int)Math.Round(MAX_PAGE_SIZE * 0.75);
|
|
if (seekDistance > source.Length) seekDistance = (int)Math.Round(source.Length * seekDistanceRatio);
|
|
|
|
bool found = false;
|
|
while (!found && seekDistanceRatio <= 1)
|
|
{
|
|
source.Seek(-seekDistance, SeekOrigin.End);
|
|
found = StreamUtils.FindSequence(source, OGG_PAGE_ID);
|
|
if (!found) // Increase seek distance if not found
|
|
{
|
|
seekDistanceRatio += 0.1;
|
|
seekDistance = (int)Math.Round(source.Length * seekDistanceRatio);
|
|
}
|
|
}
|
|
if (!found)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "No OGG header found; aborting read operation"); // Throw exception ?
|
|
return 0;
|
|
}
|
|
source.Seek(-4, SeekOrigin.Current);
|
|
|
|
// Iterate until last page is encountered
|
|
do
|
|
{
|
|
if (source.Position + nextPageOffset + 27 > source.Length) // End of stream about to be reached => last OGG header did not have the proper type flag
|
|
{
|
|
break;
|
|
}
|
|
|
|
source.Seek(nextPageOffset, SeekOrigin.Current);
|
|
if (source.ReadBytes(4).SequenceEqual(OGG_PAGE_ID))
|
|
{
|
|
source.Seek(1, SeekOrigin.Current);
|
|
typeFlag = source.ReadByte();
|
|
source.Seek(20, SeekOrigin.Current);
|
|
nbLacingValues = source.ReadByte();
|
|
nextPageOffset = 0;
|
|
source.Read(lacingValues, 0, nbLacingValues);
|
|
for (int i = 0; i < nbLacingValues; i++)
|
|
{
|
|
nextPageOffset += lacingValues[i];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "Invalid OGG header found while looking for total samples; aborting read operation"); // Throw exception ?
|
|
return 0;
|
|
}
|
|
|
|
} while (0 == (typeFlag & 0x04)); // 0x04 marks the last page of the logical bitstream
|
|
|
|
|
|
// Stream is positioned at the end of the last page header; backtracking to read AbsolutePosition field
|
|
source.Seek(-nbLacingValues - 21, SeekOrigin.Current);
|
|
|
|
return source.ReadUInt64();
|
|
}
|
|
|
|
private bool getInfo(BufferedBinaryReader source, FileInfo info, ReadTagParams readTagParams)
|
|
{
|
|
IList<long> pageOffsets = new List<long>();
|
|
IDictionary<int, MemoryStream> bitstreams = new Dictionary<int, MemoryStream>();
|
|
IDictionary<int, int> pageCount = new Dictionary<int, int>();
|
|
IDictionary<int, bool> isSupported = new Dictionary<int, bool>();
|
|
IDictionary<int, bool> multiPagecommentPacket = new Dictionary<int, bool>();
|
|
bool isValidHeader = false;
|
|
|
|
try
|
|
{
|
|
// Reads all Vorbis pages that describe bitstream metadata (i.e. Identification, Comment and Setup packets)
|
|
// and concatenate their content into a single, continuous data stream
|
|
//
|
|
// As per OGG specs :
|
|
// - ID packet is alone on the 1st single page of its stream
|
|
// - Comment and Setup packets are together on the 2nd page and may share its last sub-page
|
|
// - Audio data starts on a fresh page
|
|
//
|
|
// NB : Only detects bitstream headers positioned at the beginning of the file
|
|
OggPageHeader pageHeader;
|
|
source.Seek(0, SeekOrigin.Begin);
|
|
do
|
|
{
|
|
pageOffsets.Add(source.Position);
|
|
|
|
pageHeader = OggPageHeader.ReadFromStream(source);
|
|
if (!pageHeader.IsValid()) return false;
|
|
int pageSize = pageHeader.GetPageSize();
|
|
|
|
if (!pageCount.TryAdd(pageHeader.StreamId, 1))
|
|
{
|
|
if (pageHeader.IsFirstPage())
|
|
{
|
|
int newPageCount = pageCount[pageHeader.StreamId] + 1;
|
|
pageCount[pageHeader.StreamId] = newPageCount;
|
|
if (2 == newPageCount) info.CommentHeaderStart = source.Position - pageHeader.GetHeaderSize();
|
|
else if (3 == newPageCount) info.SetupHeaderEnd = source.Position - pageHeader.GetHeaderSize();
|
|
}
|
|
if (isSupported[pageHeader.StreamId] && 2 == pageCount[pageHeader.StreamId]) // Comment packet
|
|
{
|
|
// Load all Comment packet sub-pages into a MemoryStream to read them later in one go
|
|
// NB : Detecting when to read Comment packet page directly is too difficult
|
|
// as some files have their 1st subpage of many using less than 255 segments
|
|
multiPagecommentPacket[pageHeader.StreamId] = true;
|
|
MemoryStream stream;
|
|
if (bitstreams.TryGetValue(pageHeader.StreamId, out var bitstream)) stream = bitstream;
|
|
else
|
|
{
|
|
stream = new MemoryStream();
|
|
bitstreams[pageHeader.StreamId] = stream;
|
|
}
|
|
stream.Write(source.ReadBytes(pageSize), 0, pageSize);
|
|
}
|
|
}
|
|
else // 1st page of a new stream
|
|
{
|
|
multiPagecommentPacket[pageHeader.StreamId] = false;
|
|
// The very first page of any given stream is its Identification packet
|
|
bool supported = readIdentificationPacket(source);
|
|
isSupported[pageHeader.StreamId] = supported;
|
|
if (supported)
|
|
{
|
|
info.AudioStreamId = pageHeader.StreamId;
|
|
isValidHeader = true;
|
|
}
|
|
}
|
|
source.Seek(pageHeader.Offset + pageHeader.GetHeaderSize() + pageSize, SeekOrigin.Begin);
|
|
} while (pageCount[pageHeader.StreamId] < 3); // Stop when the two first pages (containing ID, Comment and Setup packets) have been scanned
|
|
|
|
AudioDataOffset = info.SetupHeaderEnd; // Not exactly true as audio is useless without the setup header
|
|
AudioDataSize = sizeInfo.FileSize - AudioDataOffset;
|
|
|
|
if (readTagParams.PrepareForWriting) // Metrics to prepare writing
|
|
{
|
|
if (CONTENTS_VORBIS == contents || CONTENTS_FLAC == contents)
|
|
{
|
|
// Determine the boundaries of 3rd header (Setup header) by searching from the last-but-one page
|
|
if (pageOffsets.Count > 1) source.Position = pageOffsets[pageOffsets.Count - 2]; else source.Position = pageOffsets[0];
|
|
source.Position += OGG_PAGE_ID.Length;
|
|
if (StreamUtils.FindSequence(source, VORBIS_SETUP_ID))
|
|
{
|
|
info.SetupHeaderStart = source.Position - VORBIS_SETUP_ID.Length;
|
|
info.CommentHeaderEnd = info.SetupHeaderStart;
|
|
|
|
// Determine over how many OGG pages Comment and Setup pages span
|
|
if (pageOffsets.Count > 1)
|
|
{
|
|
int firstSetupPage = -1;
|
|
for (int i = 1; i < pageOffsets.Count; i++)
|
|
{
|
|
if (info.CommentHeaderEnd < pageOffsets[i])
|
|
{
|
|
info.CommentHeaderSpanPages = i - 1;
|
|
firstSetupPage = i - 1;
|
|
}
|
|
if (info.SetupHeaderEnd <= pageOffsets[i]) info.SetupHeaderSpanPages = i - firstSetupPage;
|
|
}
|
|
// Not found yet => comment header takes up all pages, and setup header is on the end of the last page
|
|
if (-1 == firstSetupPage)
|
|
{
|
|
info.CommentHeaderSpanPages = pageOffsets.Count;
|
|
info.SetupHeaderSpanPages = 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
info.CommentHeaderSpanPages = 1;
|
|
info.SetupHeaderSpanPages = 1;
|
|
}
|
|
}
|
|
// Case of embedded FLAC as setup header doesn't exist => end is the end of the page
|
|
if (0 == info.CommentHeaderEnd && StreamUtils.FindSequence(source, OGG_PAGE_ID))
|
|
{
|
|
info.CommentHeaderEnd = source.Position - OGG_PAGE_ID.Length;
|
|
}
|
|
}
|
|
else if (CONTENTS_OPUS == contents)
|
|
{
|
|
info.SetupHeaderStart = info.SetupHeaderEnd;
|
|
info.CommentHeaderEnd = info.SetupHeaderStart;
|
|
info.CommentHeaderSpanPages = pageOffsets.Count;
|
|
info.SetupHeaderSpanPages = 0;
|
|
}
|
|
}
|
|
|
|
// Get total number of samples
|
|
info.Samples = getSamples(source);
|
|
|
|
// Read metadata from Comment pages that span over multiple segments
|
|
foreach (var kvp in bitstreams)
|
|
{
|
|
using BufferedBinaryReader reader = new BufferedBinaryReader(kvp.Value);
|
|
reader.Position = 0;
|
|
readCommentPacket(reader, contents, vorbisTag, readTagParams);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Liberate all MemoryStreams
|
|
foreach (KeyValuePair<int, MemoryStream> entry in bitstreams)
|
|
{
|
|
entry.Value.Close();
|
|
}
|
|
}
|
|
return isValidHeader;
|
|
}
|
|
|
|
public static bool IsValidHeader(byte[] data)
|
|
{
|
|
return StreamUtils.ArrBeginsWith(data, OGG_PAGE_ID);
|
|
}
|
|
|
|
private bool readIdentificationPacket(BufferedBinaryReader source)
|
|
{
|
|
bool isSupportedHeader = false;
|
|
long initialOffset = source.Position;
|
|
|
|
byte[] headerStart = source.ReadBytes(3);
|
|
source.Seek(initialOffset, SeekOrigin.Begin);
|
|
|
|
if (StreamUtils.ArrBeginsWith(VORBIS_HEADER_ID, headerStart))
|
|
{
|
|
contents = CONTENTS_VORBIS;
|
|
info.VorbisParameters.ID = source.ReadBytes(7);
|
|
isSupportedHeader = StreamUtils.ArrBeginsWith(info.VorbisParameters.ID, VORBIS_HEADER_ID);
|
|
|
|
info.VorbisParameters.BitstreamVersion = source.ReadBytes(4);
|
|
info.VorbisParameters.ChannelMode = source.ReadByte();
|
|
info.VorbisParameters.SampleRate = source.ReadInt32();
|
|
info.VorbisParameters.BitRateMaximal = source.ReadInt32();
|
|
info.VorbisParameters.BitRateNominal = source.ReadInt32();
|
|
info.VorbisParameters.BitRateMinimal = source.ReadInt32();
|
|
info.VorbisParameters.BlockSize = source.ReadByte();
|
|
info.VorbisParameters.StopFlag = source.ReadByte();
|
|
}
|
|
else if (StreamUtils.ArrBeginsWith(OPUS_HEADER_ID, headerStart))
|
|
{
|
|
contents = CONTENTS_OPUS;
|
|
info.OpusParameters.ID = source.ReadBytes(8);
|
|
isSupportedHeader = StreamUtils.ArrBeginsWith(info.OpusParameters.ID, OPUS_HEADER_ID);
|
|
|
|
info.OpusParameters.Version = source.ReadByte();
|
|
info.OpusParameters.OutputChannelCount = source.ReadByte();
|
|
info.OpusParameters.PreSkip = source.ReadUInt16();
|
|
info.OpusParameters.InputSampleRate = 48000; // Actual sample rate is hardware-dependent. Let's assume for now that the hardware ATL runs on supports 48KHz
|
|
source.Seek(4, SeekOrigin.Current);
|
|
info.OpusParameters.OutputGain = source.ReadInt16();
|
|
|
|
info.OpusParameters.ChannelMappingFamily = source.ReadByte();
|
|
|
|
if (info.OpusParameters.ChannelMappingFamily > 0)
|
|
{
|
|
info.OpusParameters.StreamCount = source.ReadByte();
|
|
info.OpusParameters.CoupledStreamCount = source.ReadByte();
|
|
|
|
info.OpusParameters.ChannelMapping = new byte[info.OpusParameters.OutputChannelCount];
|
|
for (int i = 0; i < info.OpusParameters.OutputChannelCount; i++)
|
|
{
|
|
info.OpusParameters.ChannelMapping[i] = source.ReadByte();
|
|
}
|
|
}
|
|
}
|
|
else if (StreamUtils.ArrBeginsWith(FLAC_HEADER_ID, headerStart))
|
|
{
|
|
contents = CONTENTS_FLAC;
|
|
source.Seek(FLAC_HEADER_ID.Length, SeekOrigin.Current); // Skip the entire FLAC segment header
|
|
source.Seek(2, SeekOrigin.Current); // FLAC-to-Ogg mapping version
|
|
short nbHeaderPackets = StreamUtils.DecodeBEInt16(source.ReadBytes(2));
|
|
info.FlacParameters = FlacHelper.readHeader(source);
|
|
isSupportedHeader = info.FlacParameters.IsValid();
|
|
}
|
|
else if (StreamUtils.ArrBeginsWith(THEORA_HEADER_ID, headerStart))
|
|
{
|
|
// ATL doesn't support video data; don't examine this bitstream
|
|
}
|
|
return isSupportedHeader;
|
|
}
|
|
|
|
private static void readCommentPacket(BufferedBinaryReader source, int contentType, VorbisTag tag, ReadTagParams readTagParams)
|
|
{
|
|
byte[] tagId;
|
|
bool isValidTagHeader = false;
|
|
if (contentType.Equals(CONTENTS_VORBIS))
|
|
{
|
|
tagId = source.ReadBytes(7);
|
|
isValidTagHeader = StreamUtils.ArrBeginsWith(tagId, VORBIS_COMMENT_ID);
|
|
}
|
|
else if (contentType.Equals(CONTENTS_OPUS))
|
|
{
|
|
tagId = source.ReadBytes(8);
|
|
isValidTagHeader = StreamUtils.ArrBeginsWith(tagId, OPUS_TAG_ID);
|
|
}
|
|
else if (contentType.Equals(CONTENTS_FLAC))
|
|
{
|
|
byte[] aMetaDataBlockHeader = source.ReadBytes(4);
|
|
uint blockLength = StreamUtils.DecodeBEUInt24(aMetaDataBlockHeader, 1);
|
|
byte blockType = (byte)(aMetaDataBlockHeader[0] & 0x7F); // decode metablock type
|
|
isValidTagHeader = blockType < 7;
|
|
}
|
|
|
|
if (isValidTagHeader)
|
|
{
|
|
tag.Clear();
|
|
tag.Read(source, readTagParams);
|
|
}
|
|
}
|
|
|
|
// Calculate duration time
|
|
private double getDuration()
|
|
{
|
|
double result;
|
|
|
|
if (samples > 0)
|
|
{
|
|
if (SampleRate > 0)
|
|
result = samples * 1000.0 / SampleRate;
|
|
else
|
|
result = 0;
|
|
}
|
|
else if ((bitRateNominal > 0) && (ChannelsArrangement.NbChannels > 0))
|
|
{
|
|
result = 1000.0 * sizeInfo.FileSize / bitRateNominal / ChannelsArrangement.NbChannels / 125.0 * 2;
|
|
}
|
|
else
|
|
result = 0;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate average bitrate
|
|
/// </summary>
|
|
/// <returns>Average bitrate</returns>
|
|
private double getBitRate()
|
|
{
|
|
double result = 0;
|
|
|
|
if (getDuration() > 0) result = (sizeInfo.FileSize - sizeInfo.TotalTagSize) * 8.0 / getDuration();
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check for file correctness
|
|
/// </summary>
|
|
/// <returns>True if file data is coherent; false if not</returns>
|
|
private bool isValid()
|
|
{
|
|
return (ChannelsArrangement.NbChannels > 0) && (SampleRate > 0) && (getDuration() > 0.1) && (getBitRate() > 0);
|
|
}
|
|
|
|
private static ChannelsArrangement getArrangementFromCode(int vorbisCode)
|
|
{
|
|
if (vorbisCode > 8) return new ChannelsArrangement(vorbisCode);
|
|
return vorbisCode switch
|
|
{
|
|
1 => MONO,
|
|
2 => STEREO,
|
|
3 => ISO_3_0_0,
|
|
4 => QUAD,
|
|
5 => ISO_3_2_0,
|
|
6 => ISO_3_2_1,
|
|
7 => LRCLFECrLssRss,
|
|
8 => LRCLFELrRrLssRss,
|
|
_ => UNKNOWN
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public bool Read(Stream source, AudioDataManager.SizeInfo sizeInfo, ReadTagParams readTagParams)
|
|
{
|
|
this.sizeInfo = sizeInfo;
|
|
|
|
return Read(source, readTagParams);
|
|
}
|
|
|
|
public bool Read(Stream source, ReadTagParams readTagParams)
|
|
{
|
|
bool result = false;
|
|
|
|
BufferedBinaryReader reader = new BufferedBinaryReader(source);
|
|
info.Reset();
|
|
|
|
if (getInfo(reader, info, readTagParams))
|
|
{
|
|
if (contents.Equals(CONTENTS_VORBIS))
|
|
{
|
|
ChannelsArrangement = getArrangementFromCode(info.VorbisParameters.ChannelMode);
|
|
SampleRate = info.VorbisParameters.SampleRate;
|
|
bitRateNominal = (ushort)(info.VorbisParameters.BitRateNominal / 1000); // Integer division
|
|
}
|
|
else if (contents.Equals(CONTENTS_OPUS))
|
|
{
|
|
ChannelsArrangement = getArrangementFromCode(info.OpusParameters.OutputChannelCount);
|
|
SampleRate = (int)info.OpusParameters.InputSampleRate;
|
|
// No nominal bitrate for OPUS
|
|
}
|
|
else if (contents.Equals(CONTENTS_FLAC))
|
|
{
|
|
ChannelsArrangement = info.FlacParameters.getChannelsArrangement();
|
|
SampleRate = info.FlacParameters.SampleRate;
|
|
BitDepth = info.FlacParameters.BitsPerSample;
|
|
// No nominal bitrate for FLAC
|
|
}
|
|
|
|
samples = info.Samples;
|
|
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Specific implementation for OGG container (multiple pages with limited size)
|
|
|
|
// TODO DOC
|
|
// Simplified implementation of MetaDataIO tweaked for OGG-Vorbis specifics, i.e.
|
|
// - tag spans over multiple pages, each having its own header
|
|
// - last page may include whole or part of Vorbis Setup header
|
|
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
|
public async Task<bool> WriteAsync(Stream s, TagData tag, ProgressToken<float> writeProgress = null)
|
|
{
|
|
bool result = true;
|
|
int writtenPages = 0;
|
|
long nextPageOffset = 0;
|
|
|
|
// Read all the fields in the existing tag (including unsupported fields)
|
|
var readTagParams = new ReadTagParams(true, true);
|
|
readTagParams.PrepareForWriting = true;
|
|
Read(s, readTagParams);
|
|
|
|
// Create the "unpaged" in-memory stream to be written, containing the vorbis tag (=comment header)
|
|
using (var memStream = new MemoryStream((int)(info.SetupHeaderEnd - info.CommentHeaderStart)))
|
|
{
|
|
if (CONTENTS_VORBIS == contents)
|
|
{
|
|
await memStream.WriteAsync(VORBIS_COMMENT_ID, 0, VORBIS_COMMENT_ID.Length);
|
|
vorbisTag.switchOggBehaviour();
|
|
vorbisTag.Write(memStream, tag);
|
|
}
|
|
else if (CONTENTS_OPUS == contents)
|
|
{
|
|
await memStream.WriteAsync(OPUS_TAG_ID, 0, OPUS_TAG_ID.Length);
|
|
vorbisTag.switchOggBehaviour();
|
|
vorbisTag.Write(memStream, tag);
|
|
}
|
|
else if (CONTENTS_FLAC == contents)
|
|
{
|
|
vorbisTag.switchFlacBehaviour();
|
|
FLAC.writeVorbisCommentBlock(memStream, tag, vorbisTag, true);
|
|
}
|
|
|
|
long newTagSize = memStream.Position;
|
|
|
|
int setupHeaderSize = 0;
|
|
int setupHeader_nbSegments = 0;
|
|
byte setupHeader_remainingBytesInLastSegment = 0;
|
|
|
|
// VORBIS: Append the setup header in the "unpaged" in-memory stream
|
|
if (CONTENTS_VORBIS == contents)
|
|
{
|
|
s.Seek(info.SetupHeaderStart, SeekOrigin.Begin);
|
|
if (1 == info.SetupHeaderSpanPages)
|
|
{
|
|
setupHeaderSize = (int)(info.SetupHeaderEnd - info.SetupHeaderStart);
|
|
await StreamUtils.CopyStreamAsync(s, memStream, setupHeaderSize);
|
|
}
|
|
else
|
|
{
|
|
// TODO - handle case where initial setup header spans across two pages
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "The case where Vorbis setup header spans across two OGG pages is not supported yet");
|
|
return false;
|
|
}
|
|
setupHeader_nbSegments = (int)Math.Ceiling(1.0 * setupHeaderSize / 255);
|
|
setupHeader_remainingBytesInLastSegment = (byte)(setupHeaderSize % 255);
|
|
}
|
|
|
|
writtenPages = constructSegmentsTable(memStream, newTagSize, setupHeaderSize, setupHeader_nbSegments, setupHeader_remainingBytesInLastSegment);
|
|
|
|
// Insert the in-memory paged stream into the actual file
|
|
long oldHeadersSize = info.SetupHeaderEnd - info.CommentHeaderStart;
|
|
long newHeadersSize = memStream.Length;
|
|
|
|
if (newHeadersSize > oldHeadersSize) // Need to build a larger file
|
|
{
|
|
await StreamUtils.LengthenStreamAsync(s, info.CommentHeaderEnd, (uint)(newHeadersSize - oldHeadersSize));
|
|
}
|
|
else if (newHeadersSize < oldHeadersSize) // Need to reduce file size
|
|
{
|
|
await StreamUtils.ShortenStreamAsync(s, info.CommentHeaderEnd, (uint)(oldHeadersSize - newHeadersSize));
|
|
}
|
|
|
|
// Rewrite Comment and Setup headers
|
|
s.Seek(info.CommentHeaderStart, SeekOrigin.Begin);
|
|
memStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
await StreamUtils.CopyStreamAsync(memStream, s);
|
|
|
|
nextPageOffset = info.CommentHeaderStart + memStream.Length;
|
|
} // using MemoryStream memStream
|
|
|
|
// If the number of written pages is different than the number of previous existing pages,
|
|
// all the next pages of the file need to be renumbered, and their CRC accordingly recalculated
|
|
if (writtenPages != info.CommentHeaderSpanPages + info.SetupHeaderSpanPages - 1)
|
|
{
|
|
result &= renumberRemainingPages(s, nextPageOffset, writtenPages);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Construct the entire segments table
|
|
private int constructSegmentsTable(Stream memStream, long newTagSize, int setupHeaderSize, int setupHeader_nbSegments, byte setupHeader_remainingBytesInLastSegment)
|
|
{
|
|
int commentsHeader_nbSegments = (int)Math.Ceiling(1.0 * newTagSize / 255);
|
|
resizeMemStream(memStream, commentsHeader_nbSegments, setupHeader_nbSegments);
|
|
return repageMemStream(memStream, newTagSize, commentsHeader_nbSegments, setupHeaderSize, setupHeader_nbSegments, setupHeader_remainingBytesInLastSegment);
|
|
}
|
|
|
|
private byte[] buildSegmentsTable(long newTagSize, int commentsHeader_nbSegments, int setupHeader_nbSegments, byte setupHeader_remainingBytesInLastSegment)
|
|
{
|
|
byte commentsHeader_remainingBytesInLastSegment = (byte)(newTagSize % 255);
|
|
|
|
byte[] entireSegmentsTable = new byte[commentsHeader_nbSegments + setupHeader_nbSegments];
|
|
for (int i = 0; i < commentsHeader_nbSegments - 1; i++)
|
|
{
|
|
entireSegmentsTable[i] = 255;
|
|
}
|
|
entireSegmentsTable[commentsHeader_nbSegments - 1] = commentsHeader_remainingBytesInLastSegment;
|
|
if (CONTENTS_VORBIS == contents)
|
|
{
|
|
for (int i = commentsHeader_nbSegments; i < commentsHeader_nbSegments + setupHeader_nbSegments - 1; i++)
|
|
{
|
|
entireSegmentsTable[i] = 255;
|
|
}
|
|
entireSegmentsTable[commentsHeader_nbSegments + setupHeader_nbSegments - 1] = setupHeader_remainingBytesInLastSegment;
|
|
}
|
|
return entireSegmentsTable;
|
|
}
|
|
|
|
// Resize the whole in-memory stream once and for all to avoid multiple reallocations while repaging
|
|
private static void resizeMemStream(Stream memStream, int commentsHeader_nbSegments, int setupHeader_nbSegments)
|
|
{
|
|
int nbPageHeaders = (int)Math.Ceiling((commentsHeader_nbSegments + setupHeader_nbSegments) / 255.0);
|
|
int totalPageHeadersSize = (nbPageHeaders * 27) + commentsHeader_nbSegments + setupHeader_nbSegments;
|
|
|
|
memStream.SetLength(memStream.Position + totalPageHeadersSize);
|
|
}
|
|
|
|
// Repage comments header & setup header within the in-memory stream
|
|
private int repageMemStream(
|
|
Stream memStream,
|
|
long newTagSize,
|
|
int commentsHeader_nbSegments,
|
|
int setupHeaderSize,
|
|
int setupHeader_nbSegments,
|
|
byte setupHeader_remainingBytesInLastSegment)
|
|
{
|
|
memStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
OggPageHeader header = new OggPageHeader(info.AudioStreamId);
|
|
|
|
int segmentsLeftToPage = commentsHeader_nbSegments + setupHeader_nbSegments;
|
|
int bytesLeftToPage = (int)newTagSize + setupHeaderSize;
|
|
int pagedSegments = 0;
|
|
int pagedBytes = 0;
|
|
long position;
|
|
|
|
IList<KeyValuePair<long, int>> pageHeaderOffsets = new List<KeyValuePair<long, int>>();
|
|
|
|
// Repaging
|
|
while (segmentsLeftToPage > 0)
|
|
{
|
|
header.Segments = (byte)Math.Min(255, segmentsLeftToPage);
|
|
header.LacingValues = new byte[header.Segments];
|
|
if (segmentsLeftToPage == header.Segments) header.AbsolutePosition = 0; // Last header page has its absolutePosition = 0
|
|
|
|
byte[] entireSegmentsTable = buildSegmentsTable(newTagSize, commentsHeader_nbSegments, setupHeader_nbSegments, setupHeader_remainingBytesInLastSegment);
|
|
Array.Copy(entireSegmentsTable, pagedSegments, header.LacingValues, 0, header.Segments);
|
|
|
|
position = memStream.Position;
|
|
// Push current data to write header
|
|
// NB : We're manipulating the MemoryStream here; calling an async variant won't have any relevant effect on performance
|
|
StreamUtils.CopySameStream(memStream, memStream.Position, memStream.Position + header.GetHeaderSize(), bytesLeftToPage);
|
|
memStream.Seek(position, SeekOrigin.Begin);
|
|
|
|
pageHeaderOffsets.Add(new KeyValuePair<long, int>(position, header.GetPageSize() + header.GetHeaderSize()));
|
|
|
|
header.WriteToStream(memStream);
|
|
memStream.Seek(header.GetPageSize(), SeekOrigin.Current);
|
|
|
|
pagedSegments += header.Segments;
|
|
segmentsLeftToPage -= header.Segments;
|
|
pagedBytes += header.GetPageSize();
|
|
bytesLeftToPage -= header.GetPageSize();
|
|
|
|
header.PageNumber++;
|
|
if (0 == header.TypeFlag) header.TypeFlag = 1;
|
|
}
|
|
generatePageCrc32(memStream, pageHeaderOffsets);
|
|
|
|
return header.PageNumber - 1;
|
|
}
|
|
|
|
// Generate CRC32 of created pages
|
|
private static void generatePageCrc32(Stream s, IEnumerable<KeyValuePair<long, int>> pageHeaderOffsets)
|
|
{
|
|
byte[] data = Array.Empty<byte>();
|
|
foreach (KeyValuePair<long, int> kv in pageHeaderOffsets)
|
|
{
|
|
s.Seek(kv.Key, SeekOrigin.Begin);
|
|
if (data.Length < kv.Value) data = new byte[kv.Value]; // Enlarge only if needed; max size is 0xffff
|
|
s.Read(data, 0, kv.Value);
|
|
uint crc = OggCRC32.CalculateCRC(0, data, (uint)kv.Value);
|
|
// Write CRC value at the dedicated location within the OGG header
|
|
s.Seek(kv.Key + 22, SeekOrigin.Begin);
|
|
s.Write(StreamUtils.EncodeUInt32(crc));
|
|
}
|
|
}
|
|
|
|
private static bool renumberRemainingPages(Stream s, long nextPageOffset, int writtenPages)
|
|
{
|
|
OggPageHeader header = new OggPageHeader();
|
|
byte[] data = Array.Empty<byte>();
|
|
do
|
|
{
|
|
s.Seek(nextPageOffset, SeekOrigin.Begin);
|
|
header.ReadFromStream(s);
|
|
|
|
if (header.IsValid())
|
|
{
|
|
// Rewrite page number
|
|
writtenPages++;
|
|
s.Seek(nextPageOffset + 18, SeekOrigin.Begin);
|
|
s.Write(StreamUtils.EncodeInt32(writtenPages));
|
|
|
|
// Rewrite CRC
|
|
s.Seek(nextPageOffset, SeekOrigin.Begin);
|
|
int dataSize = header.GetHeaderSize() + header.GetPageSize();
|
|
if (data.Length < dataSize) data = new byte[dataSize]; // Only realloc when size is insufficient
|
|
s.Read(data, 0, dataSize);
|
|
|
|
// Checksum has to include its own location, as if it were 0
|
|
data[22] = 0;
|
|
data[23] = 0;
|
|
data[24] = 0;
|
|
data[25] = 0;
|
|
|
|
uint crc = OggCRC32.CalculateCRC(0, data, (uint)dataSize);
|
|
s.Seek(nextPageOffset + 22, SeekOrigin.Begin); // Position of CRC within OGG header
|
|
s.Write(StreamUtils.EncodeUInt32(crc));
|
|
|
|
// To the next header
|
|
nextPageOffset += dataSize;
|
|
}
|
|
else
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "Invalid OGG header found; aborting writing operation"); // Throw exception ?
|
|
return false;
|
|
}
|
|
|
|
} while (0 == (header.TypeFlag & 0x04)); // 0x04 marks the last page of the logical bitstream
|
|
return true;
|
|
}
|
|
|
|
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
|
public async Task<bool> RemoveAsync(Stream s)
|
|
{
|
|
TagData tag = vorbisTag.GetDeletionTagData();
|
|
return await WriteAsync(s, tag);
|
|
}
|
|
|
|
public void SetEmbedder(IMetaDataEmbedder embedder)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
vorbisTag.Clear();
|
|
}
|
|
}
|
|
} |