1
0
mirror of https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git synced 2024-11-20 10:27:45 +00:00
VoiceCraft-MCBE_Proximity_Chat/ATL/AudioData/IO/WMA.cs
2024-07-13 11:16:08 +10:00

1126 lines
48 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using Commons;
using static ATL.ChannelsArrangements;
using System.Linq;
using System.Collections.Concurrent;
using static ATL.TagData;
using System.Threading.Tasks;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for Windows Media Audio 7,8 and 9 files manipulation (extension : .WMA)
/// </summary>
internal partial class WMA : MetaDataIO, IAudioDataIO
{
private const string ZONE_CONTENT_DESCRIPTION = "contentDescription";
private const string ZONE_EXTENDED_CONTENT_DESCRIPTION = "extContentDescription";
private const string ZONE_EXTENDED_HEADER_METADATA = "extHeaderMeta";
private const string ZONE_EXTENDED_HEADER_METADATA_LIBRARY = "extHeaderMetaLibrary";
// Object IDs
private static readonly byte[] WMA_HEADER_ID = { 48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108 };
private static readonly byte[] WMA_HEADER_EXTENSION_ID = { 0xB5, 0x03, 0xBF, 0x5F, 0x2E, 0xA9, 0xCF, 0x11, 0x8E, 0xE3, 0x00, 0xc0, 0x0c, 0x20, 0x53, 0x65 };
private static readonly byte[] WMA_METADATA_OBJECT_ID = { 0xEA, 0xCB, 0xF8, 0xC5, 0xAF, 0x5B, 0x77, 0x48, 0x84, 0x67, 0xAA, 0x8C, 0x44, 0xFA, 0x4C, 0xCA };
private static readonly byte[] WMA_METADATA_LIBRARY_OBJECT_ID = { 0x94, 0x1C, 0x23, 0x44, 0x98, 0x94, 0xD1, 0x49, 0xA1, 0x41, 0x1D, 0x13, 0x4E, 0x45, 0x70, 0x54 };
private static readonly byte[] WMA_FILE_PROPERTIES_ID = { 161, 220, 171, 140, 71, 169, 207, 17, 142, 228, 0, 192, 12, 32, 83, 101 };
private static readonly byte[] WMA_STREAM_PROPERTIES_ID = { 145, 7, 220, 183, 183, 169, 207, 17, 142, 230, 0, 192, 12, 32, 83, 101 };
private static readonly byte[] WMA_CONTENT_DESCRIPTION_ID = { 51, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108 };
private static readonly byte[] WMA_EXTENDED_CONTENT_DESCRIPTION_ID = { 64, 164, 208, 210, 7, 227, 210, 17, 151, 240, 0, 160, 201, 94, 168, 80 };
private static readonly byte[] WMA_LANGUAGE_LIST_OBJECT_ID = { 0xA9, 0x46, 0x43, 0x7C, 0xE0, 0xEF, 0xFC, 0x4B, 0xB2, 0x29, 0x39, 0x3E, 0xDE, 0x41, 0x5C, 0x85 };
// Format IDs
#pragma warning disable S1144 // Unused private types or members should be removed
// ReSharper disable UnusedMember.Local
#pragma warning disable IDE0051 // Remove unused private members
private const int WMA_ID = 0x161;
private const int WMA_PRO_ID = 0x162;
private const int WMA_LOSSLESS_ID = 0x163;
private const int WMA_GSM_CBR_ID = 0x7A21;
private const int WMA_GSM_VBR_ID = 0x7A22;
// Max. number of characters in tag field
private const byte WMA_MAX_STRING_SIZE = 250;
#pragma warning restore IDE0051 // Remove unused private members
// ReSharper restore UnusedMember.Local
#pragma warning restore S1144 // Unused private types or members should be removed
// File data - for internal use
private sealed class FileData
{
public long HeaderSize;
public int FormatTag; // Format ID tag
public ushort Channels; // Number of channels
public int SampleRate; // Sample rate (hz)
public uint ObjectCount; // Number of high-level objects
public long ObjectListOffset; // Offset of the high-level objects list
public FileData() { Reset(); }
private void Reset()
{
HeaderSize = 0;
FormatTag = 0;
Channels = 0;
SampleRate = 0;
ObjectCount = 0;
ObjectListOffset = -1;
}
}
private FileData fileData;
private bool isLossless;
// Mapping between WMA frame codes and ATL frame codes
// NB : WM/TITLE, WM/AUTHOR, WM/COPYRIGHT, WM/DESCRIPTION and WM/RATING are not WMA extended fields; therefore
// their ID will not appear as is in the WMA header.
// Their info is contained in the standard Content Description block at the very beginning of the file
public static readonly IDictionary<string, Field> frameMapping = new Dictionary<string, Field>
{
{ "WM/TITLE", Field.TITLE },
{ "WM/AlbumTitle", Field.ALBUM },
{ "WM/AUTHOR", Field.ARTIST },
{ "WM/COPYRIGHT", Field.COPYRIGHT },
{ "WM/DESCRIPTION", Field.COMMENT },
{ "WM/Year", Field.RECORDING_YEAR },
{ "WM/Genre", Field.GENRE },
{ "WM/TrackNumber", Field.TRACK_NUMBER_TOTAL },
{ "WM/PartOfSet", Field.DISC_NUMBER_TOTAL },
{ "WM/RATING", Field.RATING },
{ "WM/SharedUserRating", Field.RATING },
{ "WM/Composer", Field.COMPOSER },
{ "WM/AlbumArtist", Field.ALBUM_ARTIST },
{ "WM/Conductor", Field.CONDUCTOR },
{ "WM/Lyrics", Field.LYRICS_UNSYNCH },
{ "WM/AlbumSortOrder", Field.SORT_ALBUM },
{ "WM/ArtistSortOrder", Field.SORT_ARTIST },
{ "WM/TitleSortOrder", Field.SORT_TITLE },
{ "WM/ContentGroupDescription", Field.GROUP },
{ "WM/BeatsPerMinute", Field.BPM },
{ "WM/EncodedBy", Field.ENCODED_BY },
{ "WM/OriginalReleaseTime", Field.PUBLISHING_DATE }, // De facto standard as specs don't define any dedicated field to store a full date
{ "WM/OriginalReleaseYear", Field.ORIG_RELEASE_YEAR },
{ "WM/ToolName", Field.ENCODER },
{ "WM/Language", Field.LANGUAGE },
{ "WM/ISRC", Field.ISRC },
{ "WM/CatalogNo", Field.CATALOG_NUMBER },
{ "WM/AudioSourceURL", Field.AUDIO_SOURCE_URL },
{ "WM/Writer", Field.LYRICIST }
};
public static readonly IDictionary<string, Field> frameMappingLower = new Dictionary<string, Field>();
public static readonly IDictionary<Field, string> invertedFrameMapping = new Dictionary<Field, string>();
// Field that are embedded in standard ASF description, and do not need to be written in any other frame
private static readonly IList<string> embeddedFields = new List<string>
{
"WM/TITLE",
"WM/AUTHOR",
"WM/COPYRIGHT",
"WM/DESCRIPTION",
"WM/RATING"
};
// Mapping between WMA frame codes and frame classes that aren't class 0 (Unicode string)
// To be further populated while reading
private static readonly ConcurrentDictionary<string, ushort> frameClasses = new ConcurrentDictionary<string, ushort>
{
["WM/SharedUserRating"] = 3
};
private IList<string> languages; // Optional language index described in the WMA header
private AudioDataManager.SizeInfo m_sizeInfo;
// Keep these in memory to prevent setting them twice using AdditionalFields
private readonly ISet<string> m_writtenFieldCodes = new HashSet<string>();
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
// IAudioDataIO
/// <inheritdoc/>
public int SampleRate { get; private set; }
/// <inheritdoc/>
public bool IsVBR { get; private set; }
/// <inheritdoc/>
public Format AudioFormat
{
get;
}
/// <inheritdoc/>
public int CodecFamily => isLossless ? AudioDataIOFactory.CF_LOSSLESS : AudioDataIOFactory.CF_LOSSY;
/// <inheritdoc/>
public string FileName { get; }
/// <inheritdoc/>
public double BitRate { get; private set; }
/// <inheritdoc/>
public int BitDepth => 16; // Seems to be constant
/// <inheritdoc/>
public double Duration { get; private set; }
/// <inheritdoc/>
public ChannelsArrangement ChannelsArrangement { get; private set; }
/// <inheritdoc/>
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
{
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE, MetaDataIOFactory.TagType.ID3V2, MetaDataIOFactory.TagType.APE, MetaDataIOFactory.TagType.ID3V1 };
}
/// <inheritdoc/>
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
{
Field supportedMetaId = Field.NO_FIELD;
// Finds the ATL field identifier
if (frameMappingLower.TryGetValue(ID.ToLower(), out var value)) supportedMetaId = value;
return supportedMetaId;
}
/// <inheritdoc/>
public long AudioDataOffset { get; set; }
/// <inheritdoc/>
public long AudioDataSize { get; set; }
// IMetaDataIO
/// <inheritdoc/>
protected override int getDefaultTagOffset()
{
return TO_BUILTIN;
}
/// <inheritdoc/>
protected override MetaDataIOFactory.TagType getImplementedTagType()
{
return MetaDataIOFactory.TagType.NATIVE;
}
/// <inheritdoc/>
protected override byte ratingConvention => RC_ASF;
// ---------- CONSTRUCTORS & INITIALIZERS
private void resetData()
{
SampleRate = 0;
IsVBR = false;
isLossless = false;
BitRate = 0;
Duration = 0;
AudioDataOffset = -1;
AudioDataSize = 0;
ResetData();
}
private static void generateLowerMappings()
{
foreach (var mapping in frameMapping)
{
frameMappingLower[mapping.Key.ToLower()] = mapping.Value;
}
}
/// <summary>
/// Constructor
/// </summary>
public WMA(string filePath, Format format)
{
FileName = filePath;
AudioFormat = format;
resetData();
generateLowerMappings();
}
// ---------- SUPPORT METHODS
private static void addFrameClass(string frameCode, ushort frameClass)
{
frameClasses.TryAdd(frameCode, frameClass);
}
private void cacheLanguageIndex(Stream source)
{
if (null == languages)
{
languages = new List<string>();
var initialPosition = source.Position;
source.Seek(fileData.ObjectListOffset, SeekOrigin.Begin);
BinaryReader r = new BinaryReader(source);
for (int i = 0; i < fileData.ObjectCount; i++)
{
var position = source.Position;
var bytes = r.ReadBytes(16);
var objectSize = r.ReadUInt64();
// Language index (optional; one only -- useful to map language codes to extended header tag information)
if (WMA_LANGUAGE_LIST_OBJECT_ID.SequenceEqual(bytes))
{
ushort nbLanguages = r.ReadUInt16();
for (int j = 0; j < nbLanguages; j++)
{
var strLen = r.ReadByte();
long position2 = source.Position;
if (strLen > 2) languages.Add(Utils.StripEndingZeroChars(Encoding.Unicode.GetString(r.ReadBytes(strLen))));
source.Seek(position2 + strLen, SeekOrigin.Begin);
}
}
source.Seek(position + (long)objectSize, SeekOrigin.Begin);
}
source.Seek(initialPosition, SeekOrigin.Begin);
}
}
private ushort encodeLanguage(Stream source, string languageCode)
{
if (null == languages) cacheLanguageIndex(source);
if (0 == languages!.Count) return 0;
return (ushort)languages.IndexOf(languageCode);
}
private string decodeLanguage(Stream source, ushort languageIndex)
{
if (null == languages) cacheLanguageIndex(source);
if (languages!.Count > 0)
{
if (languageIndex < languages.Count) return languages[languageIndex];
return languages[0]; // Index out of bounds
}
return "";
}
private void readContentDescription(BufferedBinaryReader source, ReadTagParams readTagParams)
{
ushort[] fieldSize = new ushort[5];
// Read standard field sizes
for (int i = 0; i < 5; i++) fieldSize[i] = source.ReadUInt16();
// Read standard field values
for (int i = 0; i < 5; i++)
{
if (fieldSize[i] > 0)
{
// Read field value
var fieldValue = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode);
// Set corresponding tag field if supported
switch (i)
{
case 0: SetMetaField("WM/TITLE", fieldValue, readTagParams.ReadAllMetaFrames, ZONE_CONTENT_DESCRIPTION); break;
case 1: SetMetaField("WM/AUTHOR", fieldValue, readTagParams.ReadAllMetaFrames, ZONE_CONTENT_DESCRIPTION); break;
case 2: SetMetaField("WM/COPYRIGHT", fieldValue, readTagParams.ReadAllMetaFrames, ZONE_CONTENT_DESCRIPTION); break;
case 3: SetMetaField("WM/DESCRIPTION", fieldValue, readTagParams.ReadAllMetaFrames, ZONE_CONTENT_DESCRIPTION); break;
case 4: SetMetaField("WM/RATING", fieldValue, readTagParams.ReadAllMetaFrames, ZONE_CONTENT_DESCRIPTION); break;
}
}
}
}
private void readHeaderExtended(BufferedBinaryReader source, long sizePosition1, ulong size1, long sizePosition2, ulong size2, ReadTagParams readTagParams)
{
source.Seek(16, SeekOrigin.Current); // Reserved field 1
source.Seek(2, SeekOrigin.Current); // Reserved field 2
var sizePosition3 = source.Position;
uint headerExtendedSize = source.ReadUInt32(); // Size of actual data
// Looping through header extension objects
var position = source.Position;
var limit = (ulong)position + headerExtendedSize;
while ((ulong)position < limit)
{
var framePosition = source.Position;
var headerExtensionObjectId = source.ReadBytes(16);
var headerExtensionObjectSize = source.ReadUInt64();
// Additional metadata (Optional frames)
if (WMA_METADATA_OBJECT_ID.SequenceEqual(headerExtensionObjectId) || WMA_METADATA_LIBRARY_OBJECT_ID.SequenceEqual(headerExtensionObjectId))
{
ushort nbObjects = source.ReadUInt16();
bool isLibraryObject = WMA_METADATA_LIBRARY_OBJECT_ID.SequenceEqual(headerExtensionObjectId);
string zoneCode = isLibraryObject ? ZONE_EXTENDED_HEADER_METADATA_LIBRARY : ZONE_EXTENDED_HEADER_METADATA;
structureHelper.AddZone(framePosition, (int)headerExtensionObjectSize, zoneCode);
// Store frame information for future editing, since current frame is optional
if (readTagParams.PrepareForWriting)
{
structureHelper.AddSize(sizePosition1, size1, zoneCode);
structureHelper.AddSize(sizePosition2, size2, zoneCode);
structureHelper.AddSize(sizePosition3, headerExtendedSize, zoneCode);
}
for (int i = 0; i < nbObjects; i++)
{
var languageIndex = source.ReadUInt16();
var streamNumber = source.ReadUInt16();
// Length (in bytes) of Name field
var nameSize = source.ReadUInt16();
// Type of data stored in current field
var fieldDataType = source.ReadUInt16();
// Size of data stored in current field
var fieldDataSize = source.ReadInt32();
// Name of current field
var fieldName = Utils.StripEndingZeroChars(Encoding.Unicode.GetString(source.ReadBytes(nameSize)));
var dataPosition = source.Position;
readTagField(source, zoneCode, fieldName, fieldDataType, fieldDataSize, readTagParams, true, languageIndex, streamNumber);
source.Seek(dataPosition + fieldDataSize, SeekOrigin.Begin);
}
}
source.Seek(position + (long)headerExtensionObjectSize, SeekOrigin.Begin);
position = source.Position;
}
// Add absent zone definitions for further editing
if (readTagParams.PrepareForWriting)
{
if (!structureHelper.ZoneNames.Contains(ZONE_EXTENDED_HEADER_METADATA))
{
structureHelper.AddZone(source.Position, 0, ZONE_EXTENDED_HEADER_METADATA);
structureHelper.AddSize(sizePosition1, size1, ZONE_EXTENDED_HEADER_METADATA);
structureHelper.AddSize(sizePosition2, size2, ZONE_EXTENDED_HEADER_METADATA);
structureHelper.AddSize(sizePosition3, headerExtendedSize, ZONE_EXTENDED_HEADER_METADATA);
}
if (!structureHelper.ZoneNames.Contains(ZONE_EXTENDED_HEADER_METADATA_LIBRARY))
{
structureHelper.AddZone(source.Position, 0, ZONE_EXTENDED_HEADER_METADATA_LIBRARY);
structureHelper.AddSize(sizePosition1, size1, ZONE_EXTENDED_HEADER_METADATA_LIBRARY);
structureHelper.AddSize(sizePosition2, size2, ZONE_EXTENDED_HEADER_METADATA_LIBRARY);
structureHelper.AddSize(sizePosition3, headerExtendedSize, ZONE_EXTENDED_HEADER_METADATA_LIBRARY);
}
}
}
private void readExtendedContentDescription(BufferedBinaryReader source, ReadTagParams readTagParams)
{
// Read extended tag data
var fieldCount = source.ReadUInt16();
for (int iterator1 = 0; iterator1 < fieldCount; iterator1++)
{
// Read field name
var dataSize = source.ReadUInt16();
var fieldName = Utils.StripEndingZeroChars(Encoding.Unicode.GetString(source.ReadBytes(dataSize)));
// Read value data type
var dataType = source.ReadUInt16();
dataSize = source.ReadUInt16();
var dataPosition = source.Position;
readTagField(source, ZONE_EXTENDED_CONTENT_DESCRIPTION, fieldName, dataType, dataSize, readTagParams);
source.Seek(dataPosition + dataSize, SeekOrigin.Begin);
}
}
public void readTagField(BufferedBinaryReader source, string zoneCode, string fieldName, ushort fieldDataType, int fieldDataSize, ReadTagParams readTagParams, bool isExtendedHeader = false, ushort languageIndex = 0, ushort streamNumber = 0)
{
string fieldValue = "";
bool setMeta = true;
addFrameClass(fieldName, fieldDataType);
switch (fieldDataType)
{
// Unicode string
case 0:
fieldValue = Utils.StripEndingZeroChars(Encoding.Unicode.GetString(source.ReadBytes(fieldDataSize)));
break;
// Byte array
case 1 when fieldName.ToUpper().Equals("WM/PICTURE"):
{
byte picCode = source.ReadByte();
// TODO factorize : abstract PictureTypeDecoder + unsupported / supported decision in MetaDataIO ?
PictureInfo.PIC_TYPE picType = ID3v2.DecodeID3v2PictureType(picCode);
int picturePosition;
if (picType.Equals(PictureInfo.PIC_TYPE.Unsupported))
{
picturePosition = takePicturePosition(MetaDataIOFactory.TagType.NATIVE, picCode);
}
else
{
picturePosition = takePicturePosition(picType);
}
if (readTagParams.ReadPictures)
{
int picSize = source.ReadInt32();
StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // MIME type
string description = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode);
PictureInfo picInfo = PictureInfo.fromBinaryData(source, picSize, picType, getImplementedTagType(), picCode, picturePosition);
picInfo.Description = description;
tagData.Pictures.Add(picInfo);
}
setMeta = false;
break;
}
case 1:
fieldValue = Utils.Latin1Encoding.GetString(Utils.EncodeTo64(source.ReadBytes(fieldDataSize)));
break;
// 16-bit Boolean (metadata); 32-bit Boolean (extended header)
case 2:
fieldValue = isExtendedHeader ? source.ReadUInt32().ToString() : source.ReadUInt16().ToString();
break;
// 32-bit unsigned integer
case 3:
{
uint intValue = source.ReadUInt32();
if (fieldName.Equals("WM/GENRE", StringComparison.OrdinalIgnoreCase)) intValue++;
fieldValue = intValue.ToString();
break;
}
// 64-bit unsigned integer
case 4:
fieldValue = source.ReadUInt64().ToString();
break;
// 16-bit unsigned integer
case 5:
fieldValue = source.ReadUInt16().ToString();
break;
// 128-bit GUID; unused for now
case 6:
fieldValue = Utils.Latin1Encoding.GetString(Utils.EncodeTo64(source.ReadBytes(fieldDataSize)));
break;
}
if (setMeta) SetMetaField(fieldName.Trim(), fieldValue, readTagParams.ReadAllMetaFrames, zoneCode, 0, streamNumber, decodeLanguage(source, languageIndex));
}
public static bool IsValidHeader(byte[] data)
{
return StreamUtils.ArrBeginsWith(data, WMA_HEADER_ID);
}
private bool readData(Stream source, ReadTagParams readTagParams)
{
bool result = false;
languages?.Clear();
BufferedBinaryReader reader = new BufferedBinaryReader(source);
reader.Seek(m_sizeInfo.ID3v2Size, SeekOrigin.Begin);
var initialPos = reader.Position;
// Check for existing header
var ID = reader.ReadBytes(16);
// Header (mandatory; one only)
if (IsValidHeader(ID))
{
var sizePosition1 = reader.Position;
var headerSize = reader.ReadUInt64();
var countPosition = reader.Position;
var objectCount = reader.ReadUInt32();
reader.Seek(2, SeekOrigin.Current); // Reserved data
fileData.ObjectCount = objectCount;
fileData.ObjectListOffset = reader.Position;
// Read all objects in header and get needed data
for (int i = 0; i < objectCount; i++)
{
var position = reader.Position;
ID = reader.ReadBytes(16);
var sizePosition2 = reader.Position;
var objectSize = reader.ReadUInt64();
// File properties (mandatory; one only)
if (WMA_FILE_PROPERTIES_ID.SequenceEqual(ID))
{
reader.Seek(40, SeekOrigin.Current);
Duration = reader.ReadUInt64() / 10000.0; // Play duration (100-nanoseconds)
reader.Seek(8, SeekOrigin.Current); // Send duration; unused for now
Duration -= reader.ReadUInt64(); // Preroll duration (ms)
}
// Stream properties (mandatory; one per stream)
else if (WMA_STREAM_PROPERTIES_ID.SequenceEqual(ID))
{
reader.Seek(54, SeekOrigin.Current);
fileData.FormatTag = reader.ReadUInt16();
fileData.Channels = reader.ReadUInt16();
fileData.SampleRate = reader.ReadInt32();
}
// Content description (optional; one only)
// -> standard, pre-defined metadata
else if (WMA_CONTENT_DESCRIPTION_ID.SequenceEqual(ID) && readTagParams.ReadTag)
{
tagExists = true;
structureHelper.AddZone(position, (int)objectSize, ZONE_CONTENT_DESCRIPTION);
// Store frame information for future editing, since current frame is optional
if (readTagParams.PrepareForWriting)
{
structureHelper.AddSize(sizePosition1, headerSize, ZONE_CONTENT_DESCRIPTION);
structureHelper.AddCounter(countPosition, objectCount, ZONE_CONTENT_DESCRIPTION);
}
readContentDescription(reader, readTagParams);
}
// Extended content description (optional; one only)
// -> extended, dynamic metadata
else if (WMA_EXTENDED_CONTENT_DESCRIPTION_ID.SequenceEqual(ID) && readTagParams.ReadTag)
{
tagExists = true;
structureHelper.AddZone(position, (int)objectSize, ZONE_EXTENDED_CONTENT_DESCRIPTION);
// Store frame information for future editing, since current frame is optional
if (readTagParams.PrepareForWriting)
{
structureHelper.AddSize(sizePosition1, headerSize, ZONE_EXTENDED_CONTENT_DESCRIPTION);
structureHelper.AddCounter(countPosition, objectCount, ZONE_EXTENDED_CONTENT_DESCRIPTION);
}
readExtendedContentDescription(reader, readTagParams);
}
// Header extension (mandatory; one only)
// -> extended, dynamic additional metadata such as additional embedded pictures (any picture after the 1st one stored in extended content)
else if (WMA_HEADER_EXTENSION_ID.SequenceEqual(ID) && readTagParams.ReadTag)
{
readHeaderExtended(reader, sizePosition1, headerSize, sizePosition2, objectSize, readTagParams);
}
reader.Seek(position + (long)objectSize, SeekOrigin.Begin);
}
// Add absent zone definitions for further editing
if (readTagParams.PrepareForWriting)
{
if (!structureHelper.ZoneNames.Contains(ZONE_CONTENT_DESCRIPTION))
{
structureHelper.AddZone(reader.Position, 0, ZONE_CONTENT_DESCRIPTION);
structureHelper.AddSize(sizePosition1, headerSize, ZONE_CONTENT_DESCRIPTION);
structureHelper.AddCounter(countPosition, objectCount, ZONE_CONTENT_DESCRIPTION);
}
if (!structureHelper.ZoneNames.Contains(ZONE_EXTENDED_CONTENT_DESCRIPTION))
{
structureHelper.AddZone(reader.Position, 0, ZONE_EXTENDED_CONTENT_DESCRIPTION);
structureHelper.AddSize(sizePosition1, headerSize, ZONE_EXTENDED_CONTENT_DESCRIPTION);
structureHelper.AddCounter(countPosition, objectCount, ZONE_EXTENDED_CONTENT_DESCRIPTION);
}
}
result = true;
}
fileData.HeaderSize = reader.Position - initialPos;
AudioDataOffset = reader.Position;
AudioDataSize = m_sizeInfo.FileSize - AudioDataOffset;
return result;
}
private static bool isValid(FileData Data)
{
return Data.Channels > 0 && Data.SampleRate >= 8000 && Data.SampleRate <= 96000;
}
/// <inheritdoc/>
public bool Read(Stream source, AudioDataManager.SizeInfo sizeInfo, ReadTagParams readTagParams)
{
m_sizeInfo = sizeInfo;
return read(source, readTagParams);
}
/// <inheritdoc/>
protected override bool read(Stream source, ReadTagParams readTagParams)
{
fileData = new FileData();
resetData();
bool result = readData(source, readTagParams);
// Process data if loaded and valid
if (result && isValid(fileData))
{
ChannelsArrangement = GuessFromChannelNumber(fileData.Channels);
SampleRate = fileData.SampleRate;
BitRate = (m_sizeInfo.FileSize - m_sizeInfo.TotalTagSize - fileData.HeaderSize) * 8.0 / Duration;
IsVBR = WMA_GSM_VBR_ID == fileData.FormatTag;
isLossless = WMA_LOSSLESS_ID == fileData.FormatTag;
}
return result;
}
protected override void preprocessWrite(TagData dataToWrite)
{
base.preprocessWrite(dataToWrite);
m_writtenFieldCodes.Clear();
}
/// <inheritdoc/>
protected override int write(TagData tag, Stream s, string zone)
{
using BinaryWriter w = new BinaryWriter(s, Encoding.UTF8, true);
return write(tag, w, zone);
}
private int write(TagData tag, BinaryWriter w, string zone)
{
computePicturesDestination(tag.Pictures);
return zone switch
{
ZONE_CONTENT_DESCRIPTION => writeContentDescription(tag, m_writtenFieldCodes, w),
ZONE_EXTENDED_HEADER_METADATA => writeExtendedHeaderMeta(tag, w),
ZONE_EXTENDED_HEADER_METADATA_LIBRARY => writeExtendedHeaderMetaLibrary(tag, w),
ZONE_EXTENDED_CONTENT_DESCRIPTION => writeExtendedContentDescription(tag, m_writtenFieldCodes, w),
_ => 0
};
}
private static int writeContentDescription(TagData tag, ISet<string> writtenFieldCodes, BinaryWriter w)
{
var beginPos = w.BaseStream.Position;
w.Write(WMA_CONTENT_DESCRIPTION_ID);
var frameSizePos = w.BaseStream.Position;
w.Write((ulong)0); // Frame size placeholder to be rewritten at the end of the method
string title = "";
string author = "";
string copyright = "";
string comment = "";
string rating = "";
IDictionary<Field, string> map = tag.ToMap();
if (0 == invertedFrameMapping.Count)
{
foreach (var kvp in frameMapping)
{
invertedFrameMapping[kvp.Value] = kvp.Key.ToLower();
}
}
// Supported textual fields
foreach (Field frameType in map.Keys)
{
if (map[frameType].Length <= 0) continue; // No frame with empty value
switch (frameType)
{
case Field.TITLE:
title = map[frameType];
writtenFieldCodes.Add(invertedFrameMapping[frameType]);
break;
case Field.ARTIST:
author = map[frameType];
writtenFieldCodes.Add(invertedFrameMapping[frameType]);
break;
case Field.COPYRIGHT:
copyright = map[frameType];
writtenFieldCodes.Add(invertedFrameMapping[frameType]);
break;
case Field.COMMENT:
comment = map[frameType];
writtenFieldCodes.Add(invertedFrameMapping[frameType]);
break;
case Field.RATING:
rating = map[frameType];
writtenFieldCodes.Add(invertedFrameMapping[frameType]);
break;
}
}
// Read standard field sizes (+1 for last null characher; x2 for unicode)
if (title.Length > 0) w.Write((ushort)((title.Length + 1) * 2)); else w.Write((ushort)0);
if (author.Length > 0) w.Write((ushort)((author.Length + 1) * 2)); else w.Write((ushort)0);
if (copyright.Length > 0) w.Write((ushort)((copyright.Length + 1) * 2)); else w.Write((ushort)0);
if (comment.Length > 0) w.Write((ushort)((comment.Length + 1) * 2)); else w.Write((ushort)0);
if (rating.Length > 0) w.Write((ushort)((rating.Length + 1) * 2)); else w.Write((ushort)0);
if (title.Length > 0) w.Write(Encoding.Unicode.GetBytes(title + '\0'));
if (author.Length > 0) w.Write(Encoding.Unicode.GetBytes(author + '\0'));
if (copyright.Length > 0) w.Write(Encoding.Unicode.GetBytes(copyright + '\0'));
if (comment.Length > 0) w.Write(Encoding.Unicode.GetBytes(comment + '\0'));
if (rating.Length > 0) w.Write(Encoding.Unicode.GetBytes(rating + '\0'));
// Go back to frame size locations to write their actual size
var finalFramePos = w.BaseStream.Position;
w.BaseStream.Seek(frameSizePos, SeekOrigin.Begin);
w.Write(Convert.ToUInt64(finalFramePos - beginPos));
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
return (title.Length > 0 ? 1 : 0) + (author.Length > 0 ? 1 : 0) + (copyright.Length > 0 ? 1 : 0) + (comment.Length > 0 ? 1 : 0) + (rating.Length > 0 ? 1 : 0);
}
private int writeExtendedContentDescription(TagData tag, ISet<string> writtenFieldCodes, BinaryWriter w)
{
ushort counter = 0;
var beginPos = w.BaseStream.Position;
w.Write(WMA_EXTENDED_CONTENT_DESCRIPTION_ID);
var frameSizePos = w.BaseStream.Position;
w.Write((ulong)0); // Frame size placeholder to be rewritten at the end of the method
var counterPos = w.BaseStream.Position;
w.Write((ushort)0); // Counter placeholder to be rewritten at the end of the method
IDictionary<Field, string> map = tag.ToMap();
// Supported textual fields
foreach (Field frameType in map.Keys)
{
foreach (string s in frameMapping.Keys)
{
if (!embeddedFields.Contains(s) && frameType == frameMapping[s])
{
if (map[frameType].Length > 0) // No frame with empty value
{
string value = formatBeforeWriting(frameType, tag, map);
writeTextFrame(w, s, value);
writtenFieldCodes.Add(s.ToLower());
counter++;
}
break;
}
}
}
// Other textual fields
foreach (MetaFieldInfo fieldInfo in tag.AdditionalFields)
{
if ((fieldInfo.TagType.Equals(MetaDataIOFactory.TagType.ANY) || fieldInfo.TagType.Equals(getImplementedTagType()))
&& !fieldInfo.MarkedForDeletion
&& (ZONE_EXTENDED_CONTENT_DESCRIPTION.Equals(fieldInfo.Zone) || "".Equals(fieldInfo.Zone))
&& !writtenFieldCodes.Contains(fieldInfo.NativeFieldCode.ToLower())
)
{
writeTextFrame(w, fieldInfo.NativeFieldCode, FormatBeforeWriting(fieldInfo.Value));
counter++;
}
}
// Picture fields
foreach (PictureInfo picInfo in tag.Pictures)
{
if (0 == picInfo.TransientFlag)
{
writePictureFrame(w, picInfo.PictureData, picInfo.MimeType, picInfo.PicType.Equals(PictureInfo.PIC_TYPE.Unsupported) ? (byte)picInfo.NativePicCode : ID3v2.EncodeID3v2PictureType(picInfo.PicType), picInfo.Description);
counter++;
}
}
// Go back to frame size locations to write their actual size
var finalFramePos = w.BaseStream.Position;
w.BaseStream.Seek(frameSizePos, SeekOrigin.Begin);
w.Write(Convert.ToUInt64(finalFramePos - beginPos));
w.BaseStream.Seek(counterPos, SeekOrigin.Begin);
w.Write(counter);
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
return counter;
}
private int writeExtendedHeaderMeta(TagData tag, BinaryWriter w)
{
var beginPos = w.BaseStream.Position;
w.Write(WMA_METADATA_OBJECT_ID);
var frameSizePos = w.BaseStream.Position;
w.Write((ulong)0); // Frame size placeholder to be rewritten at the end of the method
var counterPos = w.BaseStream.Position;
w.Write((ushort)0); // Counter placeholder to be rewritten at the end of the method
var counter = writeExtendedMeta(tag, w);
// Go back to frame size locations to write their actual size
var finalFramePos = w.BaseStream.Position;
w.BaseStream.Seek(frameSizePos, SeekOrigin.Begin);
w.Write(Convert.ToUInt64(finalFramePos - beginPos));
w.BaseStream.Seek(counterPos, SeekOrigin.Begin);
w.Write(counter);
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
return counter;
}
private int writeExtendedHeaderMetaLibrary(TagData tag, BinaryWriter w)
{
var beginPos = w.BaseStream.Position;
w.Write(WMA_METADATA_LIBRARY_OBJECT_ID);
var frameSizePos = w.BaseStream.Position;
w.Write((ulong)0); // Frame size placeholder to be rewritten at the end of the method
var counterPos = w.BaseStream.Position;
w.Write((ushort)0); // Counter placeholder to be rewritten at the end of the method
var counter = writeExtendedMeta(tag, w, true);
// Go back to frame size locations to write their actual size
var finalFramePos = w.BaseStream.Position;
w.BaseStream.Seek(frameSizePos, SeekOrigin.Begin);
w.Write(Convert.ToUInt64(finalFramePos - beginPos));
w.BaseStream.Seek(counterPos, SeekOrigin.Begin);
w.Write(counter);
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
return counter;
}
private ushort writeExtendedMeta(TagData tag, BinaryWriter w, bool isExtendedMetaLibrary = false)
{
ushort counter = 0;
// Supported textual fields : all current supported fields are located in extended content description frame
// Other textual fields
foreach (MetaFieldInfo fieldInfo in tag.AdditionalFields)
{
if ((fieldInfo.TagType.Equals(MetaDataIOFactory.TagType.ANY) || fieldInfo.TagType.Equals(getImplementedTagType())) && !fieldInfo.MarkedForDeletion)
{
if ((ZONE_EXTENDED_HEADER_METADATA.Equals(fieldInfo.Zone) && !isExtendedMetaLibrary) || (ZONE_EXTENDED_HEADER_METADATA_LIBRARY.Equals(fieldInfo.Zone) && isExtendedMetaLibrary))
{
writeTextFrame(w, fieldInfo.NativeFieldCode, fieldInfo.Value, true, encodeLanguage(w.BaseStream, fieldInfo.Language), fieldInfo.StreamNumber);
counter++;
}
}
}
// Picture fields
if (isExtendedMetaLibrary)
{
foreach (PictureInfo picInfo in tag.Pictures)
{
if (1 == picInfo.TransientFlag)
{
writePictureFrame(w, picInfo.PictureData, picInfo.MimeType, picInfo.PicType.Equals(PictureInfo.PIC_TYPE.Unsupported) ? (byte)picInfo.NativePicCode : ID3v2.EncodeID3v2PictureType(picInfo.PicType), picInfo.Description, true);
counter++;
}
}
}
return counter;
}
private static void writeTextFrame(BinaryWriter writer, string frameCode, string text, bool isExtendedHeader = false, ushort languageIndex = 0, ushort streamNumber = 0)
{
byte[] nameBytes = Encoding.Unicode.GetBytes(frameCode + '\0');
ushort nameSize = (ushort)nameBytes.Length;
if (isExtendedHeader)
{
writer.Write(languageIndex); // Metadata object : Reserved / Metadata library object : Language list index
writer.Write(streamNumber); // Corresponding stream number
}
// Name length and name
writer.Write(nameSize);
if (!isExtendedHeader) writer.Write(nameBytes);
ushort frameClass = 0;
if (frameClasses.TryGetValue(frameCode, out var clazz)) frameClass = clazz;
writer.Write(frameClass);
var dataSizePos = writer.BaseStream.Position;
// Data size placeholder to be rewritten in a few lines
if (isExtendedHeader) writer.Write((uint)0);
else writer.Write((ushort)0);
if (isExtendedHeader) writer.Write(nameBytes);
var dataPos = writer.BaseStream.Position;
switch (frameClass)
{
// Unicode string
case 0:
writer.Write(Encoding.Unicode.GetBytes(text + '\0'));
break;
// Non-picture byte array
case 1:
writer.Write(Utils.DecodeFrom64(Utils.Latin1Encoding.GetBytes(text)));
break;
// 32-bit boolean; 16-bit boolean if in extended header
case 2 when isExtendedHeader:
writer.Write(Utils.ToBoolean(text) ? (ushort)1 : (ushort)0);
break;
case 2:
writer.Write(Utils.ToBoolean(text) ? (uint)1 : (uint)0);
break;
// 32-bit unsigned integer
case 3:
writer.Write(Convert.ToUInt32(text));
break;
// 64-bit unsigned integer
case 4:
writer.Write(Convert.ToUInt64(text));
break;
// 16-bit unsigned integer
case 5:
writer.Write(Convert.ToUInt16(text));
break;
// 128-bit GUID
case 6:
writer.Write(Utils.DecodeFrom64(Utils.Latin1Encoding.GetBytes(text)));
break;
}
// Go back to frame size locations to write their actual size
var finalFramePos = writer.BaseStream.Position;
writer.BaseStream.Seek(dataSizePos, SeekOrigin.Begin);
if (!isExtendedHeader)
{
writer.Write(Convert.ToUInt16(finalFramePos - dataPos));
}
else
{
writer.Write(Convert.ToUInt32(finalFramePos - dataPos));
}
writer.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
}
private static void writePictureFrame(BinaryWriter writer, byte[] pictureData, string mimeType, byte pictureTypeCode, string description, bool isExtendedHeader = false, ushort languageIndex = 0, ushort streamNumber = 0)
{
byte[] nameBytes = Encoding.Unicode.GetBytes("WM/Picture" + '\0');
ushort nameSize = (ushort)nameBytes.Length;
if (isExtendedHeader)
{
writer.Write(languageIndex); // Metadata object : Reserved / Metadata library object : Language list index
writer.Write(streamNumber); // Corresponding stream number
}
// Name length and name
writer.Write(nameSize);
if (!isExtendedHeader) writer.Write(nameBytes);
ushort frameClass = 1;
writer.Write(frameClass);
var dataSizePos = writer.BaseStream.Position;
// Data size placeholder to be rewritten in a few lines
if (isExtendedHeader)
{
writer.Write((uint)0);
}
else
{
writer.Write((ushort)0);
}
if (isExtendedHeader)
{
writer.Write(nameBytes);
}
var dataPos = writer.BaseStream.Position;
writer.Write(pictureTypeCode);
writer.Write(pictureData.Length);
writer.Write(Encoding.Unicode.GetBytes(mimeType + '\0'));
writer.Write(Encoding.Unicode.GetBytes(description + '\0')); // Picture description
writer.Write(pictureData);
// Go back to frame size locations to write their actual size
var finalFramePos = writer.BaseStream.Position;
writer.BaseStream.Seek(dataSizePos, SeekOrigin.Begin);
if (isExtendedHeader)
{
writer.Write(Convert.ToUInt32(finalFramePos - dataPos));
}
else
{
writer.Write(Convert.ToUInt16(finalFramePos - dataPos));
}
writer.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
}
// Specific implementation for conservation of non-WM/xxx fields
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async Task<bool> RemoveAsync(Stream s)
{
if (Settings.ASF_keepNonWMFieldsWhenRemovingTag)
{
TagData tag = prepareRemove();
return await WriteAsync(s, tag);
}
return await base.RemoveAsync(s);
}
private TagData prepareRemove()
{
TagData result = new TagData();
foreach (Field b in frameMapping.Values)
{
result.IntegrateValue(b, "");
}
foreach (var fieldInfo in GetAdditionalFields().Where(fieldInfo => fieldInfo.NativeFieldCode.ToUpper().StartsWith("WM/")))
{
MetaFieldInfo emptyFieldInfo = new MetaFieldInfo(fieldInfo)
{
MarkedForDeletion = true
};
result.AdditionalFields.Add(emptyFieldInfo);
}
return result;
}
// Decides whether picture has to be written and set it to their TransientFlag field
// -1: Nowhere
// 0: In content description
// 1: In extended metadata
private void computePicturesDestination(IEnumerable<PictureInfo> picInfos)
{
bool foundFirstContentDescPicture = false;
foreach (PictureInfo picInfo in picInfos)
{
// Picture has either to be supported, or to come from the right tag standard
bool doWritePicture = !picInfo.PicType.Equals(PictureInfo.PIC_TYPE.Unsupported);
if (!doWritePicture) doWritePicture = getImplementedTagType() == picInfo.TagType;
// It also has not to be marked for deletion
doWritePicture = doWritePicture && !picInfo.MarkedForDeletion;
if (doWritePicture)
{
if (picInfo.PictureData.Length + 50 <= ushort.MaxValue && !foundFirstContentDescPicture)
{
picInfo.TransientFlag = 0;
foundFirstContentDescPicture = true;
}
else picInfo.TransientFlag = 1;
}
else picInfo.TransientFlag = -1;
}
}
}
}