diff --git a/CUETools.Processor/AudioReadWrite.cs b/CUETools.Processor/AudioReadWrite.cs new file mode 100644 index 0000000..a363fab --- /dev/null +++ b/CUETools.Processor/AudioReadWrite.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using FLACDotNet; +using WavPackDotNet; +using APEDotNet; +using CUETools.Codecs; +using CUETools.Codecs.ALAC; +using CUETools.Codecs.LossyWAV; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace CUETools.Processor +{ + public static class AudioReadWrite { + public static IAudioSource GetAudioSource(string path, Stream IO, string extension) + { + switch (extension) + { + case ".wav": + return new WAVReader(path, IO); + case ".m4a": + return new ALACReader(path, IO); +#if !MONO + case ".flac": + return new FLACReader(path, IO); + case ".wv": + return new WavPackReader(path, IO, null); + case ".ape": + return new APEReader(path, IO); +#endif + default: + throw new Exception("Unsupported audio type."); + } + } + + public static IAudioSource GetAudioSource(string path, Stream IO) + { + string extension = Path.GetExtension(path).ToLower(); + string filename = Path.GetFileNameWithoutExtension(path); + string secondExtension = Path.GetExtension(filename).ToLower(); + if (secondExtension != ".lossy" && secondExtension != ".lwcdf") + return GetAudioSource(path, IO, extension); + + string lossyPath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(filename) + ".lossy" + extension); + string lwcdfPath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(filename) + ".lwcdf" + extension); + IAudioSource lossySource = GetAudioSource(lossyPath, null, extension); + IAudioSource lwcdfSource = GetAudioSource(lwcdfPath, null, extension); + return new LossyWAVReader(lossySource, lwcdfSource); + } + + public static IAudioDest GetAudioDest(string path, int bitsPerSample, int channelCount, int sampleRate, long finalSampleCount, string extension, CUEConfig config) { + IAudioDest dest; + switch (extension) { + case ".wav": + dest = new WAVWriter(path, bitsPerSample, channelCount, sampleRate, null); + break; +#if !MONO + case ".flac": + dest = new FLACWriter(path, bitsPerSample, channelCount, sampleRate); + ((FLACWriter)dest).CompressionLevel = (int)config.flacCompressionLevel; + ((FLACWriter)dest).Verify = config.flacVerify; + break; + case ".wv": + dest = new WavPackWriter(path, bitsPerSample, channelCount, sampleRate); + ((WavPackWriter)dest).CompressionMode = config.wvCompressionMode; + ((WavPackWriter)dest).ExtraMode = config.wvExtraMode; + ((WavPackWriter)dest).MD5Sum = config.wvStoreMD5; + break; + case ".ape": + dest = new APEWriter(path, bitsPerSample, channelCount, sampleRate); + ((APEWriter)dest).CompressionLevel = (int)config.apeCompressionLevel; + break; + case ".dummy": + dest = new DummyWriter(path, bitsPerSample, channelCount, sampleRate); + break; +#endif + default: + throw new Exception("Unsupported audio type."); + } + dest.FinalSampleCount = finalSampleCount; + return dest; + } + + public static IAudioDest GetAudioDest(string path, long finalSampleCount, CUEConfig config) + { + string extension = Path.GetExtension(path).ToLower(); + string filename = Path.GetFileNameWithoutExtension(path); + if (Path.GetExtension(filename).ToLower() != ".lossy") + { + int bitsPerSample = (config.detectHDCD && config.decodeHDCD) ? (config.decodeHDCDto24bit ? 24 : 20) : 16; + return GetAudioDest(path, bitsPerSample, 2, 44100, finalSampleCount, extension, config); + } + + string lwcdfPath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(filename) + ".lwcdf" + extension); + int destBitsPerSample = (config.detectHDCD && config.decodeHDCD) ? ((!config.decodeHDCDtoLW16 && config.decodeHDCDto24bit) ? 24 : 20) : 16; + int lossyBitsPerSample = (config.detectHDCD && config.decodeHDCD && !config.decodeHDCDtoLW16) ? 24 : 16; + IAudioDest lossyDest = GetAudioDest(path, lossyBitsPerSample, 2, 44100, finalSampleCount, extension, config); + IAudioDest lwcdfDest = GetAudioDest(lwcdfPath, destBitsPerSample, 2, 44100, finalSampleCount, extension, config); + return new LossyWAVWriter(lossyDest, lwcdfDest, destBitsPerSample, 2, 44100, config.lossyWAVQuality); + } + } +} \ No newline at end of file diff --git a/CUETools.Processor/CUETools.Processor.csproj b/CUETools.Processor/CUETools.Processor.csproj new file mode 100644 index 0000000..6ad0da6 --- /dev/null +++ b/CUETools.Processor/CUETools.Processor.csproj @@ -0,0 +1,141 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {4911BD82-49EF-4858-8B51-5394F86739A4} + Library + Properties + CUETools.Processor + CUETools.Processor + + + true + full + false + ..\bin\win32\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + true + ..\bin\win32\Debug\ + DEBUG;TRACE + full + x86 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + true + + + ..\bin\win32\Release\ + TRACE + true + true + pdbonly + x86 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + + + true + ..\bin\x64\Debug\ + DEBUG;TRACE + full + x64 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + true + + + ..\bin\x64\Release\ + TRACE + true + true + pdbonly + x64 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + + + + + + + + + + + + + + + {F2EC7193-D5E5-4252-9803-5CEB407E910F} + CUETools.Codecs.ALAC + + + {9AE965C4-301E-4C01-B90F-297AF341ACC6} + APEDotNet + + + {6458A13A-30EF-45A9-9D58-E5031B17BEE2} + CUETools.Codecs + + + {5802C7E9-157E-4124-946D-70B5AE48A5A1} + CUETools.AccurateRip + + + {1DD41038-D885-46C5-8DDE-E0B82F066584} + CUETools.CDImage + + + {E70FA90A-7012-4A52-86B5-362B699D1540} + FLACDotNet + + + {32338A04-5B6B-4C63-8EE7-C6400F73B5D7} + HDCDDotNet + + + {8A0426FA-0BC2-4C49-A6E5-1F9A68156F19} + CUETools.Codecs.LossyWAV + + + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40} + UnRarDotNet + + + {CC2E74B6-534A-43D8-9F16-AC03FE955000} + WavPackDotNet + + + + + \ No newline at end of file diff --git a/CUETools.Processor/Main.cs b/CUETools.Processor/Main.cs new file mode 100644 index 0000000..176118d --- /dev/null +++ b/CUETools.Processor/Main.cs @@ -0,0 +1,2548 @@ +// **************************************************************************** +// +// CUE Tools +// Copyright (C) 2006-2007 Moitah (moitah@yahoo.com) +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// **************************************************************************** + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using System.Globalization; +using System.IO; +using System.Net; +using System.Security.Cryptography; +using System.Threading; +using System.Xml; +using HDCDDotNet; +using CUETools.Codecs; +using CUETools.Codecs.LossyWAV; +using CUETools.CDImage; +using CUETools.AccurateRip; +#if !MONO +using UnRarDotNet; +using FLACDotNet; +#endif + +namespace CUETools.Processor +{ + + public enum OutputAudioFormat + { + WAV, + FLAC, + WavPack, + APE, + NoAudio + } + + public static class General { + public static string FormatExtension(OutputAudioFormat value) + { + switch (value) + { + case OutputAudioFormat.FLAC: return ".flac"; + case OutputAudioFormat.WavPack: return ".wv"; + case OutputAudioFormat.APE: return ".ape"; + case OutputAudioFormat.WAV: return ".wav"; + case OutputAudioFormat.NoAudio: return ".dummy"; + } + return ".wav"; + } + + public static CUELine FindCUELine(List list, string command) { + command = command.ToUpper(); + foreach (CUELine line in list) { + if (line.Params[0].ToUpper() == command) { + return line; + } + } + return null; + } + + public static CUELine FindCUELine(List list, string command, string command2) + { + command = command.ToUpper(); + command2 = command2.ToUpper(); + foreach (CUELine line in list) + { + if (line.Params.Count > 1 && line.Params[0].ToUpper() == command && line.Params[1].ToUpper() == command2) + { + return line; + } + } + return null; + } + + public static void SetCUELine(List list, string command, string value, bool quoted) + { + CUELine line = General.FindCUELine(list, command); + if (line == null) + { + line = new CUELine(); + line.Params.Add(command); line.IsQuoted.Add(false); + line.Params.Add(value); line.IsQuoted.Add(true); + list.Add(line); + } + else + { + line.Params[1] = value; + line.IsQuoted[1] = quoted; + } + } + + public static void SetCUELine(List list, string command, string command2, string value, bool quoted) + { + CUELine line = General.FindCUELine(list, command, command2); + if (line == null) + { + line = new CUELine(); + line.Params.Add(command); line.IsQuoted.Add(false); + line.Params.Add(command2); line.IsQuoted.Add(false); + line.Params.Add(value); line.IsQuoted.Add(true); + list.Add(line); + } + else + { + line.Params[2] = value; + line.IsQuoted[2] = quoted; + } + } + + public static string ReplaceMultiple(string s, List find, List replace) + { + if (find.Count != replace.Count) + { + throw new ArgumentException(); + } + StringBuilder sb; + int iChar, iFind; + string f; + bool found; + + sb = new StringBuilder(); + + for (iChar = 0; iChar < s.Length; iChar++) + { + found = false; + for (iFind = 0; iFind < find.Count; iFind++) + { + f = find[iFind]; + if ((f.Length <= (s.Length - iChar)) && (s.Substring(iChar, f.Length) == f)) + { + if (replace[iFind] == null) + { + return null; + } + sb.Append(replace[iFind]); + iChar += f.Length - 1; + found = true; + break; + } + } + + if (!found) + { + sb.Append(s[iChar]); + } + } + + return sb.ToString(); + } + + public static string EmptyStringToNull(string s) + { + return ((s != null) && (s.Length == 0)) ? null : s; + } + } + + public enum CUEStyle { + SingleFileWithCUE, + SingleFile, + GapsPrepended, + GapsAppended, + GapsLeftOut + } + + public class CUEConfig { + public uint fixWhenConfidence; + public uint fixWhenPercent; + public uint encodeWhenConfidence; + public uint encodeWhenPercent; + public bool encodeWhenZeroOffset; + public bool writeArTagsOnVerify; + public bool writeArLogOnVerify; + public bool writeArTagsOnConvert; + public bool writeArLogOnConvert; + public bool fixOffset; + public bool noUnverifiedOutput; + public bool autoCorrectFilenames; + public bool flacVerify; + public uint flacCompressionLevel; + public uint apeCompressionLevel; + public bool preserveHTOA; + public int wvCompressionMode; + public int wvExtraMode; + public bool wvStoreMD5; + public bool keepOriginalFilenames; + public string trackFilenameFormat; + public string singleFilenameFormat; + public bool removeSpecial; + public string specialExceptions; + public bool replaceSpaces; + public bool embedLog; + public bool fillUpCUE; + public bool filenamesANSISafe; + public bool bruteForceDTL; + public bool detectHDCD; + public bool decodeHDCD; + public bool wait750FramesForHDCD; + public bool createM3U; + public bool createTOC; + public bool createCUEFileWhenEmbedded; + public bool truncate4608ExtraSamples; + public int lossyWAVQuality; + public bool decodeHDCDtoLW16; + public bool decodeHDCDto24bit; + + public CUEConfig() + { + fixWhenConfidence = 2; + fixWhenPercent = 51; + encodeWhenConfidence = 2; + encodeWhenPercent = 100; + encodeWhenZeroOffset = false; + fixOffset = false; + noUnverifiedOutput = false; + writeArTagsOnConvert = true; + writeArLogOnConvert = true; + writeArTagsOnVerify = false; + writeArLogOnVerify = true; + + autoCorrectFilenames = true; + flacVerify = false; + flacCompressionLevel = 8; + apeCompressionLevel = 2; + preserveHTOA = true; + wvCompressionMode = 1; + wvExtraMode = 0; + wvStoreMD5 = false; + keepOriginalFilenames = true; + trackFilenameFormat = "%N-%A-%T"; + singleFilenameFormat = "%F"; + removeSpecial = false; + specialExceptions = "-()"; + replaceSpaces = true; + embedLog = true; + fillUpCUE = true; + filenamesANSISafe = true; + bruteForceDTL = false; + detectHDCD = true; + wait750FramesForHDCD = true; + decodeHDCD = false; + createM3U = false; + createTOC = false; + createCUEFileWhenEmbedded = false; + truncate4608ExtraSamples = true; + lossyWAVQuality = 5; + decodeHDCDtoLW16 = false; + decodeHDCDto24bit = true; + } + + public void Save (SettingsWriter sw) + { + sw.Save("ArFixWhenConfidence", fixWhenConfidence); + sw.Save("ArFixWhenPercent", fixWhenPercent); + sw.Save("ArEncodeWhenConfidence", encodeWhenConfidence); + sw.Save("ArEncodeWhenPercent", encodeWhenPercent); + sw.Save("ArEncodeWhenZeroOffset", encodeWhenZeroOffset); + sw.Save("ArNoUnverifiedOutput", noUnverifiedOutput); + sw.Save("ArFixOffset", fixOffset); + sw.Save("ArWriteCRC", writeArTagsOnConvert); + sw.Save("ArWriteLog", writeArLogOnConvert); + sw.Save("ArWriteTagsOnVerify", writeArTagsOnVerify); + sw.Save("ArWriteLogOnVerify", writeArLogOnVerify); + + sw.Save("PreserveHTOA", preserveHTOA); + sw.Save("AutoCorrectFilenames", autoCorrectFilenames); + sw.Save("FLACCompressionLevel", flacCompressionLevel); + sw.Save("APECompressionLevel", apeCompressionLevel); + sw.Save("FLACVerify", flacVerify); + sw.Save("WVCompressionMode", wvCompressionMode); + sw.Save("WVExtraMode", wvExtraMode); + sw.Save("WVStoreMD5", wvStoreMD5); + sw.Save("KeepOriginalFilenames", keepOriginalFilenames); + sw.Save("SingleFilenameFormat", singleFilenameFormat); + sw.Save("TrackFilenameFormat", trackFilenameFormat); + sw.Save("RemoveSpecialCharacters", removeSpecial); + sw.Save("SpecialCharactersExceptions", specialExceptions); + sw.Save("ReplaceSpaces", replaceSpaces); + sw.Save("EmbedLog", embedLog); + sw.Save("FillUpCUE", fillUpCUE); + sw.Save("FilenamesANSISafe", filenamesANSISafe); + sw.Save("BruteForceDTL", bruteForceDTL); + sw.Save("DetectHDCD", detectHDCD); + sw.Save("Wait750FramesForHDCD", wait750FramesForHDCD); + sw.Save("DecodeHDCD", decodeHDCD); + sw.Save("CreateM3U", createM3U); + sw.Save("CreateTOC", createTOC); + sw.Save("CreateCUEFileWhenEmbedded", createCUEFileWhenEmbedded); + sw.Save("Truncate4608ExtraSamples", truncate4608ExtraSamples); + sw.Save("LossyWAVQuality", lossyWAVQuality); + sw.Save("DecodeHDCDToLossyWAV16", decodeHDCDtoLW16); + sw.Save("DecodeHDCDTo24bit", decodeHDCDto24bit); + } + + public void Load(SettingsReader sr) + { + fixWhenConfidence = sr.LoadUInt32("ArFixWhenConfidence", 1, 1000) ?? 2; + fixWhenPercent = sr.LoadUInt32("ArFixWhenPercent", 1, 100) ?? 51; + encodeWhenConfidence = sr.LoadUInt32("ArEncodeWhenConfidence", 1, 1000) ?? 2; + encodeWhenPercent = sr.LoadUInt32("ArEncodeWhenPercent", 1, 100) ?? 100; + encodeWhenZeroOffset = sr.LoadBoolean("ArEncodeWhenZeroOffset") ?? false; + noUnverifiedOutput = sr.LoadBoolean("ArNoUnverifiedOutput") ?? false; + fixOffset = sr.LoadBoolean("ArFixOffset") ?? false; + writeArTagsOnConvert = sr.LoadBoolean("ArWriteCRC") ?? true; + writeArLogOnConvert = sr.LoadBoolean("ArWriteLog") ?? true; + writeArTagsOnVerify = sr.LoadBoolean("ArWriteTagsOnVerify") ?? false; + writeArLogOnVerify = sr.LoadBoolean("ArWriteLogOnVerify") ?? true; + + preserveHTOA = sr.LoadBoolean("PreserveHTOA") ?? true; + autoCorrectFilenames = sr.LoadBoolean("AutoCorrectFilenames") ?? true; + flacCompressionLevel = sr.LoadUInt32("FLACCompressionLevel", 0, 8) ?? 8; + flacVerify = sr.LoadBoolean("FLACVerify") ?? false; + apeCompressionLevel = sr.LoadUInt32("APECompressionLevel", 1, 5) ?? 2; + wvCompressionMode = sr.LoadInt32("WVCompressionMode", 0, 3) ?? 1; + wvExtraMode = sr.LoadInt32("WVExtraMode", 0, 6) ?? 0; + wvStoreMD5 = sr.LoadBoolean("WVStoreMD5") ?? false; + keepOriginalFilenames = sr.LoadBoolean("KeepOriginalFilenames") ?? true; + singleFilenameFormat = sr.Load("SingleFilenameFormat") ?? "%F"; + trackFilenameFormat = sr.Load("TrackFilenameFormat") ?? "%N-%A-%T"; + removeSpecial = sr.LoadBoolean("RemoveSpecialCharacters") ?? false; + specialExceptions = sr.Load("SpecialCharactersExceptions") ?? "-()"; + replaceSpaces = sr.LoadBoolean("ReplaceSpaces") ?? true; + embedLog = sr.LoadBoolean("EmbedLog") ?? true; + fillUpCUE = sr.LoadBoolean("FillUpCUE") ?? true; + filenamesANSISafe = sr.LoadBoolean("FilenamesANSISafe") ?? true; + bruteForceDTL = sr.LoadBoolean("BruteForceDTL") ?? false; + detectHDCD = sr.LoadBoolean("DetectHDCD") ?? true; + wait750FramesForHDCD = sr.LoadBoolean("Wait750FramesForHDCD") ?? true; + decodeHDCD = sr.LoadBoolean("DecodeHDCD") ?? false; + createM3U = sr.LoadBoolean("CreateM3U") ?? false; + createTOC = sr.LoadBoolean("CreateTOC") ?? false; + createCUEFileWhenEmbedded = sr.LoadBoolean("CreateCUEFileWhenEmbedded") ?? false; + truncate4608ExtraSamples = sr.LoadBoolean("Truncate4608ExtraSamples") ?? true; + lossyWAVQuality = sr.LoadInt32("LossyWAVQuality", 0, 10) ?? 5; + decodeHDCDtoLW16 = sr.LoadBoolean("DecodeHDCDToLossyWAV16") ?? false; + decodeHDCDto24bit = sr.LoadBoolean("DecodeHDCDTo24bit") ?? true; + } + + public string CleanseString (string s) + { + StringBuilder sb = new StringBuilder(); + char[] invalid = Path.GetInvalidFileNameChars(); + + if (filenamesANSISafe) + s = Encoding.Default.GetString(Encoding.Default.GetBytes(s)); + + for (int i = 0; i < s.Length; i++) + { + char ch = s[i]; + if (filenamesANSISafe && removeSpecial && specialExceptions.IndexOf(ch) < 0 && !( + ((ch >= 'a') && (ch <= 'z')) || + ((ch >= 'A') && (ch <= 'Z')) || + ((ch >= '0') && (ch <= '9')) || + (ch == ' ') || (ch == '_'))) + ch = '_'; + if ((Array.IndexOf(invalid, ch) >= 0) || (replaceSpaces && ch == ' ')) + sb.Append("_"); + else + sb.Append(ch); + } + + return sb.ToString(); + } + } + + public class CUEToolsProgressEventArgs + { + public string status = string.Empty; + public uint percentTrack = 0; + public double percentDisk = 0.0; + public string input = string.Empty; + public string output = string.Empty; + } + + public class ArchivePasswordRequiredEventArgs + { + public string Password = string.Empty; + public bool ContinueOperation = true; + } + + public delegate void CUEToolsProgressHandler(object sender, CUEToolsProgressEventArgs e); + public delegate void ArchivePasswordRequiredHandler(object sender, ArchivePasswordRequiredEventArgs e); + + public class CUESheet { + private bool _stop, _pause; + private List _attributes; + private List _tracks; + private List _sources; + private List _sourcePaths, _trackFilenames; + private string _htoaFilename, _singleFilename; + private bool _hasHTOAFilename, _hasTrackFilenames, _hasSingleFilename, _appliedWriteOffset; + private bool _hasEmbeddedCUESheet; + private bool _paddedToFrame, _truncated4608, _usePregapForFirstTrackInSingleFile; + private int _writeOffset; + private bool _accurateRip, _accurateOffset; + private uint? _dataTrackLength; + private uint? _minDataTrackLength; + private string _accurateRipId; + private string _accurateRipIdActual; + private string _mbDiscId; + private string _mbReleaseId; + private string _eacLog; + private string _cuePath; + private NameValueCollection _albumTags; + private const int _arOffsetRange = 5 * 588 - 1; + private HDCDDotNet.HDCDDotNet hdcdDecoder; + private bool _outputLossyWAV = false; + CUEConfig _config; + string _cddbDiscIdTag; + private bool _isArchive; + private List _archiveContents; + private string _archiveCUEpath; + private string _archivePath; + private string _archivePassword; + private CUEToolsProgressEventArgs _progress; + private AccurateRipVerify _arVerify; + + public event ArchivePasswordRequiredHandler PasswordRequired; + public event CUEToolsProgressHandler CUEToolsProgress; + + public CUESheet(CUEConfig config) + { + _config = config; + _progress = new CUEToolsProgressEventArgs(); + _attributes = new List(); + _tracks = new List(); + _toc = new CDImageLayout(0); + _sources = new List(); + _sourcePaths = new List(); + _albumTags = new NameValueCollection(); + _stop = false; + _pause = false; + _cuePath = null; + _paddedToFrame = false; + _truncated4608 = false; + _usePregapForFirstTrackInSingleFile = false; + _accurateRip = false; + _accurateOffset = false; + _appliedWriteOffset = false; + _dataTrackLength = null; + _minDataTrackLength = null; + hdcdDecoder = null; + _hasEmbeddedCUESheet = false; + _isArchive = false; + } + + public void Open(string pathIn, bool outputLossyWAV) + { + _outputLossyWAV = outputLossyWAV; + if (_config.detectHDCD) + { + try { hdcdDecoder = new HDCDDotNet.HDCDDotNet(2, 44100, ((_outputLossyWAV && _config.decodeHDCDtoLW16) || !_config.decodeHDCDto24bit) ? 20 : 24, _config.decodeHDCD); } + catch { } + } + + string cueDir, lineStr, command, pathAudio = null, fileType; + CUELine line; + TrackInfo trackInfo; + int timeRelativeToFileStart, absoluteFileStartTime; + int fileTimeLengthSamples, fileTimeLengthFrames, i; + int trackNumber = 0; + bool seenFirstFileIndex = false, seenDataTrack = false; + List indexes = new List(); + IndexInfo indexInfo; + SourceInfo sourceInfo; + NameValueCollection _trackTags = null; + + cueDir = Path.GetDirectoryName(pathIn); + trackInfo = null; + absoluteFileStartTime = 0; + fileTimeLengthSamples = 0; + fileTimeLengthFrames = 0; + TextReader sr; + + if (Directory.Exists(pathIn)) + { + if (cueDir + Path.DirectorySeparatorChar != pathIn) + throw new Exception("Input directory must end on path separator character."); + string cueSheet = null; + string[] audioExts = new string[] { "*.wav", "*.flac", "*.wv", "*.ape", "*.m4a" }; + for (i = 0; i < audioExts.Length && cueSheet == null; i++) + cueSheet = CUESheet.CreateDummyCUESheet(pathIn, audioExts[i]); + if (cueSheet == null) + throw new Exception("Input directory doesn't contain supported audio files."); + sr = new StringReader(cueSheet); + } +#if !MONO + else if (Path.GetExtension(pathIn).ToLower() == ".rar") + { + Unrar _unrar = new Unrar(); + _unrar.PasswordRequired += new PasswordRequiredHandler(unrar_PasswordRequired); + string cueName = null, cueText = null; + _unrar.Open(pathIn, Unrar.OpenMode.List); + _archiveContents = new List(); + while (_unrar.ReadHeader()) + { + if (!_unrar.CurrentFile.IsDirectory) + { + _archiveContents.Add(_unrar.CurrentFile.FileName); + if (Path.GetExtension(_unrar.CurrentFile.FileName).ToLower() == ".cue") + cueName = _unrar.CurrentFile.FileName; + } + _unrar.Skip(); + } + _unrar.Close(); + if (cueName != null) + { + RarStream rarStream = new RarStream(pathIn, cueName); + rarStream.PasswordRequired += new PasswordRequiredHandler(unrar_PasswordRequired); + StreamReader cueReader = new StreamReader(rarStream, CUESheet.Encoding); + cueText = cueReader.ReadToEnd(); + cueReader.Close(); + rarStream.Close(); + if (cueText == "") + throw new Exception("Empty cue sheet."); + } + if (cueText == null) + throw new Exception("Input archive doesn't contain a cue sheet."); + _archiveCUEpath = Path.GetDirectoryName(cueName); + sr = new StringReader(cueText); + _isArchive = true; + _archivePath = pathIn; + } +#endif + else if (Path.GetExtension(pathIn).ToLower() == ".cue") + { + if (_config.autoCorrectFilenames) + sr = new StringReader (CorrectAudioFilenames(pathIn, false)); + else + sr = new StreamReader (pathIn, CUESheet.Encoding); + + try + { + StreamReader logReader = new StreamReader(Path.ChangeExtension(pathIn, ".log"), CUESheet.Encoding); + _eacLog = logReader.ReadToEnd(); + logReader.Close(); + } + catch { } + } else + { + IAudioSource audioSource; + NameValueCollection tags; + string cuesheetTag = null; + + audioSource = AudioReadWrite.GetAudioSource(pathIn,null); + tags = audioSource.Tags; + cuesheetTag = tags.Get("CUESHEET"); + _accurateRipId = tags.Get("ACCURATERIPID"); + _eacLog = tags.Get("LOG"); + if (_eacLog == null) _eacLog = tags.Get("LOGFILE"); + if (_eacLog == null) _eacLog = tags.Get("EACLOG"); + audioSource.Close(); + if (cuesheetTag == null) + throw new Exception("Input file does not contain a .cue sheet."); + sr = new StringReader (cuesheetTag); + pathAudio = pathIn; + _hasEmbeddedCUESheet = true; + } + + using (sr) { + while ((lineStr = sr.ReadLine()) != null) { + line = new CUELine(lineStr); + if (line.Params.Count > 0) { + command = line.Params[0].ToUpper(); + + if (command == "FILE") { + fileType = line.Params[2].ToUpper(); + if ((fileType == "BINARY") || (fileType == "MOTOROLA")) { + seenDataTrack = true; + } + else if (seenDataTrack) { + throw new Exception("Audio tracks cannot appear after data tracks."); + } + else { + if (!_hasEmbeddedCUESheet) + { + if (_isArchive) + pathAudio = LocateFile(_archiveCUEpath, line.Params[1], _archiveContents); + else + pathAudio = LocateFile(cueDir, line.Params[1], null); + if (pathAudio == null) + throw new Exception("Unable to locate file \"" + line.Params[1] + "\"."); + } else + { + if (_sourcePaths.Count > 0 ) + throw new Exception("Extra file in embedded CUE sheet: \"" + line.Params[1] + "\"."); + } + _sourcePaths.Add(pathAudio); + absoluteFileStartTime += fileTimeLengthFrames; + NameValueCollection tags; + fileTimeLengthSamples = GetSampleLength(pathAudio, out tags); + if ((fileTimeLengthSamples % 588) == 492 && _config.truncate4608ExtraSamples) + { + _truncated4608 = true; + fileTimeLengthSamples -= 4608; + } + fileTimeLengthFrames = (int)((fileTimeLengthSamples + 587) / 588); + if (_hasEmbeddedCUESheet) + _albumTags = tags; + else + _trackTags = tags; + seenFirstFileIndex = false; + } + } + else if (command == "TRACK") { + if (line.Params[2].ToUpper() != "AUDIO") { + seenDataTrack = true; + } + else if (seenDataTrack) { + throw new Exception("Audio tracks cannot appear after data tracks."); + } + else { + trackNumber = int.Parse(line.Params[1]); + if (trackNumber != _tracks.Count + 1) { + throw new Exception("Invalid track number."); + } + trackInfo = new TrackInfo(); + _tracks.Add(trackInfo); + _toc.AddTrack(new CDTrack((uint)trackNumber, 0, 0, true)); + } + } + else if (seenDataTrack) { + // Ignore lines belonging to data tracks + } + else if (command == "INDEX") { + timeRelativeToFileStart = CDImageLayout.TimeFromString(line.Params[2]); + if (!seenFirstFileIndex) + { + if (timeRelativeToFileStart != 0) + { + throw new Exception("First index must start at file beginning."); + } + if (trackNumber > 0 && _trackTags != null && _trackTags.Count != 0) + _tracks[trackNumber-1]._trackTags = _trackTags; + seenFirstFileIndex = true; + sourceInfo.Path = pathAudio; + sourceInfo.Offset = 0; + sourceInfo.Length = (uint)fileTimeLengthSamples; + _sources.Add(sourceInfo); + if ((fileTimeLengthSamples % 588) != 0) + { + sourceInfo.Path = null; + sourceInfo.Offset = 0; + sourceInfo.Length = (uint)((fileTimeLengthFrames * 588) - fileTimeLengthSamples); + _sources.Add(sourceInfo); + _paddedToFrame = true; + } + } + indexInfo.Track = trackNumber; + indexInfo.Index = Int32.Parse(line.Params[1]); + indexInfo.Time = absoluteFileStartTime + timeRelativeToFileStart; + indexes.Add(indexInfo); + } + else if (command == "PREGAP") { + if (seenFirstFileIndex) { + throw new Exception("Pregap must occur at the beginning of a file."); + } + int pregapLength = CDImageLayout.TimeFromString(line.Params[1]); + indexInfo.Track = trackNumber; + indexInfo.Index = 0; + indexInfo.Time = absoluteFileStartTime; + indexes.Add(indexInfo); + sourceInfo.Path = null; + sourceInfo.Offset = 0; + sourceInfo.Length = (uint)pregapLength * 588; + _sources.Add(sourceInfo); + absoluteFileStartTime += pregapLength; + } + else if (command == "POSTGAP") { + throw new Exception("POSTGAP command isn't supported."); + } + else if ((command == "REM") && + (line.Params.Count >= 3) && + (line.Params[1].Length >= 10) && + (line.Params[1].Substring(0, 10).ToUpper() == "REPLAYGAIN")) + { + // Remove ReplayGain lines + } + else if ((command == "REM") && + (line.Params.Count == 3) && + (line.Params[1].ToUpper() == "DATATRACKLENGTH")) + { + _dataTrackLength = (uint)CDImageLayout.TimeFromString(line.Params[2]); + } + else if ((command == "REM") && + (line.Params.Count == 3) && + (line.Params[1].ToUpper() == "ACCURATERIPID")) + { + _accurateRipId = line.Params[2]; + } + //else if ((command == "REM") && + // (line.Params.Count == 3) && + // (line.Params[1].ToUpper() == "SHORTEN")) + //{ + // fileTimeLengthFrames -= General.TimeFromString(line.Params[2]); + //} + //else if ((command == "REM") && + // (line.Params.Count == 3) && + // (line.Params[1].ToUpper() == "LENGTHEN")) + //{ + // fileTimeLengthFrames += General.TimeFromString(line.Params[2]); + //} + else + { + if (trackInfo != null) + { + trackInfo.Attributes.Add(line); + } + else + { + _attributes.Add(line); + } + } + } + } + sr.Close(); + } + + if (trackNumber == 0) { + throw new Exception("File must contain at least one audio track."); + } + + // Add dummy track for calculation purposes + indexInfo.Track = trackNumber + 1; + indexInfo.Index = 1; + indexInfo.Time = absoluteFileStartTime + fileTimeLengthFrames; + indexes.Add(indexInfo); + + // Calculate the length of each index + for (i = 0; i < indexes.Count - 1; i++) + { + int length = indexes[i + 1].Time - indexes[i].Time; + if (length < 0) + throw new Exception("Indexes must be in chronological order."); + _toc[indexes[i].Track].AddIndex(new CDTrackIndex((uint)indexes[i].Index, (uint)indexes[i].Time, (uint)length)); + } + _toc.Length = (uint) indexes[indexes.Count - 1].Time; + for (i = 1; i <= TrackCount; i++) + { + if (_toc[i].LastIndex < 1) + throw new Exception("Track must have an INDEX 01."); + _toc[i].Start = _toc[i][1].Start; + _toc[i].Length = (i == TrackCount ? _toc.Length - _toc[i].Start : _toc[i+1][1].Start - _toc[i].Start); + } + + // Store the audio filenames, generating generic names if necessary + _hasSingleFilename = (_sourcePaths.Count == 1); + _singleFilename = _hasSingleFilename ? Path.GetFileName(_sourcePaths[0]) : + "Range.wav"; + + _hasHTOAFilename = (_sourcePaths.Count == (TrackCount + 1)); + _htoaFilename = _hasHTOAFilename ? Path.GetFileName(_sourcePaths[0]) : "01.00.wav"; + + _hasTrackFilenames = (_sourcePaths.Count == TrackCount) || _hasHTOAFilename; + _trackFilenames = new List(); + for (i = 0; i < TrackCount; i++) { + _trackFilenames.Add( _hasTrackFilenames ? Path.GetFileName( + _sourcePaths[i + (_hasHTOAFilename ? 1 : 0)]) : String.Format("{0:00}.wav", i + 1) ); + } + + if (_hasTrackFilenames) + for (i = 0; i < TrackCount; i++) + { + TrackInfo track = _tracks[i]; + string artist = track._trackTags.Get("ARTIST"); + string title = track._trackTags.Get("TITLE"); + if (track.Artist == "" && artist != null) + track.Artist = artist; + if (track.Title == "" && title != null) + track.Title = title; + } + if (!_hasEmbeddedCUESheet && _hasSingleFilename) + { + _albumTags = _tracks[0]._trackTags; + _tracks[0]._trackTags = new NameValueCollection(); + } + if (_config.fillUpCUE) + { + if (General.FindCUELine(_attributes, "PERFORMER") == null && GetCommonTag("ALBUM ARTIST") != null) + General.SetCUELine(_attributes, "PERFORMER", GetCommonTag("ALBUM ARTIST"), true); + if (General.FindCUELine(_attributes, "PERFORMER") == null && GetCommonTag("ARTIST") != null) + General.SetCUELine(_attributes, "PERFORMER", GetCommonTag("ARTIST"), true); + if (General.FindCUELine(_attributes, "TITLE") == null && GetCommonTag("ALBUM") != null) + General.SetCUELine(_attributes, "TITLE", GetCommonTag("ALBUM"), true); + if (General.FindCUELine(_attributes, "REM", "DATE") == null && GetCommonTag("DATE") != null) + General.SetCUELine(_attributes, "REM", "DATE", GetCommonTag("DATE"), false); + if (General.FindCUELine(_attributes, "REM", "DATE") == null && GetCommonTag("YEAR") != null) + General.SetCUELine(_attributes, "REM", "DATE", GetCommonTag("YEAR"), false); + if (General.FindCUELine(_attributes, "REM", "GENRE") == null && GetCommonTag("GENRE") != null) + General.SetCUELine(_attributes, "REM", "GENRE", GetCommonTag("GENRE"), true); + } + if (_accurateRipId == null) + _accurateRipId = GetCommonTag("ACCURATERIPID"); + + if (_accurateRipId == null && _dataTrackLength == null && _eacLog != null) + { + sr = new StringReader(_eacLog); + int lastAudioSector = -1; + bool isEACLog = false; + while ((lineStr = sr.ReadLine()) != null) + { + if (!isEACLog) + { + if (!lineStr.StartsWith("Exact Audio Copy")) + break; + isEACLog = true; + } + string[] n = lineStr.Split('|'); + if (n.Length == 5) + try + { + int trNo = Int32.Parse(n[0]); + int trStart = Int32.Parse(n[3]); + int trEnd = Int32.Parse(n[4]); + if (trNo == TrackCount && trEnd > 0) + lastAudioSector = trEnd; + if (trNo == TrackCount + 1 && lastAudioSector != -1 && trEnd > lastAudioSector + (90 + 60) * 75 + 150) + { + _dataTrackLength = (uint)(trEnd - lastAudioSector - (90 + 60) * 75 - 150); + break; + } + } + catch { } + } + } + + CUELine cddbDiscIdLine = General.FindCUELine(_attributes, "REM", "DISCID"); + _cddbDiscIdTag = cddbDiscIdLine != null && cddbDiscIdLine.Params.Count == 3 ? cddbDiscIdLine.Params[2] : null; + if (_cddbDiscIdTag == null) _cddbDiscIdTag = GetCommonTag("DISCID"); + + if (_dataTrackLength != null) + _accurateRipIdActual = _accurateRipId = CalculateAccurateRipId(); + else + { + _accurateRipIdActual = CalculateAccurateRipId(); + if (_accurateRipId == null) + _accurateRipId = _accurateRipIdActual; + } + + _arVerify = new AccurateRipVerify(_toc); + + //if (!_dataTrackLength.HasValue && _cddbDiscIdTag != null) + //{ + // uint cddbDiscIdNum = UInt32.Parse(_cddbDiscIdTag, NumberStyles.HexNumber); + // if ((cddbDiscIdNum & 0xff) == TrackCount) + // { + // _cutOneFrame = true; + // string cddbDiscIdTagCut = CalculateAccurateRipId().Split('-')[2]; + // if (cddbDiscIdTagCut.ToUpper() != _cddbDiscIdTag.ToUpper()) + // _cutOneFrame = false; + // } + //} + } + + public static Encoding Encoding { + get { + return Encoding.Default; + } + } + + private void ShowProgress(string status, uint percentTrack, double percentDisk, string input, string output) + { + if (this.CUEToolsProgress == null) + return; + _progress.status = status; + _progress.percentTrack = percentTrack; + _progress.percentDisk = percentDisk; + _progress.input = input; + _progress.output = output; + this.CUEToolsProgress(this, _progress); + } + +#if !MONO + private void unrar_ExtractionProgress(object sender, ExtractionProgressEventArgs e) + { + if (this.CUEToolsProgress == null) + return; + _progress.percentTrack = (uint)Math.Round(e.PercentComplete); + this.CUEToolsProgress(this, _progress); + } + + private void unrar_PasswordRequired(object sender, PasswordRequiredEventArgs e) + { + if (_archivePassword != null) + { + e.ContinueOperation = true; + e.Password = _archivePassword; + return; + } + if (this.PasswordRequired != null) + { + ArchivePasswordRequiredEventArgs e1 = new ArchivePasswordRequiredEventArgs(); + this.PasswordRequired(this, e1); + if (e1.ContinueOperation && e1.Password != "") + { + _archivePassword = e1.Password; + e.ContinueOperation = true; + e.Password = e1.Password; + return; + } + } + throw new IOException("Password is required for extraction."); + } +#endif + + public string GetCommonTag(string tagName) + { + if (_hasEmbeddedCUESheet || _hasSingleFilename) + return _albumTags.Get(tagName); + if (_hasTrackFilenames) + { + string tagValue = null; + bool commonValue = true; + for (int i = 0; i < TrackCount; i++) + { + TrackInfo track = _tracks[i]; + string newValue = track._trackTags.Get (tagName); + if (tagValue == null) + tagValue = newValue; + else + commonValue = (newValue == null || tagValue == newValue); + } + return commonValue ? tagValue : null; + } + return null; + } + + private static string LocateFile(string dir, string file, List contents) { + List dirList, fileList; + string altDir, path; + + dirList = new List(); + fileList = new List(); + altDir = Path.GetDirectoryName(file); + file = Path.GetFileName(file); + + dirList.Add(dir); + if (altDir.Length != 0) { + dirList.Add(Path.IsPathRooted(altDir) ? altDir : Path.Combine(dir, altDir)); + } + + fileList.Add(file); + fileList.Add(file.Replace(' ', '_')); + fileList.Add(file.Replace('_', ' ')); + + for (int iDir = 0; iDir < dirList.Count; iDir++) { + for (int iFile = 0; iFile < fileList.Count; iFile++) { + path = Path.Combine(dirList[iDir], fileList[iFile]); + if ( (contents == null && File.Exists(path)) + || (contents != null && contents.Contains (path))) + return path; + } + } + + return null; + } + + public void GenerateFilenames (OutputAudioFormat format, string outputPath) + { + _cuePath = outputPath; + + string extension = General.FormatExtension(format); + List find, replace; + string filename; + int iTrack; + + find = new List(); + replace = new List(); + + find.Add("%D"); // 0: Album artist + find.Add("%C"); // 1: Album title + find.Add("%N"); // 2: Track number + find.Add("%A"); // 3: Track artist + find.Add("%T"); // 4: Track title + find.Add("%F"); // 5: Input filename + + replace.Add(General.EmptyStringToNull(_config.CleanseString(Artist))); + replace.Add(General.EmptyStringToNull(_config.CleanseString(Title))); + replace.Add(null); + replace.Add(null); + replace.Add(null); + replace.Add(Path.GetFileNameWithoutExtension(outputPath)); + + if (_outputLossyWAV) + extension = ".lossy" + extension; + if (_config.detectHDCD && _config.decodeHDCD && (!_outputLossyWAV || !_config.decodeHDCDtoLW16)) + { + if (_config.decodeHDCDto24bit ) + extension = ".24bit" + extension; + else + extension = ".20bit" + extension; + } + + if (_config.keepOriginalFilenames && HasSingleFilename) + { + SingleFilename = Path.ChangeExtension(SingleFilename, extension); + } + else + { + filename = General.ReplaceMultiple(_config.singleFilenameFormat, find, replace); + if (filename == null) + filename = "Range"; + filename += extension; + SingleFilename = filename; + } + + for (iTrack = -1; iTrack < TrackCount; iTrack++) + { + bool htoa = (iTrack == -1); + + if (_config.keepOriginalFilenames && htoa && HasHTOAFilename) + { + HTOAFilename = Path.ChangeExtension(HTOAFilename, extension); + } + else if (_config.keepOriginalFilenames && !htoa && HasTrackFilenames) + { + TrackFilenames[iTrack] = Path.ChangeExtension( + TrackFilenames[iTrack], extension); + } + else + { + string trackStr = htoa ? "01.00" : String.Format("{0:00}", iTrack + 1); + string artist = Tracks[htoa ? 0 : iTrack].Artist; + string title = htoa ? "(HTOA)" : Tracks[iTrack].Title; + + replace[2] = trackStr; + replace[3] = General.EmptyStringToNull(_config.CleanseString(artist==""?Artist:artist)); + replace[4] = General.EmptyStringToNull(_config.CleanseString(title)); + + filename = General.ReplaceMultiple(_config.trackFilenameFormat, find, replace); + if (filename == null) + filename = replace[2]; + filename += extension; + + if (htoa) + { + HTOAFilename = filename; + } + else + { + TrackFilenames[iTrack] = filename; + } + } + } + } + + private int GetSampleLength(string path, out NameValueCollection tags) + { + IAudioSource audioSource; + + ShowProgress("Analyzing input file...", 0, 0.0, path, null); +#if !MONO + if (_isArchive) + { + RarStream IO = new RarStream(_archivePath, path); + IO.PasswordRequired += new PasswordRequiredHandler(unrar_PasswordRequired); + IO.ExtractionProgress += new ExtractionProgressHandler(unrar_ExtractionProgress); + audioSource = AudioReadWrite.GetAudioSource(path, IO); + } else +#endif + audioSource = AudioReadWrite.GetAudioSource(path, null); + + if ((audioSource.BitsPerSample != 16) || + (audioSource.ChannelCount != 2) || + (audioSource.SampleRate != 44100) || + (audioSource.Length > Int32.MaxValue)) + { + audioSource.Close(); + throw new Exception("Audio format is invalid."); + } + + tags = audioSource.Tags; + audioSource.Close(); + return (int)audioSource.Length; + } + + public void WriteM3U(string path, CUEStyle style) + { + StringWriter sw = new StringWriter(); + WriteM3U(sw, style); + sw.Close(); + bool utf8Required = CUESheet.Encoding.GetString(CUESheet.Encoding.GetBytes(sw.ToString())) != sw.ToString(); + StreamWriter sw1 = new StreamWriter(path, false, utf8Required ? Encoding.UTF8 : CUESheet.Encoding); + sw1.Write(sw.ToString()); + sw1.Close(); + } + + public void WriteM3U(TextWriter sw, CUEStyle style) + { + int iTrack; + bool htoaToFile = ((style == CUEStyle.GapsAppended) && _config.preserveHTOA && + (_toc.Pregap != 0)); + + if (htoaToFile) { + WriteLine(sw, 0, _htoaFilename); + } + for (iTrack = 0; iTrack < TrackCount; iTrack++) { + WriteLine(sw, 0, _trackFilenames[iTrack]); + } + } + + public void WriteTOC(string path) + { + StreamWriter sw = new StreamWriter(path, false, CUESheet.Encoding); + WriteTOC(sw); + sw.Close(); + } + + public void WriteTOC(TextWriter sw) + { + for (int iTrack = 0; iTrack < TrackCount; iTrack++) + WriteLine(sw, 0, "\t" + _toc[iTrack+1].Start + 150); + } + + public void Write(string path, CUEStyle style) { + StringWriter sw = new StringWriter(); + Write(sw, style); + sw.Close(); + bool utf8Required = CUESheet.Encoding.GetString(CUESheet.Encoding.GetBytes(sw.ToString())) != sw.ToString(); + StreamWriter sw1 = new StreamWriter(path, false, utf8Required?Encoding.UTF8:CUESheet.Encoding); + sw1.Write(sw.ToString()); + sw1.Close(); + } + + public void Write(TextWriter sw, CUEStyle style) { + int i, iTrack, iIndex; + TrackInfo track; + bool htoaToFile = ((style == CUEStyle.GapsAppended) && _config.preserveHTOA && + (_toc.Pregap != 0)); + + uint timeRelativeToFileStart = 0; + + using (sw) { + if (_accurateRipId != null && _config.writeArTagsOnConvert) + WriteLine(sw, 0, "REM ACCURATERIPID " + + _accurateRipId); + + for (i = 0; i < _attributes.Count; i++) { + WriteLine(sw, 0, _attributes[i]); + } + + if (style == CUEStyle.SingleFile || style == CUEStyle.SingleFileWithCUE) { + WriteLine(sw, 0, String.Format("FILE \"{0}\" WAVE", _singleFilename)); + } + if (htoaToFile) { + WriteLine(sw, 0, String.Format("FILE \"{0}\" WAVE", _htoaFilename)); + } + + for (iTrack = 0; iTrack < TrackCount; iTrack++) { + track = _tracks[iTrack]; + + if ((style == CUEStyle.GapsPrepended) || + (style == CUEStyle.GapsLeftOut) || + ((style == CUEStyle.GapsAppended) && + ((_toc[iTrack+1].Pregap == 0) || ((iTrack == 0) && !htoaToFile)))) + { + WriteLine(sw, 0, String.Format("FILE \"{0}\" WAVE", _trackFilenames[iTrack])); + timeRelativeToFileStart = 0; + } + + WriteLine(sw, 1, String.Format("TRACK {0:00} AUDIO", iTrack + 1)); + for (i = 0; i < track.Attributes.Count; i++) { + WriteLine(sw, 2, track.Attributes[i]); + } + + for (iIndex = 0; iIndex <= _toc[iTrack+1].LastIndex; iIndex++) { + if (_toc[iTrack+1][iIndex].Length != 0) { + if ((iIndex == 0) && + ((style == CUEStyle.GapsLeftOut) || + ((style == CUEStyle.GapsAppended) && (iTrack == 0) && !htoaToFile) || + ((style == CUEStyle.SingleFile || style == CUEStyle.SingleFileWithCUE) && (iTrack == 0) && _usePregapForFirstTrackInSingleFile))) + { + WriteLine(sw, 2, "PREGAP " + CDImageLayout.TimeToString(_toc[iTrack + 1][iIndex].Length)); + } + else { + WriteLine(sw, 2, String.Format( "INDEX {0:00} {1}", iIndex, + CDImageLayout.TimeToString(timeRelativeToFileStart))); + timeRelativeToFileStart += _toc[iTrack + 1][iIndex].Length; + + if ((style == CUEStyle.GapsAppended) && (iIndex == 0)) { + WriteLine(sw, 0, String.Format("FILE \"{0}\" WAVE", _trackFilenames[iTrack])); + timeRelativeToFileStart = 0; + } + } + } + } + } + } + } + + private uint sumDigits(uint n) + { + uint r = 0; + while (n > 0) + { + r = r + (n % 10); + n = n / 10; + } + return r; + } + + private string CalculateAccurateRipId () + { + // Calculate the three disc ids used by AR + uint discId1 = 0; + uint discId2 = 0; + uint cddbDiscId = 0; + + for (int iTrack = 1; iTrack <= _toc.TrackCount; iTrack++) + { + discId1 += _toc[iTrack].Start; + discId2 += (_toc[iTrack].Start == 0 ? 1 : _toc[iTrack].Start) * ((uint)iTrack); + cddbDiscId += sumDigits(_toc[iTrack].Start / 75 + 2); + } + uint trackOffset = _toc.Length; + if (_dataTrackLength.HasValue) + { + trackOffset += ((90 + 60) * 75) + 150; // 90 second lead-out, 60 second lead-in, 150 sector gap + cddbDiscId += sumDigits((uint)(trackOffset / 75) + 2); + trackOffset += _dataTrackLength.Value; + } + discId1 += trackOffset; + discId2 += (trackOffset == 0 ? 1 : trackOffset) * ((uint)TrackCount + 1); + + if (!_dataTrackLength.HasValue && _cddbDiscIdTag != null) + { + uint cddbDiscIdNum = UInt32.Parse(_cddbDiscIdTag, NumberStyles.HexNumber); + if ((cddbDiscIdNum & 0xff) == TrackCount + 1) + { + uint lengthFromTag = ((cddbDiscIdNum >> 8) & 0xffff); + _minDataTrackLength = ((lengthFromTag + _toc[1].Start / 75) - 152) * 75 - trackOffset; + } + } + + cddbDiscId = ((cddbDiscId % 255) << 24) + + ((trackOffset / 75 - _toc[1].Start / 75) << 8) + + (uint)(TrackCount + (_dataTrackLength.HasValue ? 1 : 0)); + + discId1 &= 0xFFFFFFFF; + discId2 &= 0xFFFFFFFF; + cddbDiscId &= 0xFFFFFFFF; + + return String.Format("{0:x8}-{1:x8}-{2:x8}", discId1, discId2, cddbDiscId); + } + + private void CalculateMusicBrainzDiscID() { + StringBuilder mbSB = new StringBuilder(); + mbSB.AppendFormat("{0:X2}{1:X2}{2:X8}", 1, TrackCount, _toc.Length + 150); + for (int iTrack = 1; iTrack <= _toc.TrackCount; iTrack++) + mbSB.AppendFormat("{0:X8}", _toc[iTrack].Start + 150); + mbSB.Append(new string('0', (99 - TrackCount) * 8)); + + byte[] hashBytes = (new SHA1CryptoServiceProvider()).ComputeHash(Encoding.ASCII.GetBytes(mbSB.ToString())); + _mbDiscId = Convert.ToBase64String(hashBytes).Replace('+', '.').Replace('/', '_').Replace('=', '-'); + System.Diagnostics.Debug.WriteLine(_mbDiscId); + } + + private void GetMetadataFromMusicBrainz() { + if (_mbDiscId == null) return; + + using (Stream respStream = HttpGetToStream( + "http://musicbrainz.org/ws/1/release/?type=xml&limit=1&discid=" + _mbDiscId)) + { + XmlDocument xd = GetXmlDocument(respStream); + XmlNode xn; + + xn = xd.SelectSingleNode("/metadata/release-list/release"); + if (xn != null) + _mbReleaseId = xn.Attributes["id"].InnerText; + } + + if (_mbReleaseId == null) return; + + using (Stream respStream = HttpGetToStream(String.Format( + "http://musicbrainz.org/ws/1/release/{0}?type=xml&inc=artist+tracks", _mbReleaseId))) + { + string discArtist = null; + string discTitle = null; + XmlDocument xd = GetXmlDocument(respStream); + XmlNode xn; + + XmlNode xnRelease = xd.DocumentElement.SelectSingleNode("/metadata/release"); + if (xnRelease == null) return; + + XmlNodeList xnlTracks = xnRelease.SelectNodes("track-list/track"); + if (xnlTracks.Count != TrackCount) return; + + xn = xnRelease.SelectSingleNode("title"); + if (xn != null) + discTitle = xn.InnerText; + + xn = xnRelease.SelectSingleNode("artist/name"); + if (xn != null) + discArtist = xn.InnerText; + + Artist = discArtist; + Title = discTitle; + + for (int iTrack = 0; iTrack < TrackCount; iTrack++) { + string trackArtist = null; + string trackTitle = null; + XmlNode xnTrack = xnlTracks[iTrack]; + TrackInfo trackInfo = Tracks[iTrack]; + + xn = xnTrack.SelectSingleNode("title"); + if (xn != null) + trackTitle = xn.InnerText; + + xn = xnTrack.SelectSingleNode("artist/name"); + if (xn != null) + trackArtist = xn.InnerText; + + trackInfo.Artist = trackArtist ?? discArtist; + trackInfo.Title = trackTitle; + } + } + } + + private XmlDocument GetXmlDocument(Stream stream) { + XmlDocument xd = new XmlDocument(); + + xd.Load(stream); + + if (xd.DocumentElement.NamespaceURI.Length > 0) { + // Strip namespace to simplify xpath expressions + XmlDocument xdNew = new XmlDocument(); + xd.DocumentElement.SetAttribute("xmlns", String.Empty); + xdNew.LoadXml(xd.OuterXml); + xd = xdNew; + } + + return xd; + } + + private Stream HttpGetToStream(string url) { + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); + req.UserAgent = "CUE Tools"; + try { + HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); + return resp.GetResponseStream(); + } + catch (WebException ex) { + if (ex.Status == WebExceptionStatus.ProtocolError) { + HttpStatusCode code = ((HttpWebResponse)ex.Response).StatusCode; + if (code == HttpStatusCode.NotFound) { + throw new HttpNotFoundException(); + } + } + throw; + } + } + + public void GenerateAccurateRipLog(TextWriter sw) + { + int iTrack; + sw.WriteLine (String.Format("[Disc ID: {0}]", _accurateRipId)); + if (_dataTrackLength.HasValue) + sw.WriteLine("Assuming a data track was present, length {0}.", CDImageLayout.TimeToString(_dataTrackLength.Value)); + else + { + if (_cddbDiscIdTag != null && _accurateRipId.Split('-')[2].ToUpper() != _cddbDiscIdTag.ToUpper()) + sw.WriteLine("CDDBId mismatch: {0} vs {1}", _cddbDiscIdTag.ToUpper(), _accurateRipId.Split('-')[2].ToUpper()); + if (_minDataTrackLength.HasValue) + sw.WriteLine("Data track was probably present, length {0}-{1}.", CDImageLayout.TimeToString(_minDataTrackLength.Value), CDImageLayout.TimeToString(_minDataTrackLength.Value + 74)); + if (_accurateRipIdActual != _accurateRipId) + sw.WriteLine("Using preserved id, actual id is {0}.", _accurateRipIdActual); + if (_truncated4608) + sw.WriteLine("Truncated 4608 extra samples in some input files."); + if (_paddedToFrame) + sw.WriteLine("Padded some input files to a frame boundary."); + } + + if (hdcdDecoder != null && hdcdDecoder.Detected) + { + hdcd_decoder_statistics stats; + hdcdDecoder.GetStatistics(out stats); + sw.WriteLine("HDCD: peak extend: {0}, transient filter: {1}, gain: {2}", + (stats.enabled_peak_extend ? (stats.disabled_peak_extend ? "some" : "yes") : "none"), + (stats.enabled_transient_filter ? (stats.disabled_transient_filter ? "some" : "yes") : "none"), + stats.min_gain_adjustment == stats.max_gain_adjustment ? + (stats.min_gain_adjustment == 1.0 ? "none" : String.Format ("{0:0.0}dB", (Math.Log10(stats.min_gain_adjustment) * 20))) : + String.Format ("{0:0.0}dB..{1:0.0}dB", (Math.Log10(stats.min_gain_adjustment) * 20), (Math.Log10(stats.max_gain_adjustment) * 20)) + ); + } + + if (_arVerify.AccResult == HttpStatusCode.NotFound) + { + sw.WriteLine("Disk not present in database."); + //for (iTrack = 0; iTrack < TrackCount; iTrack++) + // sw.WriteLine(String.Format(" {0:00}\t[{1:x8}] Disk not present in database", iTrack + 1, _tracks[iTrack].CRC)); + } + else if (_arVerify.AccResult != HttpStatusCode.OK) + { + sw.WriteLine("Database access error: " + _arVerify.AccResult.ToString()); + //for (iTrack = 0; iTrack < TrackCount; iTrack++) + // sw.WriteLine(String.Format(" {0:00}\t[{1:x8}] Database access error {2}", iTrack + 1, _tracks[iTrack].CRC, accResult.ToString())); + } + else + { + if (0 != _writeOffset) + sw.WriteLine(String.Format("Offset applied: {0}", _writeOffset)); + int offsetApplied = _accurateOffset ? _writeOffset : 0; + sw.WriteLine(String.Format("Track\t[ CRC ] Status")); + _arVerify.GenerateAccurateRipLog(sw, offsetApplied); + uint offsets_match = 0; + for (int oi = -_arOffsetRange; oi <= _arOffsetRange; oi++) + { + uint matches = 0; + for (iTrack = 0; iTrack < TrackCount; iTrack++) + for (int di = 0; di < (int)_arVerify.AccDisks.Count; di++) + if ((_arVerify.CRC(iTrack, oi) == _arVerify.AccDisks[di].tracks[iTrack].CRC && _arVerify.AccDisks[di].tracks[iTrack].CRC != 0) || + (_arVerify.CRC450(iTrack, oi) == _arVerify.AccDisks[di].tracks[iTrack].Frame450CRC && _arVerify.AccDisks[di].tracks[iTrack].Frame450CRC != 0)) + matches++; + if (matches != 0 && oi != offsetApplied) + { + if (offsets_match++ > 10) + { + sw.WriteLine("More than 10 offsets match!"); + break; + } + sw.WriteLine(String.Format("Offsetted by {0}:", oi)); + _arVerify.GenerateAccurateRipLog(sw, oi); + } + } + } + } + + public void GenerateAccurateRipTagsForTrack(NameValueCollection tags, int offset, int bestOffset, int iTrack, string prefix) + { + uint total = 0; + uint matching = 0; + uint matching2 = 0; + uint matching3 = 0; + for (int iDisk = 0; iDisk < _arVerify.AccDisks.Count; iDisk++) + { + total += _arVerify.AccDisks[iDisk].tracks[iTrack].count; + if (_arVerify.CRC(iTrack, offset) == + _arVerify.AccDisks[iDisk].tracks[iTrack].CRC) + matching += _arVerify.AccDisks[iDisk].tracks[iTrack].count; + if (_arVerify.CRC(iTrack, bestOffset) == + _arVerify.AccDisks[iDisk].tracks[iTrack].CRC) + matching2 += _arVerify.AccDisks[iDisk].tracks[iTrack].count; + for (int oi = -_arOffsetRange; oi <= _arOffsetRange; oi++) + if (_arVerify.CRC(iTrack, oi) == + _arVerify.AccDisks[iDisk].tracks[iTrack].CRC) + matching3 += _arVerify.AccDisks[iDisk].tracks[iTrack].count; + } + tags.Add(String.Format("{0}ACCURATERIPCRC", prefix), String.Format("{0:x8}", _arVerify.CRC(iTrack, offset))); + tags.Add(String.Format("{0}AccurateRipDiscId", prefix), String.Format("{0:000}-{1}-{2:00}", TrackCount, _accurateRipId, iTrack+1)); + tags.Add(String.Format("{0}ACCURATERIPCOUNT", prefix), String.Format("{0}", matching)); + tags.Add(String.Format("{0}ACCURATERIPCOUNTALLOFFSETS", prefix), String.Format("{0}", matching3)); + tags.Add(String.Format("{0}ACCURATERIPTOTAL", prefix), String.Format("{0}", total)); + if (bestOffset != offset) + tags.Add(String.Format("{0}ACCURATERIPCOUNTWITHOFFSET", prefix), String.Format("{0}", matching2)); + } + + public void GenerateAccurateRipTags(NameValueCollection tags, int offset, int bestOffset, int iTrack) + { + tags.Add("ACCURATERIPID", _accurateRipId); + if (bestOffset != offset) + tags.Add("ACCURATERIPOFFSET", String.Format("{1}{0}", bestOffset - offset, bestOffset > offset ? "+" : "")); + if (iTrack != -1) + GenerateAccurateRipTagsForTrack(tags, offset, bestOffset, iTrack, ""); + else + for (iTrack = 0; iTrack < TrackCount; iTrack++) + { + GenerateAccurateRipTagsForTrack(tags, offset, bestOffset, iTrack, + String.Format("cue_track{0:00}_", iTrack + 1)); + } + } + + public void CleanupTags (NameValueCollection tags, string substring) + { + string [] keys = tags.AllKeys; + for (int i = 0; i < keys.Length; i++) + if (keys[i].ToUpper().Contains(substring)) + tags.Remove (keys[i]); + } + + private void FindBestOffset(uint minConfidence, bool optimizeConfidence, out uint outTracksMatch, out int outBestOffset) + { + uint bestTracksMatch = 0; + uint bestConfidence = 0; + int bestOffset = 0; + + for (int offset = -_arOffsetRange; offset <= _arOffsetRange; offset++) + { + uint tracksMatch = 0; + uint sumConfidence = 0; + + for (int iTrack = 0; iTrack < TrackCount; iTrack++) + { + uint confidence = 0; + + for (int di = 0; di < (int)_arVerify.AccDisks.Count; di++) + if (_arVerify.CRC(iTrack, offset) == _arVerify.AccDisks[di].tracks[iTrack].CRC) + confidence += _arVerify.AccDisks[di].tracks[iTrack].count; + + if (confidence >= minConfidence) + tracksMatch++; + + sumConfidence += confidence; + } + + if (tracksMatch > bestTracksMatch + || (tracksMatch == bestTracksMatch && optimizeConfidence && sumConfidence > bestConfidence) + || (tracksMatch == bestTracksMatch && optimizeConfidence && sumConfidence == bestConfidence && Math.Abs(offset) < Math.Abs(bestOffset)) + || (tracksMatch == bestTracksMatch && !optimizeConfidence && Math.Abs(offset) < Math.Abs(bestOffset)) + ) + { + bestTracksMatch = tracksMatch; + bestConfidence = sumConfidence; + bestOffset = offset; + } + } + outBestOffset = bestOffset; + outTracksMatch = bestTracksMatch; + } + + public void WriteAudioFiles(string dir, CUEStyle style) { + string[] destPaths; + int[] destLengths; + bool htoaToFile = ((style == CUEStyle.GapsAppended) && _config.preserveHTOA && + (_toc.Pregap != 0)); + + if (_usePregapForFirstTrackInSingleFile) { + throw new Exception("UsePregapForFirstTrackInSingleFile is not supported for writing audio files."); + } + + if (style == CUEStyle.SingleFile || style == CUEStyle.SingleFileWithCUE) { + destPaths = new string[1]; + destPaths[0] = Path.Combine(dir, _singleFilename); + } + else { + destPaths = new string[TrackCount + (htoaToFile ? 1 : 0)]; + if (htoaToFile) { + destPaths[0] = Path.Combine(dir, _htoaFilename); + } + for (int i = 0; i < TrackCount; i++) { + destPaths[i + (htoaToFile ? 1 : 0)] = Path.Combine(dir, _trackFilenames[i]); + } + } + + if ( !_accurateRip || _accurateOffset ) + for (int i = 0; i < destPaths.Length; i++) { + for (int j = 0; j < _sourcePaths.Count; j++) { + if (destPaths[i].ToLower() == _sourcePaths[j].ToLower()) { + throw new Exception("Source and destination audio file paths cannot be the same."); + } + } + } + + destLengths = CalculateAudioFileLengths(style); + + bool SkipOutput = false; + + if (_accurateRip) { + ShowProgress((string)"Contacting AccurateRip database...", 0, 0, null, null); + if (!_dataTrackLength.HasValue && _minDataTrackLength.HasValue && _accurateRipId == _accurateRipIdActual && _config.bruteForceDTL) + { + uint minDTL = _minDataTrackLength.Value; + for (uint dtl = minDTL; dtl < minDTL + 75; dtl++) + { + _dataTrackLength = dtl; + _accurateRipId = CalculateAccurateRipId(); + _arVerify.ContactAccurateRip(_accurateRipId); + if (_arVerify.AccResult != HttpStatusCode.NotFound) + break; + ShowProgress((string)"Contacting AccurateRip database...", 0, (dtl - minDTL) / 75.0, null, null); + lock (this) { + if (_stop) + throw new StopException(); + if (_pause) + { + ShowProgress("Paused...", 0, 0, null, null); + Monitor.Wait(this); + } + else + Monitor.Wait(this, 1000); + } + } + if (_arVerify.AccResult != HttpStatusCode.OK) + { + _dataTrackLength = null; + _accurateRipId = _accurateRipIdActual; + } + } else + _arVerify.ContactAccurateRip(_accurateRipId); + + if (_arVerify.AccResult != HttpStatusCode.OK) + { + if (!_accurateOffset || _config.noUnverifiedOutput) + { + if ((_accurateOffset && _config.writeArLogOnConvert) || + (!_accurateOffset && _config.writeArLogOnVerify)) + { + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + StreamWriter sw = new StreamWriter(Path.ChangeExtension(_cuePath, ".accurip"), + false, CUESheet.Encoding); + GenerateAccurateRipLog(sw); + sw.Close(); + } + if (_config.createTOC) + { + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + WriteTOC(Path.ChangeExtension(_cuePath, ".toc")); + } + return; + } + } + else if (_accurateOffset) + { + _writeOffset = 0; + WriteAudioFilesPass(dir, style, destPaths, destLengths, htoaToFile, true); + + uint tracksMatch; + int bestOffset; + + if (_config.noUnverifiedOutput) + { + FindBestOffset(_config.encodeWhenConfidence, false, out tracksMatch, out bestOffset); + if (tracksMatch * 100 < _config.encodeWhenPercent * TrackCount || (_config.encodeWhenZeroOffset && bestOffset != 0)) + SkipOutput = true; + } + + if (!SkipOutput && _config.fixOffset) + { + FindBestOffset(_config.fixWhenConfidence, false, out tracksMatch, out bestOffset); + if (tracksMatch * 100 >= _config.fixWhenPercent * TrackCount) + _writeOffset = bestOffset; + } + } + } + + if (!SkipOutput) + { + bool verifyOnly = _accurateRip && !_accurateOffset; + if (!verifyOnly) + { + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + if (style != CUEStyle.SingleFileWithCUE) + Write(_cuePath, style); + else if (_config.createCUEFileWhenEmbedded) + Write(Path.ChangeExtension(_cuePath, ".cue"), style); + if (style != CUEStyle.SingleFileWithCUE && style != CUEStyle.SingleFile && _config.createM3U) + WriteM3U(Path.ChangeExtension(_cuePath, ".m3u"), style); + } + WriteAudioFilesPass(dir, style, destPaths, destLengths, htoaToFile, verifyOnly); + } + + if (_accurateRip) + { + ShowProgress((string)"Generating AccurateRip report...", 0, 0, null, null); + if (!_accurateOffset && _config.writeArTagsOnVerify && _writeOffset == 0 && !_isArchive) + { + uint tracksMatch; + int bestOffset; + FindBestOffset(1, true, out tracksMatch, out bestOffset); + + if (_hasEmbeddedCUESheet) + { + IAudioSource audioSource = AudioReadWrite.GetAudioSource(_sourcePaths[0], null); + NameValueCollection tags = audioSource.Tags; + CleanupTags(tags, "ACCURATERIP"); + GenerateAccurateRipTags (tags, 0, bestOffset, -1); +#if !MONO + if (audioSource is FLACReader) + ((FLACReader)audioSource).UpdateTags (true); +#endif + audioSource.Close(); + audioSource = null; + } else if (_hasTrackFilenames) + { + for (int iTrack = 0; iTrack < TrackCount; iTrack++) + { + string src = _sourcePaths[iTrack + (_hasHTOAFilename ? 1 : 0)]; + IAudioSource audioSource = AudioReadWrite.GetAudioSource(src, null); +#if !MONO + if (audioSource is FLACReader) + { + NameValueCollection tags = audioSource.Tags; + CleanupTags(tags, "ACCURATERIP"); + GenerateAccurateRipTags (tags, 0, bestOffset, iTrack); + ((FLACReader)audioSource).UpdateTags(true); + } +#endif + audioSource.Close(); + audioSource = null; + } + } + } + + if ((_accurateOffset && _config.writeArLogOnConvert) || + (!_accurateOffset && _config.writeArLogOnVerify)) + { + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + StreamWriter sw = new StreamWriter(Path.ChangeExtension(_cuePath, ".accurip"), + false, CUESheet.Encoding); + GenerateAccurateRipLog(sw); + sw.Close(); + } + if (_config.createTOC) + { + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + WriteTOC(Path.ChangeExtension(_cuePath, ".toc")); + } + } + } + + private void SetTrackTags(IAudioDest audioDest, int iTrack, int bestOffset) + { + NameValueCollection destTags = new NameValueCollection(); + + if (_hasEmbeddedCUESheet) + { + string trackPrefix = String.Format ("cue_track{0:00}_", iTrack + 1); + string[] keys = _albumTags.AllKeys; + for (int i = 0; i < keys.Length; i++) + { + if (keys[i].ToLower().StartsWith(trackPrefix) + || !keys[i].ToLower().StartsWith("cue_track")) + { + string name = keys[i].ToLower().StartsWith(trackPrefix) ? + keys[i].Substring(trackPrefix.Length) : keys[i]; + string[] values = _albumTags.GetValues(keys[i]); + for (int j = 0; j < values.Length; j++) + destTags.Add(name, values[j]); + } + } + } + else if (_hasTrackFilenames) + destTags.Add(_tracks[iTrack]._trackTags); + else if (_hasSingleFilename) + { + // TODO? + } + + destTags.Remove("CUESHEET"); + destTags.Remove("TRACKNUMBER"); + destTags.Remove("LOG"); + destTags.Remove("LOGFILE"); + destTags.Remove("EACLOG"); + CleanupTags(destTags, "ACCURATERIP"); + CleanupTags(destTags, "REPLAYGAIN"); + + if (destTags.Get("TITLE") == null && "" != _tracks[iTrack].Title) + destTags.Add("TITLE", _tracks[iTrack].Title); + if (destTags.Get("ARTIST") == null && "" != _tracks[iTrack].Artist) + destTags.Add("ARTIST", _tracks[iTrack].Artist); + destTags.Add("TRACKNUMBER", (iTrack + 1).ToString()); + if (_accurateRipId != null && _config.writeArTagsOnConvert) + { + if (_accurateOffset && _arVerify.AccResult == HttpStatusCode.OK) + GenerateAccurateRipTags(destTags, _writeOffset, bestOffset, iTrack); + else + destTags.Add("ACCURATERIPID", _accurateRipId); + } + audioDest.SetTags(destTags); + } + + private void SetAlbumTags(IAudioDest audioDest, int bestOffset, bool fWithCUE) + { + NameValueCollection destTags = new NameValueCollection(); + + if (_hasEmbeddedCUESheet || _hasSingleFilename) + { + destTags.Add(_albumTags); + if (!fWithCUE) + CleanupTags(destTags, "CUE_TRACK"); + } + else if (_hasTrackFilenames) + { + for (int iTrack = 0; iTrack < TrackCount; iTrack++) + { + string[] keys = _tracks[iTrack]._trackTags.AllKeys; + for (int i = 0; i < keys.Length; i++) + { + string singleValue = GetCommonTag (keys[i]); + if (singleValue != null) + { + if (destTags.Get(keys[i]) == null) + destTags.Add(keys[i], singleValue); + } + else if (fWithCUE && keys[i].ToUpper() != "TRACKNUMBER") + { + string[] values = _tracks[iTrack]._trackTags.GetValues(keys[i]); + for (int j = 0; j < values.Length; j++) + destTags.Add(String.Format("cue_track{0:00}_{1}", iTrack + 1, keys[i]), values[j]); + } + } + } + } + + destTags.Remove("CUESHEET"); + destTags.Remove("TITLE"); + destTags.Remove("TRACKNUMBER"); + CleanupTags(destTags, "ACCURATERIP"); + CleanupTags(destTags, "REPLAYGAIN"); + + if (fWithCUE) + { + StringWriter sw = new StringWriter(); + Write(sw, CUEStyle.SingleFileWithCUE); + destTags.Add("CUESHEET", sw.ToString()); + sw.Close(); + } + + if (_config.embedLog) + { + destTags.Remove("LOG"); + destTags.Remove("LOGFILE"); + destTags.Remove("EACLOG"); + if (_eacLog != null) + destTags.Add("LOG", _eacLog); + } + + if (_accurateRipId != null && _config.writeArTagsOnConvert) + { + if (fWithCUE && _accurateOffset && _arVerify.AccResult == HttpStatusCode.OK) + GenerateAccurateRipTags(destTags, _writeOffset, bestOffset, -1); + else + destTags.Add("ACCURATERIPID", _accurateRipId); + } + audioDest.SetTags(destTags); + } + + public void WriteAudioFilesPass(string dir, CUEStyle style, string[] destPaths, int[] destLengths, bool htoaToFile, bool noOutput) + { + const int buffLen = 16384; + int iTrack, iIndex; + int[,] sampleBuffer = new int[buffLen, 2]; + TrackInfo track; + IAudioSource audioSource = null; + IAudioDest audioDest = null; + bool discardOutput; + int iSource = -1; + int iDest = -1; + uint samplesRemSource = 0; + + if (_writeOffset != 0) + { + uint absOffset = (uint)Math.Abs(_writeOffset); + SourceInfo sourceInfo; + + sourceInfo.Path = null; + sourceInfo.Offset = 0; + sourceInfo.Length = absOffset; + + if (_writeOffset < 0) + { + _sources.Insert(0, sourceInfo); + + int last = _sources.Count - 1; + while (absOffset >= _sources[last].Length) + { + absOffset -= _sources[last].Length; + _sources.RemoveAt(last--); + } + sourceInfo = _sources[last]; + sourceInfo.Length -= absOffset; + _sources[last] = sourceInfo; + } + else + { + _sources.Add(sourceInfo); + + while (absOffset >= _sources[0].Length) + { + absOffset -= _sources[0].Length; + _sources.RemoveAt(0); + } + sourceInfo = _sources[0]; + sourceInfo.Offset += absOffset; + sourceInfo.Length -= absOffset; + _sources[0] = sourceInfo; + } + + _appliedWriteOffset = true; + } + + uint tracksMatch; + int bestOffset = _writeOffset; + if (!noOutput && _accurateRipId != null && _config.writeArTagsOnConvert && _accurateOffset && _arVerify.AccResult == HttpStatusCode.OK) + FindBestOffset(1, true, out tracksMatch, out bestOffset); + + if (hdcdDecoder != null) + hdcdDecoder.Reset(); + + if (style == CUEStyle.SingleFile || style == CUEStyle.SingleFileWithCUE) + { + iDest++; + audioDest = GetAudioDest(destPaths[iDest], destLengths[iDest], noOutput); + if (!noOutput) + SetAlbumTags(audioDest, bestOffset, style == CUEStyle.SingleFileWithCUE); + } + + uint currentOffset = 0, previousOffset = 0; + uint trackLength = _toc.Pregap * 588; + uint diskLength = _toc.Length * 588, diskOffset = 0; + + if (_accurateRip && noOutput) + _arVerify.Init(); + + ShowProgress(String.Format("{2} track {0:00} ({1:00}%)...", 0, 0, noOutput ? "Verifying" : "Writing"), 0, 0.0, null, null); + + for (iTrack = 0; iTrack < TrackCount; iTrack++) { + track = _tracks[iTrack]; + + if ((style == CUEStyle.GapsPrepended) || (style == CUEStyle.GapsLeftOut)) { + iDest++; + if (hdcdDecoder != null) + hdcdDecoder.AudioDest = null; + if (audioDest != null) + audioDest.Close(); + audioDest = GetAudioDest(destPaths[iDest], destLengths[iDest], noOutput); + if (!noOutput) + SetTrackTags(audioDest, iTrack, bestOffset); + } + + for (iIndex = 0; iIndex <= _toc[iTrack+1].LastIndex; iIndex++) { + uint trackPercent= 0, lastTrackPercent= 101; + uint samplesRemIndex = _toc[iTrack + 1][iIndex].Length * 588; + + if (iIndex == 1) + { + previousOffset = currentOffset; + currentOffset = 0; + trackLength = _toc[iTrack + 1].Length * 588; + } + + if ((style == CUEStyle.GapsAppended) && (iIndex == 1)) + { + if (hdcdDecoder != null) + hdcdDecoder.AudioDest = null; + if (audioDest != null) + audioDest.Close(); + iDest++; + audioDest = GetAudioDest(destPaths[iDest], destLengths[iDest], noOutput); + if (!noOutput) + SetTrackTags(audioDest, iTrack, bestOffset); + } + + if ((style == CUEStyle.GapsAppended) && (iIndex == 0) && (iTrack == 0)) { + discardOutput = !htoaToFile; + if (htoaToFile) { + iDest++; + audioDest = GetAudioDest(destPaths[iDest], destLengths[iDest], noOutput); + } + } + else if ((style == CUEStyle.GapsLeftOut) && (iIndex == 0)) { + discardOutput = true; + } + else { + discardOutput = false; + } + + while (samplesRemIndex != 0) { + if (samplesRemSource == 0) { + if (audioSource != null) audioSource.Close(); + audioSource = GetAudioSource(++iSource); + samplesRemSource = (uint) _sources[iSource].Length; + } + + uint copyCount = (uint) Math.Min(Math.Min(samplesRemIndex, samplesRemSource), buffLen); + + if ( trackLength > 0 ) + { + trackPercent = (uint)(currentOffset / 0.01 / trackLength); + double diskPercent = ((float)diskOffset) / diskLength; + if (trackPercent != lastTrackPercent) + ShowProgress(String.Format("{2} track {0:00} ({1:00}%)...", iIndex > 0 ? iTrack + 1 : iTrack, trackPercent, + noOutput ? "Verifying" : "Writing"), trackPercent, diskPercent, + audioSource.Path, discardOutput ? null : audioDest.Path); + lastTrackPercent = trackPercent; + } + + audioSource.Read(sampleBuffer, copyCount); + if (!discardOutput) + { + if (!_config.detectHDCD || !_config.decodeHDCD) + audioDest.Write(sampleBuffer, copyCount); + if (_config.detectHDCD && hdcdDecoder != null) + { + if (_config.wait750FramesForHDCD && diskOffset > 750 * 588 && !hdcdDecoder.Detected) + { + hdcdDecoder.AudioDest = null; + hdcdDecoder = null; + if (_config.decodeHDCD) + { + audioSource.Close(); + audioDest.Delete(); + throw new Exception("HDCD not detected."); + } + } + else + { + if (_config.decodeHDCD) + hdcdDecoder.AudioDest = (discardOutput || noOutput) ? null : audioDest; + hdcdDecoder.Process(sampleBuffer, copyCount); + } + } + } + if (_accurateRip && noOutput) + _arVerify.Write(sampleBuffer, copyCount); + + currentOffset += copyCount; + diskOffset += copyCount; + samplesRemIndex -= copyCount; + samplesRemSource -= copyCount; + + lock (this) { + if (_stop) { + if (hdcdDecoder != null) + hdcdDecoder.AudioDest = null; + audioSource.Close(); + try { + if (audioDest != null) audioDest.Close(); + } catch { } + throw new StopException(); + } + if (_pause) + { + ShowProgress("Paused...", 0, 0, null, null); + Monitor.Wait(this); + } + } + } + } + } + + if (hdcdDecoder != null) + hdcdDecoder.AudioDest = null; + if (audioSource != null) + audioSource.Close(); + if (audioDest != null) + audioDest.Close(); + } + + public static string CreateDummyCUESheet(string path, string extension) + { + string[] audioFiles = Directory.GetFiles(path, extension); + if (audioFiles.Length < 2) + return null; + Array.Sort(audioFiles); + StringWriter sw = new StringWriter(); + sw.WriteLine(String.Format("REM COMMENT \"CUETools generated dummy CUE sheet\"")); + for (int iFile = 0; iFile < audioFiles.Length; iFile++) + { + sw.WriteLine(String.Format("FILE \"{0}\" WAVE", Path.GetFileName(audioFiles[iFile]))); + sw.WriteLine(String.Format(" TRACK {0:00} AUDIO", iFile + 1)); + sw.WriteLine(String.Format(" INDEX 01 00:00:00")); + } + sw.Close(); + return sw.ToString(); + } + + public static string CorrectAudioFilenames(string path, bool always) + { + StreamReader sr = new StreamReader(path, CUESheet.Encoding); + string cue = sr.ReadToEnd(); + sr.Close(); + return CorrectAudioFilenames(Path.GetDirectoryName(path), cue, always); + } + + public static string CorrectAudioFilenames(string dir, string cue, bool always) { + string[] audioExts = new string[] { "*.wav", "*.flac", "*.wv", "*.ape", "*.m4a" }; + List lines = new List(); + List filePos = new List(); + List origFiles = new List(); + bool foundAll = true; + string[] audioFiles = null; + string lineStr; + CUELine line; + int i; + + using (StringReader sr = new StringReader(cue)) { + while ((lineStr = sr.ReadLine()) != null) { + lines.Add(lineStr); + line = new CUELine(lineStr); + if ((line.Params.Count == 3) && (line.Params[0].ToUpper() == "FILE")) { + string fileType = line.Params[2].ToUpper(); + if ((fileType != "BINARY") && (fileType != "MOTOROLA")) { + filePos.Add(lines.Count - 1); + origFiles.Add(line.Params[1]); + foundAll &= (LocateFile(dir, line.Params[1], null) != null); + } + } + } + sr.Close(); + } + + if (!foundAll || always) + { + foundAll = false; + for (i = 0; i < audioExts.Length; i++) + { + foundAll = true; + List newFiles = new List(); + for (int j = 0; j < origFiles.Count; j++) + { + string newFilename = Path.ChangeExtension(Path.GetFileName(origFiles[j]), audioExts[i].Substring(1)); + foundAll &= LocateFile(dir, newFilename, null) != null; + newFiles.Add (newFilename); + } + if (foundAll) + { + audioFiles = newFiles.ToArray(); + break; + } + } + if (!foundAll) + for (i = 0; i < audioExts.Length; i++) + { + audioFiles = Directory.GetFiles(dir == "" ? "." : dir, audioExts[i]); + if (audioFiles.Length == filePos.Count) + { + Array.Sort(audioFiles); + foundAll = true; + break; + } + } + if (!foundAll) + throw new Exception("Unable to locate the audio files."); + + for (i = 0; i < filePos.Count; i++) + lines[filePos[i]] = "FILE \"" + Path.GetFileName(audioFiles[i]) + "\" WAVE"; + } + + using (StringWriter sw = new StringWriter()) { + for (i = 0; i < lines.Count; i++) { + sw.WriteLine(lines[i]); + } + return sw.ToString (); + } + } + + private int[] CalculateAudioFileLengths(CUEStyle style) { + int iTrack, iIndex, iFile; + TrackInfo track; + int[] fileLengths; + bool htoaToFile = (style == CUEStyle.GapsAppended && _config.preserveHTOA && _toc.Pregap != 0); + bool discardOutput; + + if (style == CUEStyle.SingleFile || style == CUEStyle.SingleFileWithCUE) { + fileLengths = new int[1]; + iFile = 0; + } + else { + fileLengths = new int[TrackCount + (htoaToFile ? 1 : 0)]; + iFile = -1; + } + + for (iTrack = 0; iTrack < TrackCount; iTrack++) { + track = _tracks[iTrack]; + + if (style == CUEStyle.GapsPrepended || style == CUEStyle.GapsLeftOut) + iFile++; + + for (iIndex = 0; iIndex <= _toc[iTrack+1].LastIndex; iIndex++) { + if (style == CUEStyle.GapsAppended && (iIndex == 1 || (iIndex == 0 && iTrack == 0 && htoaToFile))) + iFile++; + + if (style == CUEStyle.GapsAppended && iIndex == 0 && iTrack == 0) + discardOutput = !htoaToFile; + else + discardOutput = (style == CUEStyle.GapsLeftOut && iIndex == 0); + + if (!discardOutput) + fileLengths[iFile] += (int) _toc[iTrack+1][iIndex].Length * 588; + } + } + + return fileLengths; + } + + public void Stop() { + lock (this) { + if (_pause) + { + _pause = false; + Monitor.Pulse(this); + } + _stop = true; + } + } + + public void Pause() + { + lock (this) + { + if (_pause) + { + _pause = false; + Monitor.Pulse(this); + } else + { + _pause = true; + } + } + } + + public int TrackCount { + get { + return _tracks.Count; + } + } + + private IAudioDest GetAudioDest(string path, int finalSampleCount, bool noOutput) + { + if (noOutput) + return new DummyWriter(path, (_config.detectHDCD && _config.decodeHDCD) ? 24 : 16, 2, 44100); + return AudioReadWrite.GetAudioDest(path, finalSampleCount, _config); + } + + private IAudioSource GetAudioSource(int sourceIndex) { + SourceInfo sourceInfo = _sources[sourceIndex]; + IAudioSource audioSource; + + if (sourceInfo.Path == null) { + audioSource = new SilenceGenerator(sourceInfo.Offset + sourceInfo.Length); + } + else { +#if !MONO + if (_isArchive) + { + RarStream IO = new RarStream(_archivePath, sourceInfo.Path); + IO.PasswordRequired += new PasswordRequiredHandler(unrar_PasswordRequired); + audioSource = AudioReadWrite.GetAudioSource(sourceInfo.Path, IO); + } + else +#endif + audioSource = AudioReadWrite.GetAudioSource(sourceInfo.Path, null); + } + + if (sourceInfo.Offset != 0) + audioSource.Position = sourceInfo.Offset; + + return audioSource; + } + + private void WriteLine(TextWriter sw, int level, CUELine line) { + WriteLine(sw, level, line.ToString()); + } + + private void WriteLine(TextWriter sw, int level, string line) { + sw.Write(new string(' ', level * 2)); + sw.WriteLine(line); + } + + public List Attributes { + get { + return _attributes; + } + } + + public List Tracks { + get { + return _tracks; + } + } + + public bool HasHTOAFilename { + get { + return _hasHTOAFilename; + } + } + + public string HTOAFilename { + get { + return _htoaFilename; + } + set { + _htoaFilename = value; + } + } + + public bool HasTrackFilenames { + get { + return _hasTrackFilenames; + } + } + + public List TrackFilenames { + get { + return _trackFilenames; + } + } + + public bool HasSingleFilename { + get { + return _hasSingleFilename; + } + } + + public string SingleFilename { + get { + return _singleFilename; + } + set { + _singleFilename = value; + } + } + + public string Artist { + get { + CUELine line = General.FindCUELine(_attributes, "PERFORMER"); + return (line == null) ? String.Empty : line.Params[1]; + } + set { + General.SetCUELine(_attributes, "PERFORMER", value, true); + } + } + + public string Title { + get { + CUELine line = General.FindCUELine(_attributes, "TITLE"); + return (line == null) ? String.Empty : line.Params[1]; + } + set { + General.SetCUELine(_attributes, "TITLE", value, true); + } + } + + public int WriteOffset { + get { + return _writeOffset; + } + set { + if (_appliedWriteOffset) { + throw new Exception("Cannot change write offset after audio files have been written."); + } + _writeOffset = value; + } + } + + public bool PaddedToFrame { + get { + return _paddedToFrame; + } + } + + public string DataTrackLength + { + get + { + return CDImageLayout.TimeToString(_dataTrackLength.HasValue ? _dataTrackLength.Value : 0); + } + set + { + uint dtl = (uint)CDImageLayout.TimeFromString(value); + if (dtl != 0) + { + _dataTrackLength = dtl; + _accurateRipId = _accurateRipIdActual = CalculateAccurateRipId(); + } + } + } + + public bool UsePregapForFirstTrackInSingleFile { + get { + return _usePregapForFirstTrackInSingleFile; + } + set{ + _usePregapForFirstTrackInSingleFile = value; + } + } + + public CUEConfig Config { + get { + return _config; + } + } + + public bool AccurateRip { + get { + return _accurateRip; + } + set { + _accurateRip = value; + } + } + + public bool AccurateOffset { + get { + return _accurateOffset; + } + set { + _accurateOffset = value; + } + } + + CDImageLayout _toc; + } + + public class CUELine { + private List _params; + private List _quoted; + + public CUELine() { + _params = new List(); + _quoted = new List(); + } + + public CUELine(string line) { + int start, end, lineLen; + bool isQuoted; + + _params = new List(); + _quoted = new List(); + + start = 0; + lineLen = line.Length; + + while (true) { + while ((start < lineLen) && (line[start] == ' ')) { + start++; + } + if (start >= lineLen) { + break; + } + + isQuoted = (line[start] == '"'); + if (isQuoted) { + start++; + } + + end = line.IndexOf(isQuoted ? '"' : ' ', start); + if (end == -1) { + end = lineLen; + } + + _params.Add(line.Substring(start, end - start)); + _quoted.Add(isQuoted); + + start = isQuoted ? end + 1 : end; + } + } + + public List Params { + get { + return _params; + } + } + + public List IsQuoted { + get { + return _quoted; + } + } + + public override string ToString() { + if (_params.Count != _quoted.Count) { + throw new Exception("Parameter and IsQuoted lists must match."); + } + + StringBuilder sb = new StringBuilder(); + int last = _params.Count - 1; + + for (int i = 0; i <= last; i++) { + if (_quoted[i]) sb.Append('"'); + sb.Append(_params[i]); + if (_quoted[i]) sb.Append('"'); + if (i < last) sb.Append(' '); + } + + return sb.ToString(); + } + } + + public class TrackInfo { + private List _attributes; + public NameValueCollection _trackTags; + + public TrackInfo() { + _attributes = new List(); + _trackTags = new NameValueCollection(); + } + + public List Attributes { + get { + return _attributes; + } + } + + public string Artist { + get { + CUELine line = General.FindCUELine(_attributes, "PERFORMER"); + return (line == null) ? String.Empty : line.Params[1]; + } + set + { + General.SetCUELine(_attributes, "PERFORMER", value, true); + } + } + + public string Title { + get { + CUELine line = General.FindCUELine(_attributes, "TITLE"); + return (line == null) ? String.Empty : line.Params[1]; + } + set + { + General.SetCUELine(_attributes, "TITLE", value, true); + } + } + } + + struct IndexInfo { + public int Track; + public int Index; + public int Time; + } + + struct SourceInfo { + public string Path; + public uint Offset; + public uint Length; + } + + public class StopException : Exception { + public StopException() : base() { + } + } + + class HttpNotFoundException : Exception { + } +} \ No newline at end of file diff --git a/CUETools.Processor/Properties/AssemblyInfo.cs b/CUETools.Processor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0bfe4fa --- /dev/null +++ b/CUETools.Processor/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CUEToolsLib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CUEToolsLib")] +[assembly: AssemblyCopyright("Copyright © 2006-2008 Moitah, Gregory S. Chudov")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d3afb938-f35e-4e5a-b650-7d7a4e885aff")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CUETools.Processor/Settings.cs b/CUETools.Processor/Settings.cs new file mode 100644 index 0000000..1959fb1 --- /dev/null +++ b/CUETools.Processor/Settings.cs @@ -0,0 +1,129 @@ +// **************************************************************************** +// +// CUE Tools +// Copyright (C) 2006-2007 Moitah (moitah@yahoo.com) +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// **************************************************************************** + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace CUETools.Processor +{ + static class SettingsShared + { + public static string GetMyAppDataDir(string appName) { + string appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string myAppDataDir = Path.Combine(appDataDir, appName); + + if (Directory.Exists(myAppDataDir) == false) { + Directory.CreateDirectory(myAppDataDir); + } + + return myAppDataDir; + } + } + + public class SettingsReader { + Dictionary _settings; + + public SettingsReader(string appName, string fileName) { + _settings = new Dictionary(); + + string path = Path.Combine(SettingsShared.GetMyAppDataDir(appName), fileName); + if (!File.Exists(path)) { + return; + } + + using (StreamReader sr = new StreamReader(path, Encoding.UTF8)) { + string line, name, val; + int pos; + + while ((line = sr.ReadLine()) != null) { + pos = line.IndexOf('='); + if (pos != -1) { + name = line.Substring(0, pos); + val = line.Substring(pos + 1); + + if (!_settings.ContainsKey(name)) { + _settings.Add(name, val); + } + } + } + } + } + + public string Load(string name) { + return _settings.ContainsKey(name) ? _settings[name] : null; + } + + public bool? LoadBoolean(string name) { + string val = Load(name); + if (val == "0") return false; + if (val == "1") return true; + return null; + } + + public int? LoadInt32(string name, int? min, int? max) { + int val; + if (!Int32.TryParse(Load(name), out val)) return null; + if (min.HasValue && (val < min.Value)) return null; + if (max.HasValue && (val > max.Value)) return null; + return val; + } + + public uint? LoadUInt32(string name, uint? min, uint? max) { + uint val; + if (!UInt32.TryParse(Load(name), out val)) return null; + if (min.HasValue && (val < min.Value)) return null; + if (max.HasValue && (val > max.Value)) return null; + return val; + } + } + + public class SettingsWriter { + StreamWriter _sw; + + public SettingsWriter(string appName, string fileName) { + string path = Path.Combine(SettingsShared.GetMyAppDataDir(appName), fileName); + + _sw = new StreamWriter(path, false, Encoding.UTF8); + } + + public void Save(string name, string value) { + _sw.WriteLine(name + "=" + value); + } + + public void Save(string name, bool value) { + Save(name, value ? "1" : "0"); + } + + public void Save(string name, int value) { + Save(name, value.ToString()); + } + + public void Save(string name, uint value) { + Save(name, value.ToString()); + } + + public void Close() { + _sw.Close(); + } + } +} \ No newline at end of file