mirror of
https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git
synced 2024-11-20 10:27:45 +00:00
630 lines
25 KiB
C#
630 lines
25 KiB
C#
using System;
|
|
using System.IO;
|
|
using static ATL.AudioData.AudioDataManager;
|
|
using Commons;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using static ATL.ChannelsArrangements;
|
|
using static ATL.TagData;
|
|
using System.Text;
|
|
using System.Buffers.Binary;
|
|
|
|
namespace ATL.AudioData.IO
|
|
{
|
|
/// <summary>
|
|
/// Class for PCM (uncompressed audio) files manipulation (extension : .WAV)
|
|
///
|
|
/// Implementation notes
|
|
///
|
|
/// 1. BEXT metadata - UMID field
|
|
///
|
|
/// UMID field is decoded "as is" using the hex notation. No additional interpretation has been done so far.
|
|
///
|
|
/// </summary>
|
|
class WAV : MetaDataIO, IAudioDataIO, IMetaDataEmbedder
|
|
{
|
|
// Format type names
|
|
public const string WAV_FORMAT_UNKNOWN = "Unknown";
|
|
public const string WAV_FORMAT_PCM = "Windows PCM";
|
|
public const string WAV_FORMAT_ADPCM = "Microsoft ADPCM";
|
|
public const string WAV_FORMAT_ALAW = "A-LAW";
|
|
public const string WAV_FORMAT_MULAW = "MU-LAW";
|
|
public const string WAV_FORMAT_DVI_IMA_ADPCM = "DVI/IMA ADPCM";
|
|
public const string WAV_FORMAT_MP3 = "MPEG Layer III";
|
|
|
|
private static readonly byte[] HEADER_RIFF = Utils.Latin1Encoding.GetBytes("RIFF");
|
|
private static readonly byte[] HEADER_RIFX = Utils.Latin1Encoding.GetBytes("RIFX");
|
|
private static readonly byte[] HEADER_RF64 = Utils.Latin1Encoding.GetBytes("RF64");
|
|
|
|
private const string FORMAT_WAVE = "WAVE";
|
|
|
|
// Standard sub-chunks
|
|
private const string CHUNK_FORMAT = "fmt ";
|
|
private const string CHUNK_FORMAT64 = "ds64";
|
|
private const string CHUNK_FACT = "fact";
|
|
private const string CHUNK_DATA = "data";
|
|
private const string CHUNK_SAMPLE = SampleTag.CHUNK_SAMPLE;
|
|
private const string CHUNK_CUE = CueTag.CHUNK_CUE;
|
|
private const string CHUNK_LIST = List.CHUNK_LIST;
|
|
private const string CHUNK_DISP = DispTag.CHUNK_DISP;
|
|
|
|
// Broadcast Wave metadata sub-chunk
|
|
private const string CHUNK_BEXT = BextTag.CHUNK_BEXT;
|
|
private const string CHUNK_IXML = IXmlTag.CHUNK_IXML;
|
|
private const string CHUNK_XMP = XmpTag.CHUNK_XMP;
|
|
private const string CHUNK_CART = CartTag.CHUNK_CART;
|
|
private const string CHUNK_ID3 = "id3 ";
|
|
|
|
|
|
private ushort formatId;
|
|
private uint sampleRate;
|
|
private uint bytesPerSecond;
|
|
private ushort bitsPerSample;
|
|
private long sampleNumber;
|
|
private long headerSize;
|
|
|
|
private SizeInfo sizeInfo;
|
|
private readonly Format audioFormat;
|
|
|
|
private bool _isLittleEndian;
|
|
|
|
private long id3v2Offset;
|
|
private FileStructureHelper id3v2StructureHelper = new FileStructureHelper(false);
|
|
|
|
|
|
// Mapping between WAV frame codes and ATL frame codes
|
|
private static readonly IDictionary<string, Field> frameMapping = new Dictionary<string, Field>
|
|
{
|
|
{ "bext.description", Field.GENERAL_DESCRIPTION },
|
|
{ "info.INAM", Field.TITLE },
|
|
{ "info.TITL", Field.TITLE },
|
|
{ "info.IART", Field.ARTIST },
|
|
{ "info.IPRD", Field.ALBUM },
|
|
{ "info.ICMT", Field.COMMENT },
|
|
{ "info.ICOP", Field.COPYRIGHT },
|
|
{ "info.ICRD", Field.RECORDING_DATE },
|
|
{ "info.IGNR", Field.GENRE },
|
|
{ "info.IRTD", Field.RATING },
|
|
{ "info.TRCK", Field.TRACK_NUMBER },
|
|
{ "info.IPRT", Field.TRACK_NUMBER },
|
|
{ "info.ITRK", Field.TRACK_NUMBER },
|
|
{ "info.ITCH", Field.ENCODED_BY },
|
|
{ "info.ISFT", Field.ENCODER },
|
|
{ "info.ILNG", Field.LANGUAGE}
|
|
};
|
|
|
|
|
|
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
|
|
|
|
// IAudioDataIO
|
|
public Format AudioFormat
|
|
{
|
|
get
|
|
{
|
|
Format f = new Format(audioFormat);
|
|
f.Name = f.Name + " (" + getFormat() + ")";
|
|
return f;
|
|
}
|
|
}
|
|
public int SampleRate => (int)sampleRate;
|
|
public bool IsVBR => false;
|
|
public int CodecFamily => AudioDataIOFactory.CF_LOSSLESS;
|
|
public string FileName { get; }
|
|
|
|
public double BitRate { get; private set; }
|
|
|
|
public int BitDepth => bitsPerSample;
|
|
public double Duration { get; private set; }
|
|
|
|
public ChannelsArrangement ChannelsArrangement { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
|
|
{
|
|
// Native for bext, info and iXML chunks
|
|
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE, MetaDataIOFactory.TagType.ID3V2, MetaDataIOFactory.TagType.ID3V1 };
|
|
}
|
|
|
|
public long AudioDataOffset { get; set; }
|
|
public long AudioDataSize { get; set; }
|
|
|
|
|
|
// MetaDataIO
|
|
protected override int getDefaultTagOffset()
|
|
{
|
|
return TO_BUILTIN;
|
|
}
|
|
|
|
protected override MetaDataIOFactory.TagType getImplementedTagType()
|
|
{
|
|
return MetaDataIOFactory.TagType.NATIVE;
|
|
}
|
|
|
|
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
|
|
{
|
|
Field supportedMetaId = Field.NO_FIELD;
|
|
|
|
// Finds the ATL field identifier
|
|
if (frameMapping.TryGetValue(ID, out var value)) supportedMetaId = value;
|
|
|
|
return supportedMetaId;
|
|
}
|
|
|
|
protected override bool isLittleEndian => _isLittleEndian;
|
|
|
|
public override string EncodeDate(DateTime date)
|
|
{
|
|
return TrackUtils.FormatISOTimestamp(date).Replace("T", " ");
|
|
}
|
|
|
|
|
|
// IMetaDataEmbedder
|
|
public long HasEmbeddedID3v2 => id3v2Offset;
|
|
|
|
public uint ID3v2EmbeddingHeaderSize => 8;
|
|
|
|
public FileStructureHelper.Zone Id3v2Zone => id3v2StructureHelper.GetZone(CHUNK_ID3);
|
|
|
|
|
|
// ---------- CONSTRUCTORS & INITIALIZERS
|
|
|
|
protected void resetData()
|
|
{
|
|
Duration = 0;
|
|
BitRate = 0;
|
|
|
|
formatId = 0;
|
|
sampleRate = 0;
|
|
bytesPerSecond = 0;
|
|
bitsPerSample = 0;
|
|
sampleNumber = 0;
|
|
headerSize = 0;
|
|
|
|
id3v2Offset = -1;
|
|
id3v2StructureHelper.Clear();
|
|
|
|
AudioDataOffset = -1;
|
|
AudioDataSize = 0;
|
|
|
|
_isLittleEndian = false;
|
|
|
|
ResetData();
|
|
}
|
|
|
|
public WAV(string filePath, Format format)
|
|
{
|
|
this.FileName = filePath;
|
|
audioFormat = format;
|
|
resetData();
|
|
}
|
|
|
|
|
|
// ---------- SUPPORT METHODS
|
|
|
|
public static bool IsValidHeader(byte[] data)
|
|
{
|
|
return StreamUtils.ArrBeginsWith(data, HEADER_RIFF) || StreamUtils.ArrBeginsWith(data, HEADER_RIFX) || StreamUtils.ArrBeginsWith(data, HEADER_RF64);
|
|
}
|
|
|
|
private bool readWAV(Stream source, ReadTagParams readTagParams)
|
|
{
|
|
bool isRf64 = false;
|
|
long riffChunkSize;
|
|
object formattedRiffChunkSize = 0;
|
|
byte[] data = new byte[4];
|
|
byte[] data64 = new byte[8];
|
|
|
|
source.Seek(0, SeekOrigin.Begin);
|
|
|
|
// Read header
|
|
source.Read(data, 0, 4);
|
|
|
|
if (data.SequenceEqual(HEADER_RF64)) isRf64 = true;
|
|
|
|
if (data.SequenceEqual(HEADER_RIFF) || isRf64)
|
|
{
|
|
_isLittleEndian = true;
|
|
}
|
|
else if (data.SequenceEqual(HEADER_RIFX))
|
|
{
|
|
_isLittleEndian = false;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Force creation of FileStructureHelper with detected endianness
|
|
structureHelper = new FileStructureHelper(isLittleEndian);
|
|
id3v2StructureHelper = new FileStructureHelper(isLittleEndian);
|
|
|
|
var riffChunkSizePos = source.Position;
|
|
source.Read(data, 0, 4);
|
|
if (isLittleEndian) riffChunkSize = StreamUtils.DecodeUInt32(data); else riffChunkSize = StreamUtils.DecodeBEUInt32(data);
|
|
if (riffChunkSize < uint.MaxValue) formattedRiffChunkSize = getFormattedRiffChunkSize(riffChunkSize, isRf64);
|
|
|
|
// Format code
|
|
source.Read(data, 0, 4);
|
|
string str = Utils.Latin1Encoding.GetString(data);
|
|
if (!str.Equals(FORMAT_WAVE)) return false;
|
|
|
|
|
|
string subChunkId = "";
|
|
uint paddingSize = 0;
|
|
bool foundSample = false;
|
|
bool foundCue = false;
|
|
bool foundList = false;
|
|
bool foundDisp = false;
|
|
int dispIndex = 0;
|
|
bool foundBext = false;
|
|
bool foundIXml = false;
|
|
bool foundXmp = false;
|
|
bool foundCart = false;
|
|
|
|
// Sub-chunks loop
|
|
// NB1 : we're testing source.Position + 8 because the chunk header (chunk ID and size) takes up 8 bytes
|
|
// NB2 : uint.MaxValue is when the size declared in the traditional 32-bit header is discarded for the RF64 64-bit header
|
|
long totalFileSize = riffChunkSize + 8;
|
|
while (source.Position + 8 < totalFileSize || uint.MaxValue == riffChunkSize)
|
|
{
|
|
if (paddingSize > 0)
|
|
{
|
|
source.Read(data, 0, 1);
|
|
// Padding has been forgotten !
|
|
if (data[0] > 31 && data[0] < 255)
|
|
{
|
|
// Align to the correct position
|
|
source.Seek(-1, SeekOrigin.Current);
|
|
|
|
// Update zone size (remove and replace zone with updated size), if it exists
|
|
FileStructureHelper sHelper = (subChunkId == CHUNK_ID3) ? id3v2StructureHelper : structureHelper;
|
|
FileStructureHelper.Zone previousZone = sHelper.GetZone(subChunkId);
|
|
if (previousZone != null)
|
|
{
|
|
previousZone.Size--;
|
|
sHelper.RemoveZone(subChunkId);
|
|
sHelper.AddZone(previousZone);
|
|
}
|
|
}
|
|
}
|
|
// Chunk ID
|
|
source.Read(data, 0, 4);
|
|
subChunkId = Utils.Latin1Encoding.GetString(data);
|
|
|
|
// Chunk size
|
|
source.Read(data, 0, 4);
|
|
long chunkSize = isLittleEndian ? StreamUtils.DecodeUInt32(data) : StreamUtils.DecodeBEUInt32(data);
|
|
// Word-align declared chunk size, as per specs
|
|
paddingSize = (uint)(chunkSize % 2);
|
|
|
|
var chunkDataPos = source.Position;
|
|
|
|
if (subChunkId.Equals(CHUNK_FORMAT64, StringComparison.OrdinalIgnoreCase)) // DS64 always appears before FMT
|
|
{
|
|
source.Read(data64, 0, 8); // riffSize
|
|
if (uint.MaxValue == riffChunkSize)
|
|
{
|
|
riffChunkSize = StreamUtils.DecodeInt64(data64);
|
|
riffChunkSizePos = source.Position - 8;
|
|
formattedRiffChunkSize = getFormattedRiffChunkSize(riffChunkSize, isRf64);
|
|
}
|
|
|
|
source.Read(data64, 0, 8); // dataSize
|
|
AudioDataSize = StreamUtils.DecodeInt64(data64);
|
|
|
|
source.Read(data64, 0, 8); // sampleCount
|
|
sampleNumber = StreamUtils.DecodeInt64(data64);
|
|
|
|
source.Read(data, 0, 4); // wave table length
|
|
uint tableLength = StreamUtils.DecodeUInt32(data);
|
|
source.Seek(tableLength, SeekOrigin.Current); // wave table
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_FORMAT, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
source.Read(data, 0, 2);
|
|
if (isLittleEndian) formatId = StreamUtils.DecodeUInt16(data); else formatId = StreamUtils.DecodeBEUInt16(data);
|
|
|
|
source.Read(data, 0, 2);
|
|
if (isLittleEndian) ChannelsArrangement = GuessFromChannelNumber(StreamUtils.DecodeUInt16(data));
|
|
else ChannelsArrangement = GuessFromChannelNumber(StreamUtils.DecodeBEUInt16(data));
|
|
|
|
source.Read(data, 0, 4);
|
|
if (isLittleEndian) sampleRate = StreamUtils.DecodeUInt32(data); else sampleRate = StreamUtils.DecodeBEUInt32(data);
|
|
|
|
source.Read(data, 0, 4);
|
|
if (isLittleEndian) bytesPerSecond = StreamUtils.DecodeUInt32(data); else bytesPerSecond = StreamUtils.DecodeBEUInt32(data);
|
|
|
|
source.Seek(2, SeekOrigin.Current); // BlockAlign
|
|
|
|
source.Read(data, 0, 2);
|
|
if (isLittleEndian) bitsPerSample = StreamUtils.DecodeUInt16(data); else bitsPerSample = StreamUtils.DecodeBEUInt16(data);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_DATA, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
AudioDataOffset = chunkDataPos;
|
|
// Handle RF64 where size has already been set by DS64
|
|
// NB : Some files in the wild can have an erroneous size of 0x00FFFFFF
|
|
if (AudioDataSize > 0 && (uint.MaxValue == chunkSize || 0x00FFFFFF == chunkSize)) chunkSize = AudioDataSize;
|
|
else AudioDataSize = chunkSize;
|
|
headerSize = riffChunkSize - AudioDataSize;
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_FACT, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
source.Read(data, 0, 4);
|
|
uint inputSampleNumber;
|
|
if (isLittleEndian) inputSampleNumber = StreamUtils.DecodeUInt32(data); else inputSampleNumber = StreamUtils.DecodeBEUInt32(data);
|
|
if (inputSampleNumber < uint.MaxValue) sampleNumber = inputSampleNumber;
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_SAMPLE, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
structureHelper.AddZone(source.Position - 8, (int)(chunkSize + paddingSize + 8), subChunkId);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, subChunkId);
|
|
|
|
foundSample = true;
|
|
tagExists = true;
|
|
|
|
SampleTag.FromStream(source, this, readTagParams);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_CUE, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
structureHelper.AddZone(source.Position - 8, (int)(chunkSize + paddingSize + 8), subChunkId);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, subChunkId);
|
|
|
|
foundCue = true;
|
|
tagExists = true;
|
|
|
|
CueTag.FromStream(source, this, readTagParams);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_LIST, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
long initialPosition = source.Position - 8;
|
|
|
|
foundList = true;
|
|
tagExists = true;
|
|
|
|
string purpose = List.FromStream(source, this, readTagParams, chunkSize);
|
|
|
|
structureHelper.AddZone(initialPosition, (int)(chunkSize + 8), CHUNK_LIST + "." + purpose);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_LIST + "." + purpose);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_DISP, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
structureHelper.AddZone(source.Position - 8, (int)(chunkSize + paddingSize + 8), subChunkId + "." + dispIndex);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, subChunkId + "." + dispIndex);
|
|
dispIndex++;
|
|
|
|
foundDisp = true;
|
|
tagExists = true;
|
|
|
|
DispTag.FromStream(source, this, readTagParams, chunkSize);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_BEXT, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
structureHelper.AddZone(source.Position - 8, (int)(chunkSize + paddingSize + 8), subChunkId);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, subChunkId);
|
|
|
|
foundBext = true;
|
|
tagExists = true;
|
|
|
|
BextTag.FromStream(source, this, readTagParams);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_IXML, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
structureHelper.AddZone(source.Position - 8, (int)(chunkSize + paddingSize + 8), subChunkId);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, subChunkId);
|
|
|
|
foundIXml = true;
|
|
tagExists = true;
|
|
|
|
IXmlTag.FromStream(source, this, readTagParams, chunkSize);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_XMP, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
structureHelper.AddZone(source.Position - 8, (int)(chunkSize + paddingSize + 8), subChunkId);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, subChunkId);
|
|
|
|
foundXmp = true;
|
|
tagExists = true;
|
|
|
|
XmpTag.FromStream(source, this, readTagParams, chunkSize);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_CART, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
structureHelper.AddZone(source.Position - 8, (int)(chunkSize + paddingSize + 8), subChunkId);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, subChunkId);
|
|
|
|
foundCart = true;
|
|
tagExists = true;
|
|
|
|
CartTag.FromStream(source, this, readTagParams, chunkSize);
|
|
}
|
|
else if (subChunkId.Equals(CHUNK_ID3, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
id3v2Offset = source.Position;
|
|
|
|
// Zone is already added by Id3v2.Read
|
|
id3v2StructureHelper.AddZone(id3v2Offset - 8, (int)(chunkSize + paddingSize + 8), CHUNK_ID3);
|
|
id3v2StructureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_ID3);
|
|
}
|
|
|
|
source.Seek(chunkDataPos + chunkSize, SeekOrigin.Begin);
|
|
}
|
|
|
|
// Add zone placeholders for future tag writing
|
|
long eof = source.Length;
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
if (!foundSample)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_SAMPLE);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_SAMPLE);
|
|
}
|
|
if (!foundCue)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_CUE);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_CUE);
|
|
}
|
|
if (!foundList)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_LIST + "." + List.PURPOSE_INFO);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_LIST + "." + List.PURPOSE_INFO);
|
|
|
|
structureHelper.AddZone(eof, 0, CHUNK_LIST + "." + List.PURPOSE_ADTL);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_LIST + "." + List.PURPOSE_ADTL);
|
|
}
|
|
if (!foundDisp)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_DISP + ".0");
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_DISP + ".0");
|
|
}
|
|
if (!foundBext)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_BEXT);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_BEXT);
|
|
}
|
|
if (!foundIXml)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_IXML);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_IXML);
|
|
}
|
|
if (!foundXmp)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_XMP);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_XMP);
|
|
}
|
|
if (!foundCart)
|
|
{
|
|
structureHelper.AddZone(eof, 0, CHUNK_CART);
|
|
structureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_CART);
|
|
}
|
|
}
|
|
|
|
// ID3 zone should be set as the very last one for Windows to be able to read the LIST INFO zone properly
|
|
if (-1 == id3v2Offset)
|
|
{
|
|
id3v2Offset = 0; // Switch status to "tried to read, but nothing found"
|
|
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
id3v2StructureHelper.AddZone(eof, 0, CHUNK_ID3);
|
|
id3v2StructureHelper.AddSize(riffChunkSizePos, formattedRiffChunkSize, CHUNK_ID3);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static object getFormattedRiffChunkSize(long input, bool isRf64)
|
|
{
|
|
if (isRf64) return input;
|
|
return (uint)input;
|
|
}
|
|
|
|
private string getFormat()
|
|
{
|
|
// Get format type name
|
|
return formatId switch
|
|
{
|
|
1 => WAV_FORMAT_PCM,
|
|
2 => WAV_FORMAT_ADPCM,
|
|
6 => WAV_FORMAT_ALAW,
|
|
7 => WAV_FORMAT_MULAW,
|
|
17 => WAV_FORMAT_DVI_IMA_ADPCM,
|
|
85 => WAV_FORMAT_MP3,
|
|
_ => "Unknown"
|
|
};
|
|
}
|
|
|
|
private double getDuration()
|
|
{
|
|
// Get duration
|
|
double result = 0;
|
|
|
|
if (sampleNumber == 0 && bytesPerSecond > 0)
|
|
result = (double)(sizeInfo.FileSize - headerSize - sizeInfo.ID3v1Size) / bytesPerSecond;
|
|
if (sampleNumber > 0 && sampleRate > 0)
|
|
result = sampleNumber * 1.0 / sampleRate;
|
|
|
|
return result * 1000.0;
|
|
}
|
|
|
|
private double getBitrate()
|
|
{
|
|
return Math.Round(bitsPerSample / 1000.0 * sampleRate * ChannelsArrangement.NbChannels);
|
|
}
|
|
|
|
public bool Read(Stream source, SizeInfo sizeInfo, ReadTagParams readTagParams)
|
|
{
|
|
this.sizeInfo = sizeInfo;
|
|
|
|
return read(source, readTagParams);
|
|
}
|
|
|
|
protected override bool read(Stream source, ReadTagParams readTagParams)
|
|
{
|
|
resetData();
|
|
|
|
if (!readWAV(source, readTagParams)) return false;
|
|
|
|
// Process data if loaded and header valid
|
|
BitRate = getBitrate();
|
|
Duration = getDuration();
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override int write(TagData tag, Stream s, string zone)
|
|
{
|
|
using BinaryWriter w = new BinaryWriter(s, Encoding.UTF8, true);
|
|
return write(w, zone);
|
|
}
|
|
|
|
private int write(BinaryWriter w, string zone)
|
|
{
|
|
int result = 0;
|
|
|
|
switch (zone)
|
|
{
|
|
case CHUNK_SAMPLE when SampleTag.IsDataEligible(this):
|
|
result += SampleTag.ToStream(w, isLittleEndian, this);
|
|
break;
|
|
case CHUNK_CUE when CueTag.IsDataEligible(this):
|
|
result += CueTag.ToStream(w, isLittleEndian, this);
|
|
break;
|
|
default:
|
|
{
|
|
if (zone.StartsWith(CHUNK_LIST) && List.IsDataEligible(this))
|
|
{
|
|
string[] zoneParts = zone.Split('.');
|
|
if (zoneParts.Length > 1) result += List.ToStream(w, isLittleEndian, zoneParts[1], this);
|
|
}
|
|
else if (zone.Equals(CHUNK_DISP + ".0") && DispTag.IsDataEligible(this)) result += DispTag.ToStream(w, isLittleEndian, this); // Process the 1st position as a whole
|
|
else if (zone.Equals(CHUNK_BEXT) && BextTag.IsDataEligible(this)) result += BextTag.ToStream(w, isLittleEndian, this);
|
|
else if (zone.Equals(CHUNK_IXML) && IXmlTag.IsDataEligible(this)) result += IXmlTag.ToStream(w, isLittleEndian, this);
|
|
else if (zone.Equals(CHUNK_XMP) && XmpTag.IsDataEligible(this)) result += XmpTag.ToStream(w, this, isLittleEndian, true);
|
|
else if (zone.Equals(CHUNK_CART) && CartTag.IsDataEligible(this)) result += CartTag.ToStream(w, isLittleEndian, this);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public void WriteID3v2EmbeddingHeader(Stream s, long tagSize)
|
|
{
|
|
var span = new Span<byte>(new byte[4]);
|
|
|
|
Utils.Latin1Encoding.GetBytes(CHUNK_ID3.AsSpan(), span);
|
|
s.Write(span);
|
|
if (isLittleEndian) BinaryPrimitives.WriteInt32LittleEndian(span, (int)tagSize);
|
|
else BinaryPrimitives.WriteInt32BigEndian(span, (int)tagSize);
|
|
s.Write(span);
|
|
}
|
|
|
|
public void WriteID3v2EmbeddingFooter(Stream s, long tagSize)
|
|
{
|
|
if (tagSize % 2 > 0) s.WriteByte(0);
|
|
}
|
|
}
|
|
} |