mirror of
https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git
synced 2024-11-20 10:27:45 +00:00
505 lines
19 KiB
C#
505 lines
19 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
using ATL.Logging;
|
|
using System.Text;
|
|
using static ATL.AudioData.AudioDataManager;
|
|
using Commons;
|
|
using static ATL.ChannelsArrangements;
|
|
using System.Linq;
|
|
using static ATL.TagData;
|
|
|
|
namespace ATL.AudioData.IO
|
|
{
|
|
/// <summary>
|
|
/// Class for Noisetracker/Soundtracker/Protracker Module files manipulation (extensions : .MOD)
|
|
/// Based on info obtained from Thunder's readme (MODFIL10.TXT - Version 1.0)
|
|
/// </summary>
|
|
class MOD : MetaDataIO, IAudioDataIO
|
|
{
|
|
private const string ZONE_TITLE = "title";
|
|
|
|
private const string SIG_POWERPACKER = "PP20";
|
|
private const byte NB_CHANNELS_DEFAULT = 4;
|
|
private const byte MAX_ROWS = 64;
|
|
|
|
private const byte DEFAULT_TICKS_PER_ROW = 6;
|
|
private const byte DEFAULT_BPM = 125;
|
|
|
|
// Effects
|
|
#pragma warning disable S1144 // Unused private types or members should be removed
|
|
#pragma warning disable IDE0051 // Remove unused private members
|
|
private const byte EFFECT_POSITION_JUMP = 0xB;
|
|
private const byte EFFECT_PATTERN_BREAK = 0xD;
|
|
private const byte EFFECT_SET_SPEED = 0xF;
|
|
private const byte EFFECT_EXTENDED = 0xE;
|
|
|
|
private const byte EFFECT_EXTENDED_LOOP = 0x6;
|
|
private const byte EFFECT_NOTE_CUT = 0xC;
|
|
private const byte EFFECT_NOTE_DELAY = 0xD;
|
|
private const byte EFFECT_PATTERN_DELAY = 0xE;
|
|
private const byte EFFECT_INVERT_LOOP = 0xF;
|
|
#pragma warning restore IDE0051 // Remove unused private members
|
|
#pragma warning restore S1144 // Unused private types or members should be removed
|
|
|
|
private static readonly IDictionary<string, ModFormat> modFormats = new Dictionary<string, ModFormat>
|
|
{
|
|
{ "M.K.", new ModFormat("ProTracker", "M.K.", 31, 4) },
|
|
{ "M!K!", new ModFormat("ProTracker", "M!K!", 31, 4) },
|
|
{ "FLT4", new ModFormat("StarTrekker", "FLT4", 31, 4)},
|
|
{ "2CHN", new ModFormat("FastTracker", "2CHN", 31, 2)},
|
|
{ "4CHN", new ModFormat("FastTracker", "4CHN", 31, 4)},
|
|
{ "6CHN", new ModFormat("FastTracker", "6CHN", 31, 6)},
|
|
{ "8CHN", new ModFormat("FastTracker", "8CHN", 31, 8)},
|
|
{ "OCTA", new ModFormat("FastTracker", "OCTA", 31, 8)},
|
|
{ "FLT8", new ModFormat("StarTrekker", "FLT8", 31, 8)},
|
|
{ "CD81", new ModFormat("Falcon", "CD81", 31, 8) },
|
|
{ "10CH", new ModFormat("FastTracker", "10CH", 31, 10)},
|
|
{ "12CH", new ModFormat("FastTracker", "12CH", 31, 12)},
|
|
{ "14CH", new ModFormat("FastTracker", "14CH", 31, 14)},
|
|
{ "11CH", new ModFormat("TakeTracker", "11CH", 31, 11)},
|
|
{ "13CH", new ModFormat("TakeTracker", "13CH", 31, 13)},
|
|
{ "15CH", new ModFormat("TakeTracker", "15CH", 31, 15)},
|
|
{ "16CH", new ModFormat("FastTracker", "16CH", 31, 16)},
|
|
{ "17CH", new ModFormat("FastTracker", "17CH", 31, 17)},
|
|
{ "18CH", new ModFormat("FastTracker", "18CH", 31, 18)},
|
|
{ "19CH", new ModFormat("FastTracker", "19CH", 31, 19)},
|
|
{ "20CH", new ModFormat("FastTracker", "20CH", 31, 20)},
|
|
{ "21CH", new ModFormat("FastTracker", "21CH", 31, 21)},
|
|
{ "22CH", new ModFormat("FastTracker", "22CH", 31, 22)},
|
|
{ "23CH", new ModFormat("FastTracker", "23CH", 31, 23)},
|
|
{ "24CH", new ModFormat("FastTracker", "24CH", 31, 24)},
|
|
{ "25CH", new ModFormat("FastTracker", "25CH", 31, 25)},
|
|
{ "26CH", new ModFormat("FastTracker", "26CH", 31, 26)},
|
|
{ "27CH", new ModFormat("FastTracker", "27CH", 31, 27)},
|
|
{ "28CH", new ModFormat("FastTracker", "28CH", 31, 28)},
|
|
{ "29CH", new ModFormat("FastTracker", "29CH", 31, 29)},
|
|
{ "30CH", new ModFormat("FastTracker", "30CH", 31, 30)},
|
|
{ "31CH", new ModFormat("FastTracker", "31CH", 31, 31)},
|
|
{ "32CH", new ModFormat("FastTracker", "32CH", 31, 32)},
|
|
{ "33CH", new ModFormat("FastTracker", "33CH", 31, 33)},
|
|
{ "TDZ1", new ModFormat("TakeTracker", "TDZ1", 31, 1)},
|
|
{ "TDZ2", new ModFormat("TakeTracker", "TDZ2", 31, 2)},
|
|
{ "TDZ3", new ModFormat("TakeTracker", "TDZ3", 31, 3)},
|
|
{ "5CHN", new ModFormat("TakeTracker", "5CHN", 31, 5)},
|
|
{ "7CHN", new ModFormat("TakeTracker", "7CHN", 31, 7)},
|
|
{ "9CHN", new ModFormat("TakeTracker", "9CHN", 31, 9)}
|
|
};
|
|
|
|
// Standard fields
|
|
private IList<Sample> FSamples;
|
|
private IList<IList<IList<int>>> FPatterns;
|
|
private IList<byte> FPatternTable;
|
|
private byte nbValidPatterns;
|
|
private string formatTag;
|
|
private byte nbChannels;
|
|
|
|
private SizeInfo sizeInfo;
|
|
private readonly Format audioFormat;
|
|
|
|
|
|
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
|
|
|
|
// IAudioDataIO
|
|
/// <inheritdoc/>
|
|
public int SampleRate => 0;
|
|
/// <inheritdoc/>
|
|
public bool IsVBR => false;
|
|
/// <inheritdoc/>
|
|
public Format AudioFormat
|
|
{
|
|
get
|
|
{
|
|
Format f = new Format(audioFormat);
|
|
if (modFormats.TryGetValue(formatTag, out var format))
|
|
f.Name = f.Name + " (" + format.Name + ")";
|
|
else
|
|
f.Name = f.Name + " (Unknown)";
|
|
return f;
|
|
}
|
|
}
|
|
/// <inheritdoc/>
|
|
public int CodecFamily => AudioDataIOFactory.CF_SEQ_WAV;
|
|
/// <inheritdoc/>
|
|
public string FileName { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public double BitRate { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public int BitDepth => -1; // Irrelevant for that format
|
|
/// <inheritdoc/>
|
|
public double Duration { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public ChannelsArrangement ChannelsArrangement => STEREO;
|
|
/// <inheritdoc/>
|
|
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
|
|
{
|
|
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE };
|
|
}
|
|
|
|
public long AudioDataOffset { get; set; }
|
|
public long AudioDataSize { get; set; }
|
|
|
|
|
|
// IMetaDataIO
|
|
/// <inheritdoc/>
|
|
protected override int getDefaultTagOffset() => TO_BUILTIN;
|
|
/// <inheritdoc/>
|
|
protected override MetaDataIOFactory.TagType getImplementedTagType() => MetaDataIOFactory.TagType.NATIVE;
|
|
/// <inheritdoc/>
|
|
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
|
|
// === PRIVATE STRUCTURES/SUBCLASSES ===
|
|
|
|
internal class Sample
|
|
{
|
|
public string Name;
|
|
public int Size;
|
|
public SByte Finetune;
|
|
public byte Volume;
|
|
|
|
public int RepeatOffset;
|
|
public int RepeatLength;
|
|
|
|
public void Reset()
|
|
{
|
|
Name = "";
|
|
Size = 0;
|
|
Finetune = 0;
|
|
Volume = 0;
|
|
|
|
RepeatLength = 0;
|
|
RepeatOffset = 0;
|
|
}
|
|
}
|
|
|
|
internal class ModFormat
|
|
{
|
|
public readonly string Name;
|
|
public readonly string Signature;
|
|
public readonly byte NbSamples = 0;
|
|
public readonly byte NbChannels = 0;
|
|
|
|
public ModFormat(string name, string sig, byte nbSamples, byte nbChannels)
|
|
{
|
|
Name = name;
|
|
Signature = sig;
|
|
NbSamples = nbSamples;
|
|
NbChannels = nbChannels;
|
|
}
|
|
}
|
|
|
|
private void resetData()
|
|
{
|
|
Duration = 0;
|
|
BitRate = 0;
|
|
|
|
FSamples = new List<Sample>();
|
|
FPatterns = new List<IList<IList<int>>>();
|
|
FPatternTable = new List<byte>();
|
|
nbValidPatterns = 0;
|
|
formatTag = "";
|
|
nbChannels = 0;
|
|
AudioDataOffset = -1;
|
|
AudioDataSize = 0;
|
|
|
|
ResetData();
|
|
}
|
|
|
|
public MOD(string filePath, Format format)
|
|
{
|
|
this.FileName = filePath;
|
|
this.audioFormat = format;
|
|
resetData();
|
|
}
|
|
|
|
|
|
|
|
// ---------- SUPPORT METHODS
|
|
|
|
// 24 * beats/minute
|
|
// lines/minute = -----------------
|
|
// ticks/line
|
|
private double calculateDuration()
|
|
{
|
|
double result = 0;
|
|
|
|
// Jump and break control variables
|
|
int currentPattern = 0;
|
|
int currentRow = 0;
|
|
bool positionJump = false;
|
|
bool patternBreak = false;
|
|
|
|
// Loop control variables
|
|
bool isInsideLoop = false;
|
|
double loopDuration = 0;
|
|
|
|
IList<int> row;
|
|
|
|
int temp;
|
|
double ticksPerRow = DEFAULT_TICKS_PER_ROW;
|
|
double bpm = DEFAULT_BPM;
|
|
|
|
int effect;
|
|
int arg1;
|
|
int arg2;
|
|
|
|
do // Patterns loop
|
|
{
|
|
do // Rows loop
|
|
{
|
|
row = FPatterns[FPatternTable[currentPattern]][currentRow];
|
|
foreach (int note in row) // Channels loop
|
|
{
|
|
effect = (note & 0xF00) >> 8;
|
|
arg1 = (note & 0xF0) >> 4;
|
|
arg2 = note & 0xF;
|
|
|
|
if (effect.Equals(EFFECT_SET_SPEED))
|
|
{
|
|
temp = arg1 * 16 + arg2;
|
|
if (temp > 32) // BPM
|
|
{
|
|
bpm = temp;
|
|
}
|
|
else // ticks per row
|
|
{
|
|
ticksPerRow = temp;
|
|
}
|
|
}
|
|
else if (effect.Equals(EFFECT_POSITION_JUMP))
|
|
{
|
|
temp = arg1 * 16 + arg2;
|
|
|
|
// Processes position jump only if the jump is forward
|
|
// => Prevents processing "forced" song loops ad infinitum
|
|
if (temp > currentPattern)
|
|
{
|
|
currentPattern = temp;
|
|
currentRow = 0;
|
|
positionJump = true;
|
|
}
|
|
}
|
|
else if (effect.Equals(EFFECT_PATTERN_BREAK))
|
|
{
|
|
currentPattern++;
|
|
currentRow = arg1 * 10 + arg2;
|
|
patternBreak = true;
|
|
}
|
|
else if (effect.Equals(EFFECT_EXTENDED))
|
|
{
|
|
if (arg1.Equals(EFFECT_EXTENDED_LOOP))
|
|
{
|
|
if (arg2.Equals(0)) // Beginning of loop
|
|
{
|
|
loopDuration = 0;
|
|
isInsideLoop = true;
|
|
}
|
|
else // End of loop + nb. repeat indicator
|
|
{
|
|
result += loopDuration * arg2;
|
|
isInsideLoop = false;
|
|
}
|
|
}
|
|
}
|
|
if (positionJump || patternBreak) break;
|
|
} // end channels loop
|
|
|
|
result += 60 * (ticksPerRow / (24 * bpm));
|
|
if (isInsideLoop) loopDuration += 60 * (ticksPerRow / (24 * bpm));
|
|
|
|
if (positionJump || patternBreak) break;
|
|
|
|
currentRow++;
|
|
} while (currentRow < MAX_ROWS);
|
|
|
|
if (positionJump || patternBreak)
|
|
{
|
|
positionJump = false;
|
|
patternBreak = false;
|
|
}
|
|
else
|
|
{
|
|
currentPattern++;
|
|
currentRow = 0;
|
|
}
|
|
} while (currentPattern < nbValidPatterns); // end patterns loop
|
|
|
|
return result * 1000.0;
|
|
}
|
|
|
|
private byte detectNbSamples(BufferedBinaryReader source)
|
|
{
|
|
byte result = 31;
|
|
long position = source.Position;
|
|
|
|
source.Seek(1080, SeekOrigin.Begin);
|
|
|
|
formatTag = Utils.Latin1Encoding.GetString(source.ReadBytes(4)).Trim();
|
|
|
|
if (!modFormats.ContainsKey(formatTag)) result = 15;
|
|
|
|
source.Seek(position, SeekOrigin.Begin);
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// === PUBLIC METHODS ===
|
|
|
|
public bool Read(Stream source, SizeInfo sizeInfo, ReadTagParams readTagParams)
|
|
{
|
|
this.sizeInfo = sizeInfo;
|
|
|
|
return read(source, readTagParams);
|
|
}
|
|
|
|
protected override bool read(Stream source, ReadTagParams readTagParams)
|
|
{
|
|
bool result = true;
|
|
int maxPatterns = -1;
|
|
byte nbSamples;
|
|
|
|
string readString;
|
|
StringBuilder comment = new StringBuilder("");
|
|
|
|
Sample sample;
|
|
IList<IList<int>> pattern;
|
|
IList<int> row;
|
|
|
|
resetData();
|
|
|
|
BufferedBinaryReader reader = new BufferedBinaryReader(source);
|
|
|
|
// == TITLE ==
|
|
readString = Utils.Latin1Encoding.GetString(reader.ReadBytes(4));
|
|
if (readString.Equals(SIG_POWERPACKER))
|
|
{
|
|
throw new InvalidDataException("MOD files compressed with PowerPacker are not supported yet");
|
|
}
|
|
|
|
tagExists = true;
|
|
|
|
// Restart from beginning, else parser might miss empty titles
|
|
reader.Seek(0, SeekOrigin.Begin);
|
|
|
|
// Title = max first 20 chars; null-terminated
|
|
string title = StreamUtils.ReadNullTerminatedStringFixed(reader, Encoding.ASCII, 20);
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
structureHelper.AddZone(0, 20, new byte[20] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, ZONE_TITLE);
|
|
}
|
|
tagData.IntegrateValue(Field.TITLE, title.Trim());
|
|
|
|
AudioDataOffset = reader.Position;
|
|
AudioDataSize = sizeInfo.FileSize - AudioDataOffset;
|
|
|
|
// == SAMPLES ==
|
|
nbSamples = detectNbSamples(reader);
|
|
string charOne = Utils.Latin1Encoding.GetString(new byte[] { 1 });
|
|
|
|
for (int i = 0; i < nbSamples; i++)
|
|
{
|
|
sample = new Sample();
|
|
sample.Name = StreamUtils.ReadNullTerminatedStringFixed(reader, Encoding.ASCII, 22).Trim();
|
|
sample.Name = sample.Name.Replace("\0", "");
|
|
sample.Name = sample.Name.Replace(charOne, "");
|
|
sample.Size = StreamUtils.DecodeBEUInt16(reader.ReadBytes(2)) * 2;
|
|
sample.Finetune = reader.ReadSByte();
|
|
sample.Volume = reader.ReadByte();
|
|
sample.RepeatOffset = StreamUtils.DecodeBEUInt16(reader.ReadBytes(2)) * 2;
|
|
sample.RepeatLength = StreamUtils.DecodeBEUInt16(reader.ReadBytes(2)) * 2;
|
|
FSamples.Add(sample);
|
|
}
|
|
|
|
|
|
// == SONG ==
|
|
nbValidPatterns = reader.ReadByte();
|
|
reader.Seek(1, SeekOrigin.Current); // Controversial byte; no real use here
|
|
for (int i = 0; i < 128; i++) FPatternTable.Add(reader.ReadByte()); // Pattern table
|
|
|
|
// File format tag
|
|
formatTag = Utils.Latin1Encoding.GetString(reader.ReadBytes(4)).Trim();
|
|
if (modFormats.ContainsKey(formatTag))
|
|
{
|
|
nbChannels = modFormats[formatTag].NbChannels;
|
|
}
|
|
else // Default
|
|
{
|
|
nbChannels = NB_CHANNELS_DEFAULT;
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "MOD format tag '" + formatTag + "'not recognized");
|
|
}
|
|
|
|
// == PATTERNS ==
|
|
// Some extra information about the "FLT8" -type MOD's:
|
|
//
|
|
// These MOD's have 8 channels, still the format isn't the same as the
|
|
// other 8 channel formats ("OCTA", "CD81", "8CHN"): instead of storing
|
|
// ONE 8-track pattern, it stores TWO 4-track patterns per logical pattern.
|
|
// i.e. The first 4 channels of the first logical pattern are stored in
|
|
// the first physical 4-channel pattern (size 1kb) whereas channel 5 until
|
|
// channel 8 of the first logical pattern are stored as the SECOND physical
|
|
// 4-channel pattern. Got it? ;-).
|
|
// If you convert all the 4 channel patterns to 8 channel patterns, do not
|
|
// forget to divide each pattern nr by 2 in the pattern sequence table!
|
|
|
|
foreach (byte b in FPatternTable) maxPatterns = Math.Max(maxPatterns, b);
|
|
|
|
for (int p = 0; p < maxPatterns + 1; p++) // Patterns loop
|
|
{
|
|
FPatterns.Add(new List<IList<int>>());
|
|
pattern = FPatterns[FPatterns.Count - 1];
|
|
// Rows loop
|
|
for (int l = 0; l < MAX_ROWS; l++)
|
|
{
|
|
pattern.Add(new List<int>());
|
|
row = pattern[pattern.Count - 1];
|
|
for (int c = 0; c < nbChannels; c++) // Channels loop
|
|
{
|
|
row.Add(StreamUtils.DecodeBEInt32(reader.ReadBytes(4)));
|
|
} // end channels loop
|
|
} // end rows loop
|
|
} // end patterns loop
|
|
|
|
|
|
// == Computing track properties
|
|
|
|
Duration = calculateDuration();
|
|
foreach (var aSample in FSamples.Where(aSample => aSample.Name.Length > 0))
|
|
{
|
|
comment.Append(aSample.Name).Append(Settings.InternalValueSeparator);
|
|
}
|
|
|
|
if (comment.Length > 0) comment.Remove(comment.Length - 1, 1);
|
|
|
|
tagData.IntegrateValue(Field.COMMENT, comment.ToString());
|
|
|
|
BitRate = sizeInfo.FileSize / Duration;
|
|
|
|
return result;
|
|
}
|
|
|
|
protected override int write(TagData tag, Stream s, string zone)
|
|
{
|
|
int result = 0;
|
|
|
|
if (ZONE_TITLE.Equals(zone))
|
|
{
|
|
string title = tag[Field.TITLE];
|
|
if (title.Length > 20) title = title.Substring(0, 20);
|
|
else if (title.Length < 20) title = Utils.BuildStrictLengthString(title, 20, '\0');
|
|
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(title));
|
|
result = 1;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
} |