diff --git a/CHANGELOG.MD b/CHANGELOG.MD index d34253c..d654f64 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,11 +1,15 @@ + ## CHANGELOG +### V1.4.3 +- Added support for TAK Lossless audio codec. (*https://wiki.hydrogenaud.io/index.php?title=TAK*) + ### V1.4.2 - - Program will check for `ffmpeg` in the `tools` folder. Now the full release package will run out of the box. - - Removed ffmpeg clear button from settings. It didn't provide any meaningful functionality +- Program will check for `ffmpeg` in the `tools` folder. Now the full release package will run out of the box. +- Removed ffmpeg clear button from settings. It didn't provide any meaningful functionality ### V1.4.1 - - Fixed Crash when loading a CD with any track having a pregap +- Fixed Crash when loading a CD with any track having a pregap ### V1.4 - New CD Parser class diff --git a/README.MD b/README.MD index cf9d3c5..d96a60b 100644 --- a/README.MD +++ b/README.MD @@ -1,11 +1,12 @@ + # CDCRUSH dot NET **Name**: cdcrush, *Highy compress cd-image games*\ **Author:** John Dimi, *twitter*: [@jondmt](https://twitter.com/jondmt)\ **Project Page and Sources:** [https://github.com/johndimi/cdcrush.net](https://github.com/johndimi/cdcrush.net)\ **Language:** C# .NET 4.5, **Licence:** MIT\ -**Version:** 1.3 **Platform:** Windows\ +**Version:** 1.4.3 **Platform:** Windows\ **Sister Project** : [cdcrush nodejs](https://www.npmjs.com/package/cdcrush) ## Download @@ -25,14 +26,14 @@ Available for **windows** only. - The program separates the tracks of a CD image and compress them separately. - For **data** tracks it will use **ecm tools** to remove Error Correction/Detection Codes (ECC/EDC) data from the sectors. *( redundant data )* - For **audio** tracks, it will use an encoder of your choice. You can select a lossy encoder like (**opus** or **vorbis**) to produce a decent quality audio file with a much smaller filesize compared to the uncompressed PCM audio original. -- **OR** you can choose to encode audio with **FLAC** which is lossless. +- **OR** you can choose to encode audio with a **lossless audio encoder** *( FLAC, TAK)* - Finally it compresses everything into a single `.arc` archive using the **FreeArc** archiver. **⇒ Restoring a crushed CD :** **cdcrush** can **restore** the crushed CD image back to it's original form, a **.bin/.cue** image that is ready to be used however you like. -**NOTE**: Archives with the audio tracks encoded with **FLAC**, will be restored to a 1:1 copy, byte for byte, of the original source CD +**NOTE**: Archives with the audio tracks encoded with **a lossless audio encoder**, will be restored to a 1:1 copy, byte for byte, of the original source CD ### Some examples of game sizes archived with 7zip and cdcrush: @@ -104,7 +105,8 @@ To convert a CD, go to the **Compress a CD** screen and **tick** the`convert to :warning: **WARNING** : Make sure the temp folder can hold up to 1.2GB of data ! **FFmpeg Path** : -Point to the location of `ffmpeg.exe` , it is needed for encoding/decoding the audio tracks. If you have `FFmpeg` set on the system/user PATH, it will be checked once you **clear** the custom path. +Point to the location of `ffmpeg.exe` , it is needed for encoding/decoding the audio tracks. If you have `FFmpeg` set on the system/user PATH, it will be checked once you **clear** the custom path. + - Note: If you download the **full package** of cdcrush. FFMPEG is included, so don't worry about it. **Max Concurrent Tasks**: How many tracks to process at the same time. *(For when compressing or restoring with ffmpeg and ecm tools)* @@ -124,7 +126,7 @@ See [`CHANGELOG.MD`](CHANGELOG.MD) **Q** : I am worried about the audio quality.\ **A** : The OGG vorbis (also new **OPUS**) codec is decent and it can produce very good results even at 96kbps. **However** if you don't want any compressed audio you can select the **FLAC** encoder, which is lossless. -**Q**: Is storing the entire CD with FLAC really lossless? I am worried about byte integrity.\ +**Q**: Is storing the entire CD with FLAC/TAK really lossless? I am worried about byte integrity.\ **A**: YES, to the last byte. The filesize and checksums of the restored tracks are the same as the original ones. (data&audio). You can check for yourself by calculating the checksums of restored files vs original source. **ALSO Checksum reports** ~~are coming at a later version~~ implemented since v.1.2.4 **Q** : Why there are two versions of the program?\ diff --git a/cdcrush/cdcrush.csproj b/cdcrush/cdcrush.csproj index a92329f..fd3bf7d 100644 --- a/cdcrush/cdcrush.csproj +++ b/cdcrush/cdcrush.csproj @@ -119,10 +119,12 @@ PanelRestore.cs - + + + diff --git a/cdcrush/forms/FormComponentsTest.Designer.cs b/cdcrush/forms/FormComponentsTest.Designer.cs index e1e9dc2..cc3d827 100644 --- a/cdcrush/forms/FormComponentsTest.Designer.cs +++ b/cdcrush/forms/FormComponentsTest.Designer.cs @@ -44,6 +44,12 @@ private void InitializeComponent() this.txt_files = new System.Windows.Forms.TextBox(); this.textbox_log = new System.Windows.Forms.TextBox(); this.label5 = new System.Windows.Forms.Label(); + this.btn_untak = new System.Windows.Forms.Button(); + this.btn_tak = new System.Windows.Forms.Button(); + this.txt_files_2 = new System.Windows.Forms.TextBox(); + this.label6 = new System.Windows.Forms.Label(); + this.btn_tak_pcm = new System.Windows.Forms.Button(); + this.btn_untak_pcm = new System.Windows.Forms.Button(); tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); groupBox1 = new System.Windows.Forms.GroupBox(); tableLayoutPanel1.SuspendLayout(); @@ -205,7 +211,7 @@ private void InitializeComponent() this.txt_files.Name = "txt_files"; this.txt_files.Size = new System.Drawing.Size(246, 20); this.txt_files.TabIndex = 4; - this.txt_files.Click += new System.EventHandler(this.txt_files_TextChanged); + this.txt_files.Click += new System.EventHandler(this.txt_files_Click); // // textbox_log // @@ -225,16 +231,79 @@ private void InitializeComponent() this.label5.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.label5.Location = new System.Drawing.Point(247, 28); this.label5.Name = "label5"; - this.label5.Size = new System.Drawing.Size(73, 13); + this.label5.Size = new System.Drawing.Size(70, 13); this.label5.TabIndex = 6; - this.label5.Text = "Select Files"; + this.label5.Text = "Input Files:"; + // + // btn_untak + // + this.btn_untak.Location = new System.Drawing.Point(353, 123); + this.btn_untak.Name = "btn_untak"; + this.btn_untak.Size = new System.Drawing.Size(99, 23); + this.btn_untak.TabIndex = 7; + this.btn_untak.Text = "unTAK Wav"; + this.btn_untak.UseVisualStyleBackColor = true; + this.btn_untak.Click += new System.EventHandler(this.btn_untak_Click); + // + // btn_tak + // + this.btn_tak.Location = new System.Drawing.Point(248, 123); + this.btn_tak.Name = "btn_tak"; + this.btn_tak.Size = new System.Drawing.Size(99, 23); + this.btn_tak.TabIndex = 7; + this.btn_tak.Text = "TAK Wav"; + this.btn_tak.UseVisualStyleBackColor = true; + this.btn_tak.Click += new System.EventHandler(this.btn_tak_Click); + // + // txt_files_2 + // + this.txt_files_2.Location = new System.Drawing.Point(247, 91); + this.txt_files_2.Name = "txt_files_2"; + this.txt_files_2.Size = new System.Drawing.Size(246, 20); + this.txt_files_2.TabIndex = 4; + // + // label6 + // + this.label6.AutoSize = true; + this.label6.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label6.Location = new System.Drawing.Point(247, 75); + this.label6.Name = "label6"; + this.label6.Size = new System.Drawing.Size(112, 13); + this.label6.TabIndex = 6; + this.label6.Text = "Output (Optional) :"; + // + // btn_tak_pcm + // + this.btn_tak_pcm.Location = new System.Drawing.Point(247, 152); + this.btn_tak_pcm.Name = "btn_tak_pcm"; + this.btn_tak_pcm.Size = new System.Drawing.Size(99, 23); + this.btn_tak_pcm.TabIndex = 8; + this.btn_tak_pcm.Text = "TAK PCM"; + this.btn_tak_pcm.UseVisualStyleBackColor = true; + this.btn_tak_pcm.Click += new System.EventHandler(this.btn_tak_pcm_Click); + // + // btn_untak_pcm + // + this.btn_untak_pcm.Location = new System.Drawing.Point(353, 152); + this.btn_untak_pcm.Name = "btn_untak_pcm"; + this.btn_untak_pcm.Size = new System.Drawing.Size(99, 23); + this.btn_untak_pcm.TabIndex = 8; + this.btn_untak_pcm.Text = "unTAK PCM"; + this.btn_untak_pcm.UseVisualStyleBackColor = true; + this.btn_untak_pcm.Click += new System.EventHandler(this.btn_untak_pcm_Click); // // FormComponentsTest // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(508, 446); + this.Controls.Add(this.btn_untak_pcm); + this.Controls.Add(this.btn_tak_pcm); + this.Controls.Add(this.btn_tak); + this.Controls.Add(this.btn_untak); + this.Controls.Add(this.label6); this.Controls.Add(this.label5); + this.Controls.Add(this.txt_files_2); this.Controls.Add(this.txt_files); this.Controls.Add(groupBox1); this.Controls.Add(this.textbox_log); @@ -266,5 +335,11 @@ private void InitializeComponent() private System.Windows.Forms.Button btn_join; private System.Windows.Forms.Label label4; private System.Windows.Forms.Label label5; + private System.Windows.Forms.Button btn_untak; + private System.Windows.Forms.Button btn_tak; + private System.Windows.Forms.TextBox txt_files_2; + private System.Windows.Forms.Label label6; + private System.Windows.Forms.Button btn_tak_pcm; + private System.Windows.Forms.Button btn_untak_pcm; } } \ No newline at end of file diff --git a/cdcrush/forms/FormComponentsTest.cs b/cdcrush/forms/FormComponentsTest.cs index 4808fe3..8634ced 100644 --- a/cdcrush/forms/FormComponentsTest.cs +++ b/cdcrush/forms/FormComponentsTest.cs @@ -43,7 +43,7 @@ private void FormComponentsTest_Load(object sender, EventArgs e) // Select files to apply operations // - string[] SELECTED_FILES; - private void txt_files_TextChanged(object sender, EventArgs e) + private void txt_files_Click(object sender, EventArgs e) { SELECTED_FILES = FormTools.fileLoadDialog("all","",true); if(SELECTED_FILES != null) @@ -56,7 +56,6 @@ private void txt_files_TextChanged(object sender, EventArgs e) } }// ----------------------------------------- - // -- // Create ARC private void btn_arc_Click(object sender, EventArgs e) @@ -124,5 +123,84 @@ private void btn_unecm_Click(object sender, EventArgs e) }; app.unecm(SELECTED_FILES[0]); }// ----------------------------------------- + + private void btn_tak_Click(object sender, EventArgs e) + { + if(SELECTED_FILES==null) return; + var app = new Tak(prog.CDCRUSH.TOOLS_PATH); + app.onComplete = (s) => { + LOG.log("-- WAV to TAK Complete :: {0}", s); + }; + app.encode(SELECTED_FILES[0],txt_files_2.Text); + } + + private void btn_untak_Click(object sender, EventArgs e) + { + if(SELECTED_FILES==null) return; + var app = new Tak(prog.CDCRUSH.TOOLS_PATH); + app.onComplete = (s) => { + LOG.log("--TAK to WAV Complete :: {0}", s); + }; + app.decode(SELECTED_FILES[0],txt_files_2.Text); + } + + private void btn_tak_pcm_Click(object sender, EventArgs e) + { + if(SELECTED_FILES==null) return; + + var ffmp = new FFmpeg(prog.CDCRUSH.FFMPEG_PATH); + var tak = new Tak(prog.CDCRUSH.TOOLS_PATH); + + tak.onComplete = (s) => { + LOG.log($"TAK Operation Complete - {s}"); + }; + + ffmp.onComplete = (s) => { + LOG.log($"FFMPEG Operation Complete - {s}"); + }; + + string INPUT = SELECTED_FILES[0]; + string OUTPUT = Path.ChangeExtension(INPUT,".tak"); + + // This will make FFMPEG read the PCM file, convert it to WAV on the fly + // and feed it to TAK, which will convert and save it. + + ffmp.convertPCMStreamToWavStream( (ffmpegIn,ffmpegOut) => { + var sourceFile = File.OpenRead(INPUT); + tak.encodeFromStream(OUTPUT, (takIn) => { + ffmpegOut.CopyTo(takIn); + takIn.Close(); + }); + sourceFile.CopyTo(ffmpegIn); // Feed PCM to FFMPEG + ffmpegIn.Close(); + }); + }// -- + + private void btn_untak_pcm_Click(object sender, EventArgs e) + { + if(SELECTED_FILES==null) return; + + string INPUT = SELECTED_FILES[0]; + string OUTPUT = Path.ChangeExtension(INPUT,".pcm"); + var ffmp = new FFmpeg(prog.CDCRUSH.FFMPEG_PATH); + var tak = new Tak(prog.CDCRUSH.TOOLS_PATH); + + tak.onComplete = (s) => { + LOG.log($"TAK Operation Complete - {s}"); + }; + + ffmp.onComplete = (s) => { + LOG.log($"FFMPEG Operation Complete - {s}"); + }; + + tak.decodeToStream(INPUT,(_out) => { + ffmp.convertWavStreamToPCM(OUTPUT,(_in)=>{ + _out.CopyTo(_in); + _in.Close(); + }); + }); + + }// -- + }// -- }// -- diff --git a/cdcrush/forms/FormMain.cs b/cdcrush/forms/FormMain.cs index 952399e..ae6199b 100644 --- a/cdcrush/forms/FormMain.cs +++ b/cdcrush/forms/FormMain.cs @@ -242,8 +242,9 @@ public void form_setText(string msg="", int type=0) /// - /// Updates progress bar - /// Can be called from threads + /// Updates progress bar (Can be called from threads safely) + /// -1 = Use a Marquee + /// 0 - 100 = Set a progress percent /// /// Progress Percent public void form_setProgress(int per) diff --git a/cdcrush/forms/PanelCompress.cs b/cdcrush/forms/PanelCompress.cs index 2432073..fa7fab1 100644 --- a/cdcrush/forms/PanelCompress.cs +++ b/cdcrush/forms/PanelCompress.cs @@ -31,35 +31,43 @@ private void PanelCompress_Load(object sender, EventArgs e) tt.SetToolTip(chk_encodedCue, "Encodes audio tracks and creates a .CUE file that handles data and encoded audio tracks. Doesn't create a final archive. This format can be used in some emulators."); tt.SetToolTip(pictureBox1,"You can optionally set an image cover for this CD and it will be stored in the archive."); - // -- Initialize Audio Settings: - foreach(string scodec in CDCRUSH.AUDIO_CODECS) + // -- Get audio codecs into Combo Box + foreach(var codecID in AudioMaster.codecs) { - combo_audio_c.Items.Add(scodec); + combo_audio_c.Items.Add(AudioMaster.getCodecIDName(codecID)); } combo_audio_c.SelectedIndex = 0; combo_audio_c_SelectedIndexChanged(null,null); // force first call? why + combo_data_c.SelectedIndex = CDCRUSH.FREEARC_DEF_COMPRESSION_INDEX; FormTools.fileLoadDialogPrepare("cue", "CUE files (*.cue)|*.cue"); FormTools.fileLoadDialogPrepare("cover", "Image files (*.jpg)|*.jpg"); // -- - form_lockSection("action", true); - form_set_cover(null); // will also set preparedCover - form_set_info_pre(null); - form_set_crushed_size(); - loadedCuePath = null; - postCdInfo = null; - - // -- + form_zeroOutInfos(); form_set_proper_action_name(); - combo_data_c.SelectedIndex = 3; }// ----------------------------------------- // ========================================== // FORM ACTIONS // ========================================== + + /// + /// Clear CD Infos and Locks Buttons + /// + private void form_zeroOutInfos() + { + form_lockSection("action", true); + form_set_cover(null); // Empty it out + form_set_cd_info(null); + form_set_crushed_size(); // Empty it out + loadedCuePath = null; + postCdInfo = null; + numberOfTracks = 0; + }// ----------------------------------------- + /// /// LOCK parts of the form /// @@ -95,14 +103,10 @@ public void form_lockSection(string _section,bool _lock) /// void form_set_cover(string file) { - if(FormTools.imageSetFile(pictureBox1, file)) - { - // OK + if(FormTools.imageSetFile(pictureBox1, file)) { preparedCover = file; } - else - { - // FAIL + else { pictureBox1.Image = Properties.Resources.dropimage; preparedCover = null; } @@ -122,8 +126,8 @@ void form_set_proper_action_name() /// /// Set some CD infos from a CUE file /// - /// - void form_set_info_pre(dynamic cdInfo = null) + /// {title,size1,tracks} + void form_set_cd_info(dynamic cdInfo = null) { btn_chksm.Enabled = false; @@ -139,6 +143,8 @@ void form_set_info_pre(dynamic cdInfo = null) info_cdtitle.Text = cdInfo.title; info_size1.Text = String.Format("{0}MB", FormTools.bytesToMB(cdInfo.size1)); info_tracks.Text = String.Format("{0}", cdInfo.tracks); + + numberOfTracks = cdInfo.tracks; }// ----------------------------------------- /// @@ -160,31 +166,24 @@ void form_set_crushed_size(int size = 0) /// void form_quickLoadFile(string file) { - dynamic o = CDCRUSH.loadQuickCUE(file); - - // Reset Infos and Action, unlock on valid file - form_lockSection("action", true); - form_set_cover(null); // Empty it out - form_set_info_pre(null); - form_set_crushed_size(); // Empty it out - loadedCuePath = null; - postCdInfo = null; - numberOfTracks = 0; + form_zeroOutInfos(); // Clear the status infos at next tab change FormMain.FLAG_REQUEST_STATUS_CLEAR = true; + dynamic o = CDCRUSH.loadQuickCUE(file); + if (o == null) // Error { FormMain.sendMessage(CDCRUSH.ERROR, 3); } else { + // CUE Loaded OK, fill in form infos and unlock buttons loadedCuePath = file; input_in.Text = file; form_lockSection("action", false); - form_set_info_pre(o); - numberOfTracks = o.tracks; + form_set_cd_info(o); FormMain.sendMessage("Loaded CUE OK.", 2); } @@ -193,7 +192,6 @@ void form_quickLoadFile(string file) // ========================================== // EVENTS // ========================================== - /// /// A file has been dropped and the ENGINE is NOT LOCKED @@ -216,13 +214,15 @@ public void handle_dropped_file(string file) // Event Clicked Compressed Button private void btn_CRUSH_Click(object sender, EventArgs e) { - // Get a valid audio parameters tuple - Tuple audioQ = Tuple.Create(combo_audio_c.SelectedIndex,combo_audio_q.SelectedIndex); - - // Since I can fire 2 jobs from here, have a common callback - Action jobCallback = (complete, newSize, cd) => { - FormTools.invoke(this, () =>{ - + // This TUPLE will hold (CODECID,QUALITY INDEX) + Tuple audioQ = Tuple.Create( AudioMaster.codecs[combo_audio_c.SelectedIndex], + combo_audio_q.SelectedIndex ); + + // Since I can fire 2 jobs from here, have a common callback for both jobs + Action jobCallback = (complete, newSize, cd) => + { + FormTools.invoke(this, () => + { form_lockSection("all", false); form_set_crushed_size(newSize); postCdInfo = cd; // Either null or full info @@ -230,12 +230,14 @@ private void btn_CRUSH_Click(object sender, EventArgs e) if(complete) { FormMain.sendMessage("Complete", 2); btn_chksm.Enabled = true; - - }else - { + }else { FormMain.sendProgress(0); FormMain.sendMessage(CDCRUSH.ERROR,3); } + + // Make progress bar and status message clear after + FormMain.FLAG_REQUEST_STATUS_CLEAR = true; + }); }; @@ -286,11 +288,12 @@ private void btn_input_out_Click(object sender, EventArgs e) { string lastDir = null; if (loadedCuePath != null) lastDir = System.IO.Path.GetDirectoryName(loadedCuePath); - SaveFileDialog d = new SaveFileDialog(); - d.FileName = "HERE.arc"; - d.CheckPathExists = true; - d.InitialDirectory = lastDir; - d.Title = "Select output folder"; + SaveFileDialog d = new SaveFileDialog { + FileName = "HERE.arc", + CheckPathExists = true, + InitialDirectory = lastDir, + Title = "Select output folder" + }; if (d.ShowDialog() == DialogResult.OK) { @@ -325,35 +328,22 @@ private void combo_audio_c_SelectedIndexChanged(object sender, EventArgs e) { combo_audio_q.Items.Clear(); - // Ordering is defined in CDCRUSH.AUDIO_CODECS - switch(combo_audio_c.SelectedIndex) + // Will get available quality options for a CODEC ID + // Will make checkbox disabled and point it to the default quality based on Codec + + var codecID = AudioMaster.codecs[combo_audio_c.SelectedIndex]; + var qInfo = AudioMaster.getQualityInfos(codecID); + + foreach(string info in qInfo.Item1) { - case 0: // FLAC - // Keep empty - combo_audio_q.Items.Add(""); - combo_audio_q.Enabled = false; - break; - case 1: // VORBIS - for(int i=0;i0; + if(combo_audio_q.Enabled) { + combo_audio_q.SelectedIndex = qInfo.Item2; + } + }// ----------------------------------------- // -- diff --git a/cdcrush/forms/PanelCompress.resx b/cdcrush/forms/PanelCompress.resx index 4dca708..1cb561a 100644 --- a/cdcrush/forms/PanelCompress.resx +++ b/cdcrush/forms/PanelCompress.resx @@ -126,12 +126,6 @@ False - - False - - - False - False @@ -162,7 +156,4 @@ False - - False - \ No newline at end of file diff --git a/cdcrush/forms/PanelRestore.cs b/cdcrush/forms/PanelRestore.cs index f86abaf..8aa0b74 100644 --- a/cdcrush/forms/PanelRestore.cs +++ b/cdcrush/forms/PanelRestore.cs @@ -228,6 +228,9 @@ private void btn_RESTORE_Click(object sender, EventArgs e) FormMain.sendProgress(0); FormMain.sendMessage(CDCRUSH.ERROR,3); } + + // Make progress bar and status message clear after + FormMain.FLAG_REQUEST_STATUS_CLEAR = true; }); }); diff --git a/cdcrush/lib/app/AbArchiver.cs b/cdcrush/lib/app/AbArchiver.cs index 38057f4..2ec4bb5 100644 --- a/cdcrush/lib/app/AbArchiver.cs +++ b/cdcrush/lib/app/AbArchiver.cs @@ -6,7 +6,7 @@ namespace cdcrush.lib.app /// /// Generic Archiver class /// - abstract class AbArchiver:ICliReport + abstract class AbArchiver:IProcessStatus { protected CliApp proc; protected int _progress; // 0 - 100 diff --git a/cdcrush/lib/app/CliApp.cs b/cdcrush/lib/app/CliApp.cs index e31ef83..aa1e524 100644 --- a/cdcrush/lib/app/CliApp.cs +++ b/cdcrush/lib/app/CliApp.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Threading; @@ -8,9 +9,30 @@ namespace cdcrush.lib.app /** * Generic CLI application spawner + * Starts an app in a new thread asynchronously + * -- + * Example: + * + * proc = new CliApp('application.exe'); + * proc.onComplete = (code) => { + * // code 0 = OK, 1 = ERROR, 2 = CAN'T RUN EXE + * }; + * proc.onStdOut = (s) => { LOG.log(s); } + * proc.start("-i -a","c:\\temp"); // start(arguments, workingDir); + * + * //-- + * // If you want to use piping to/from the app + * // before .start() set: + * proc.flag_disable_user_stdout = true; // (this will disable user callbacks for stdout) + * proc.start(); + * proc. + * + * */ class CliApp { + public static List threads = null; + // The main process object public Process proc {get; private set;} @@ -20,95 +42,133 @@ public string executable { set { proc.StartInfo.FileName = value; } } - // #USERSET:: - public Action onComplete; // 0 = OK, 1 = Error, 2 = Could not run EXE + /// + /// Called when the application exits. + /// 0 = OK, 1 = Error, 2 = Could not run EXE + /// + public Action onComplete; + /// + /// Pushes the STDOUT as is happens from the app + /// public Action onStdOut; + /// + /// Pushes the STDERR as it happens from the app + /// public Action onStdErr; - public Action onStdOutWord; // Used if you enable HACK_STDOUT_RAW on the constructor - // Will push out words read from StdOut. + /// + /// Valid only if you set `flag_stdout_word_mode` to true + /// Pushes words read from the STDOUT + /// + public Action onStdOutWord; - // -- + // Internal stirng holders for stderr and stdout StringBuilder builderStdOut; StringBuilder builderStdErr; + /// + /// Logs the entire App STDOUT here + /// public string stdOutLog { get{ return builderStdOut.ToString(); } } + + /// + /// Logs the entire App STDERR here + /// public string stdErrLog { get{ return builderStdErr.ToString(); } } - // Helpers - private bool _hasStarted = false; - // When some CLI apps don't flush their stdOut, capture it raw word by word. - private bool HACK_STDOUT_RAW = false; + /// + /// Access the main stdIn Stream. + /// + public System.IO.Stream stdIn { get{return proc.StandardInput.BaseStream;}} + /// + /// Access the main stdOut Stream + /// + public System.IO.Stream stdOut { get{return proc.StandardOutput.BaseStream;}} + + + /// + /// When some CLI apps don't flush their stdOut, capture it raw word by word. + /// Access `onStdOutWord` + /// + public bool flag_stdout_word_mode = false; + + /// + /// Set to true to disable ALL user stdout calls. Enable this if you want to pipe the stdout + /// + public bool flag_disable_user_stdout = false; + + // Helper + bool _hasStarted = false; + + + public Action onStdIOReady; // ----------------------------------------- /// /// Starts a CLI app in a new thread /// /// Path to the executable - /// Enable Raw word capture from stdOut. Use `onStdOutWord` to capture - public CliApp(string exec,bool enableStdOutHack = false) + public CliApp(string exec) { proc = new Process(); executable = exec; // uses setter + proc.StartInfo.UseShellExecute = false; // needed for executables! - proc.StartInfo.CreateNoWindow = true; + proc.StartInfo.CreateNoWindow = true; + + proc.StartInfo.RedirectStandardInput = true; proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.RedirectStandardError = true; builderStdOut = new StringBuilder(); builderStdErr = new StringBuilder(); - HACK_STDOUT_RAW = enableStdOutHack; - - if(!HACK_STDOUT_RAW) - { - proc.OutputDataReceived += (sender, e) => - { - builderStdOut.Append(e.Data); - if(!String.IsNullOrEmpty(e.Data) && onStdOut!=null) - onStdOut(e.Data); - }; - - proc.ErrorDataReceived += (sender, e) => - { - builderStdErr.Append(e.Data); - if(!String.IsNullOrEmpty(e.Data) && onStdErr!=null) - onStdErr(e.Data); - }; - } }// ----------------------------------------- - // -- ~CliApp() { kill(); }// ----------------------------------------- - // -- public void kill() { if(_hasStarted && !proc.HasExited) proc.Kill(); - //proc.Dispose(); }// ----------------------------------------- + /// /// Start a thread in a new thread, it will keep going /// /// Arguments public void start(string args = null,string workingDir = null) { - proc.StartInfo.WorkingDirectory = workingDir; // Shoudl I make it System.Environment.CurrentDirectory ?? + proc.StartInfo.WorkingDirectory = workingDir; proc.StartInfo.Arguments = args; - LOG.log("[CLI.APP] : start() : {0} {1}", executable, proc.StartInfo.Arguments); + proc.ErrorDataReceived += (sender, e) => + { + builderStdErr.Append(e.Data); + if(!String.IsNullOrEmpty(e.Data) && onStdErr!=null) + onStdErr(e.Data); + }; - // Note: I really need a Thread here, If I don't, it will lock the main thread and forms - var th = new Thread(new ThreadStart( + if(!flag_disable_user_stdout) + { + proc.OutputDataReceived += (sender, e) => + { + builderStdOut.Append(e.Data); + if(!String.IsNullOrEmpty(e.Data) && onStdOut!=null) + onStdOut(e.Data); + }; + } - () => { - try{ + // Note: I really need a Thread here, If I don't, it will lock the main thread and forms + Thread th = new Thread(new ThreadStart( + () => { + + try{ proc.Start(); }catch(System.ComponentModel.Win32Exception){ // Could not find the executable @@ -116,56 +176,65 @@ public void start(string args = null,string workingDir = null) onComplete?.Invoke(2); return; } - + _hasStarted = true; - - if(HACK_STDOUT_RAW && onStdOutWord!=null) + + proc.BeginErrorReadLine(); + + if(flag_stdout_word_mode && onStdOutWord!=null) { - int byte_r = 0; - StringBuilder word = new StringBuilder(); - - while( (byte_r = proc.StandardOutput.BaseStream.ReadByte()) > -1 ) - { - if(byte_r==32 || byte_r==13) // SPACE or ENTER - { - if(word.Length>0) - { - onStdOutWord(word.ToString()); - word.Clear(); - } - - }else - { - if(byte_r>32) // No special characters in the stringbuilder - { - word.Append(Char.ConvertFromUtf32(byte_r)); - } - } - } // end while - }//-- - - if(!HACK_STDOUT_RAW) // Didn't use else because could be (hack=true, onStdOutWord=false) + captureWords(); + }else { - proc.BeginErrorReadLine(); - proc.BeginOutputReadLine(); - }// - + if(!flag_disable_user_stdout) + { + proc.BeginOutputReadLine(); + } + } + + onStdIOReady?.Invoke(); proc.WaitForExit(); - LOG.log("[CLI.APP] : Process \'{0}\' Exited with code {1}", executable, proc.ExitCode); onComplete?.Invoke(proc.ExitCode); - })); + })); + // - Start the thread th.IsBackground = true; th.Name = $"cliApp({executable})"; th.Start(); }// ----------------------------------------- + /// + /// Start capturing words from the stdout stream and push them on the callback + /// + private void captureWords() + { + int byte_r = 0; + StringBuilder word = new StringBuilder(); - /** - * Quickly create a CLI APP and return it - */ + while((byte_r = proc.StandardOutput.BaseStream.ReadByte()) > -1) + { + if(byte_r == 32 || byte_r == 13) // SPACE or ENTER + { + if(word.Length > 0) { + onStdOutWord(word.ToString()); + word.Clear(); + } + + } else { + if(byte_r > 32) // No special characters in the stringbuilder + { + word.Append(Char.ConvertFromUtf32(byte_r)); + } + } + } + }// ----------------------------------------- + + /// + /// Quickly create a CLI APP and return it + /// static public CliApp quickStart(string filename, string args = null,Action OnComplete = null) { var c = new CliApp(filename); @@ -183,7 +252,6 @@ static public string[] quickStartSync(string exePath,string args = null) { var app = new CliApp(exePath); app.proc.StartInfo.Arguments = args; - LOG.log("[CLI.APP] : quickrun() : {0} {1}", exePath, args); app.proc.Start(); app.proc.WaitForExit(); var s = new string[] { @@ -217,4 +285,4 @@ static public bool exists(string exePath) }// -- end class -}// -- end namespace +}// -- end namespace \ No newline at end of file diff --git a/cdcrush/lib/app/EcmTools.cs b/cdcrush/lib/app/EcmTools.cs index 7a45227..cf0de69 100644 --- a/cdcrush/lib/app/EcmTools.cs +++ b/cdcrush/lib/app/EcmTools.cs @@ -12,7 +12,7 @@ namespace cdcrush.lib.app /// /// /// - class EcmTools:ICliReport + class EcmTools:IProcessStatus { const string EXECUTABLE_ECM = "ecm.exe"; const string EXECUTABLE_UNECM = "unecm.exe"; @@ -55,7 +55,7 @@ public EcmTools(string exePath = "") else { ERROR = "EcmTools error."; - onComplete(false); + onComplete?.Invoke(false); } }; diff --git a/cdcrush/lib/app/FFmpeg.cs b/cdcrush/lib/app/FFmpeg.cs index 02f44b8..d0304a3 100644 --- a/cdcrush/lib/app/FFmpeg.cs +++ b/cdcrush/lib/app/FFmpeg.cs @@ -1,10 +1,74 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; + namespace cdcrush.lib.app { +public class FFmpegCodec +{ + public string name; // Name of codec + public string ID; // Identifier + public string ext; // Codec Extension + public string codecString; // Codec String to put in ffmpeg argument + public bool ignoreQuality = false; + public string[] qualityArg; // Quality argument to pass onto ffmpeg + // If NULL, will take 0... as parameters ( up to qualityinfo0.length ) + public int[] qualityInfo; // 1:1 map to qualityArg[]. Information on Quality Index + // If qualityInfo0 NOT SET, then info = qualityARG + qualityInfoPost + public string qualityInfoPost; // If set, quality Info is qualityInfo0 + qualityInfoPost + public int qualityDefault; // default quality INDEX ( 0...N ) + + // -- + private int sanitizeQuality(int q) + { + if(q<0) return 0; + if(qualityArg!=null) { + if(q>=qualityArg.Length) return qualityArg.Length-1; + }else { + if(qualityInfo!=null && q>=qualityInfo.Length) return qualityInfo.Length-1; + } + return q; + }// --------------- + + /// + /// Return the ffmpeg encode string with quality baked + /// + /// + /// + public string getCodecString(int q) + { + if(ignoreQuality) return codecString; + q = sanitizeQuality(q); + if(qualityArg==null) return $"{codecString}{q}"; // Quality + Q Index + return $"{codecString}{qualityArg[q]}"; + }// --------------- + + // -- + public string[] getQualityInfos() + { + List l = new List(); + if(!ignoreQuality) + { + if(qualityInfo!=null) + { + foreach(var i in qualityInfo) l.Add(i + qualityInfoPost); + }else + { + foreach(var i in qualityArg) l.Add(i + qualityInfoPost); + } + } + + return l.ToArray(); + }// --------------- + +} + + + + /// /// Simple wrapper for FFmpeg /// Currently just supports Audio Compression for use with the CDCRUSH project @@ -13,30 +77,73 @@ namespace cdcrush.lib.app /// "onComplete" => Exit code 0 for OK, other for ERROR /// /// -class FFmpeg:ICliReport +class FFmpeg:IProcessStatus { const string EXECUTABLE_NAME = "ffmpeg.exe"; private CliApp proc; // # USER SET :: - public Action onProgress { get; set; } - public Action onComplete { get; set; } - public string ERROR {get; private set;} + public Action onProgress { get; set; } // IProcessReport + public Action onComplete { get; set; } // IProcessReport + public string ERROR {get; private set;} // IProcessReport // Percentage Helpers int secondsConverted, targetSeconds; public int progress {get; private set;} // Current progress % of the current conversion - // Ogg vorbis Quality (index), VBR kbps - public static readonly int[] VORBIS_QUALITY = { 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500 }; + // Supported codecs to use + public static FFmpegCodec[] codecs; - // MP3 quality (index), VBR kbps - public static readonly int[] MP3_QUALITY = { 245, 225, 190, 175, 165, 130, 115, 100, 85, 65}; + public static FFmpegCodec getCodecByID(string id) { + return Array.Find(codecs,(s)=>s.ID==id); + } - // OPUS quality (index), VBR kbps - public static readonly int[] OPUS_QUALITY = { 32, 48, 64, 80, 96, 112, 128, 160, 320}; // ----------------------------------------- + /// + /// Constructor for the static things, this will only get called once + /// + static FFmpeg() + { + codecs = new[] + { + new FFmpegCodec() { + name = "Vorbis", ID = "VORBIS", ext = ".ogg", + qualityDefault = 3, + // qualityArg not set, meaning it accepts 0->10 (info.length) + qualityInfo = new [] { 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500 }, + qualityInfoPost = "k Vbr", + codecString = "-c:a libvorbis -q " // string ends where it expects quality string + }, + + new FFmpegCodec() { + name = "Opus", ID = "OPUS", ext = ".ogg", + qualityDefault = 4, + // qualityInfo not set, meaning final info = qualityArg + qualitInfoPost // e.g. 32k Vbr" + qualityArg = new [] { "32k", "48k", "64k", "80k", "96k", "112k", "128k", "160k", "320k" }, + qualityInfoPost = " Vbr", + codecString = "-c:a libopus -vbr on -compression_level 10 -b:a " + }, + + new FFmpegCodec() { + name = "Mp3", ID = "MP3", ext = ".mp3", + qualityDefault = 3, + qualityArg = new [] { "9","8","7","6","5","4","3","2","1","0" }, + qualityInfo = new [] { 65, 85, 100, 115, 130, 165, 175, 190, 225, 245 }, + qualityInfoPost = " Vbr", + codecString = "-c:a libmp3lame -q:a " + }, + + new FFmpegCodec() { + name = "Flac lossless", ID = "FLAC", ext = ".flac", + qualityDefault = 0, + // No quality settings, means that quality is ignored + codecString = "-c:a flac ", + ignoreQuality = true + } + }; + }// ----------- + /// /// FFMPEG wrapper /// @@ -47,24 +154,23 @@ public FFmpeg(string executablePath = "") proc.onComplete = (code) => { - if (code == 0) - { + if (code == 0) { onComplete?.Invoke(true); } - else - { + else { ERROR = "Something went wrong with FFMPEG"; - onComplete(false); + onComplete?.Invoke(false); } }; - // Get and calculate progress, "targetSeconds" needs to be set for this to work + // -- FFMPEG writes Status to StdErr + // Gets current operation progress ( proc.onStdErr = (s) => { if (targetSeconds == 0) return; secondsConverted = readSecondsFromOutput(s, @"time=(\d{2}):(\d{2}):(\d{2})"); - if (secondsConverted == -1) return; + if (secondsConverted == -1) return; progress = (int)Math.Ceiling(((double)secondsConverted / (double)targetSeconds) * 100f); // LOG.log("[FFMPEG] : {0} / {1} = {2}", secondsConverted, targetSeconds, progress); @@ -85,11 +191,10 @@ public FFmpeg(string executablePath = "") private int getSecondsFromFile(string input) { int i = 0; - var s = CliApp.quickStartSync(proc.executable, string.Format("-i \"{0}\" -f null -", input)); + var s = CliApp.quickStartSync(proc.executable,$"-i \"{input}\" -f null -"); if(s[2]=="0") // ffmpeg success { i = readSecondsFromOutput(s[1], @"\s*Duration:\s*(\d{2}):(\d{2}):(\d{2})"); - LOG.log("[FFMPEG] : {0} duration in seconds = {1}", input, i); } return i; }// ----------------------------------------- @@ -114,175 +219,141 @@ private int readSecondsFromOutput(string input,string expression) return seconds; }// ----------------------------------------- + + /// - /// Convert an audio file to a PCM file for use in a CD audio - /// ! Does not check INPUT file ! - /// ! Overwrites all generated files ! + /// Encode a PCM file with an ENCODER / QUALITY combo to another File /// - /// - /// If ommited, will be automatically set + /// OPUS, VORBIS, FLAC, MP3 + /// Use -1 for default, Starts from 0 ... MAX = dependent on codec + /// Input filepath to encode + /// Output path, if not set will create a file at same folder as input file /// - public bool audioToPCM(string input,string output = null) + public bool encodePCM(string encoderID, int quality, string input, string output = null) { - LOG.log("[FFMPEG] : Converting \"{0}\" to PCM",input); - - if(string.IsNullOrEmpty(output)) { - output = Path.ChangeExtension(input,"pcm"); + var cod = getCodecByID(encoderID); + + if(cod == null) { + ERROR = $"CodecID {encoderID} does not exist"; + return false; } + if(quality==-1) quality = cod.qualityDefault; - // Prepare progress variables + if(string.IsNullOrEmpty(output)) { + output = Path.ChangeExtension(input,cod.ext); + }else{ + //[safeguard] Make sure it is valid extension + if(!output.ToLower().EndsWith(cod.ext)) { + output += cod.ext; // try to fix it + } + } + + // Init progress variables: + var fsize = (int)new FileInfo(input).Length; secondsConverted = progress = 0; - targetSeconds = getSecondsFromFile(input); - - proc.start(string.Format("-i \"{0}\" -y -f s16le -acodec pcm_s16le \"{1}\"", input, output)); + targetSeconds = (int)Math.Floor((double)fsize / 176400); // PCM is 176400 bytes per second + // - + LOG.log("[FFMPEG] : Encoding [{0}] with {1} , {2}",input, cod.name, cod.getCodecString(quality)); + proc.start($"-y -f s16le -ar 44.1k -ac 2 -i \"{input}\" {cod.getCodecString(quality)} \"{output}\""); + return true; - }// ----------------------------------------- + }// --------------- + /// - /// Convert a PCM audio file to OGG OPUS - /// ! Overwrites all generated files ! - /// ! Does not check INPUT file ! + /// Takes a STREAM and encodes to a file + /// - callback onPipeReady() will fire with the Stream + /// - write to that stream with PCM Data + /// - Don't forget to close the STREAM once you've done writing to it /// - /// - /// In KBPS from 32 to 500 - /// If ommited, will be automatically set /// - public bool audioPCMToOggOpus(string input, int quality, string output = null) + public bool encodePCMStream(string encoderID, int quality, string output, Action onIOReady) { - // [safequard] - if (quality < 32) quality = 32; - else if (quality > 500) quality = 500; - - if(string.IsNullOrEmpty(output)) { - output = Path.ChangeExtension(input,"ogg"); - }else{ - //[safeguard] Make sure it's an OGG - if(!output.ToLower().EndsWith(".ogg")) { - output += ".ogg"; // try to fix it - } + var cod = getCodecByID(encoderID); + if(cod == null) return false; + if(quality==-1) quality = cod.qualityDefault; + if(!output.ToLower().EndsWith(cod.ext)) + { + output += cod.ext; // try to fix extension if not already set } - LOG.log("[FFMPEG] : Converting \"{0}\" to OPUS OGG {1}kbps",input,quality); - - _initProgressVars(input); + proc.onStdIOReady = () => + { + onIOReady(proc.stdIn); + }; - proc.start(string.Format( - // VBR is ON and Compression is 10 by FFmpeg defaults, but just to be sure. - "-y -f s16le -ar 44.1k -ac 2 -i \"{0}\" -c:a libopus -b:a {1}k -vbr on -compression_level 10 \"{2}\"", - input ,quality, output - )); + LOG.log("[FFMPEG] : Encoding PCM STREAM [{0}] with {1} , {2}",output, cod.name, cod.getCodecString(quality)); + proc.start($"-y -f s16le -ar 44.1k -ac 2 -i pipe:0 {cod.getCodecString(quality)} \"{output}\""); return true; }// ----------------------------------------- /// - /// Convert a PCM audio file to OGG VORBIS - /// ! Overwrites all generated files ! - /// ! Does not check INPUT file ! + /// Take a PCM Stream, and push it to another STREAM as WAV + /// - callback onPipeReady() will fire with the Streams + /// - Don't forget to close the inStream once you've done writing to it /// /// - /// In KBPS from 0 to 10 - /// If ommited, will be automatically set - /// - public bool audioPCMToOggVorbis(string input, int quality, string output = null) + /// (thisGetsData, thisPushesData) + public void convertPCMStreamToWavStream(Action onIOReady) { - // [safequard] - if (quality < 0) quality = 0; - else if (quality > 10) quality = 10; - - if(string.IsNullOrEmpty(output)) { - output = Path.ChangeExtension(input,"ogg"); - }else{ - //[safeguard] Make sure it's an OGG - if(!output.ToLower().EndsWith(".ogg")) { - output += ".ogg"; // try to fix it - } - } - - LOG.log("[FFMPEG] : Converting \"{0}\" to VORBIS OGG {1}kbps", input, VORBIS_QUALITY[quality]); - - _initProgressVars(input); - - proc.start(string.Format( - // VBR is ON and Compression is 10 by FFmpeg defaults, but just to be sure. - "-y -f s16le -ar 44.1k -ac 2 -i \"{0}\" -c:a libvorbis -q {1} \"{2}\"", - input, quality, output - )); + proc.onStdIOReady = () => + { + onIOReady(proc.stdIn,proc.stdOut); + }; - return true; + proc.flag_disable_user_stdout = true; + LOG.log("[FFMPEG] : Converting PCM Stream to WAV Stream"); + proc.start($"-f s16le -ar 44.1k -ac 2 -i pipe:0 -f wav -"); }// ----------------------------------------- /// - /// Convert a PCM audio file to FLAC - /// ! Overwrites all generated files ! - /// ! Does not check INPUT file ! + /// Take a WAV stream, and save it as a PCM File /// - /// - /// If ommited, will be automatically set + /// + /// /// - public bool audioPCMToFlac(string input,string output = null) + public bool convertWavStreamToPCM(string output,Action onReady) { - if(string.IsNullOrEmpty(output)) { - output = Path.ChangeExtension(input,"flac"); - }else{ - //[safeguard] Make sure it's a FLAC - if(!output.ToLower().EndsWith(".flac")) { - output += ".flac"; // try to fix it - } - } - - LOG.log("[FFMPEG] : Converting \"{0}\" to FLAC",input); - - _initProgressVars(input); - - // C#6 string interpolation - proc.start($"-y -f s16le -ar 44.1k -ac 2 -i \"{input}\" -c:a flac \"{output}\""); - + proc.onStdIOReady = ()=> { + onReady(proc.stdIn); + }; + // note -y overwrites output file + LOG.log("[FFMPEG] : Converting STDIN to PCM file {0}", output); + proc.start($"-f wav -i pipe:0 -y -f s16le -acodec pcm_s16le {output}"); return true; }// ----------------------------------------- + /// - /// Convert a PCM audio file to MP3 Variable Bitrate - /// ! Overwrites all generated files ! + /// Convert an audio file to a PCM file for use in a CD audio /// ! Does not check INPUT file ! + /// ! Overwrites all generated files ! /// /// - /// 9 to 0 (lowest -> highest) - /// + /// If ommited, will be automatically set /// - public bool audioPCMToMP3(string input, int quality, string output = null) + public bool audioToPCM(string input,string output = null) { if(string.IsNullOrEmpty(output)) { - output = Path.ChangeExtension(input,"mp3"); - }else{ - //[safeguard] Make sure it's a FLAC - if(!output.ToLower().EndsWith(".mp3")) { - output += ".mp3"; // try to fix it - } + output = Path.ChangeExtension(input,"pcm"); } - LOG.log("[FFMPEG] : Converting \"{0}\" to MP3 {1}kbps", input, MP3_QUALITY[quality]); - - _initProgressVars(input); - proc.start($"-y -f s16le -ar 44.1k -ac 2 -i \"{input}\" -c:a libmp3lame -q:a {quality} \"{output}\""); - // https://trac.ffmpeg.org/wiki/Encode/MP3 - return true; - }// ----------------------------------------- + // Prepare progress variables + secondsConverted = progress = 0; + targetSeconds = getSecondsFromFile(input); + LOG.log("[FFMPEG] : Converting \"{0}\" to PCM", input); + proc.start($"-i \"{input}\" -y -f s16le -acodec pcm_s16le \"{output}\""); - // Helper - void _initProgressVars(string input) - { - var fsize = (int)new FileInfo(input).Length; - secondsConverted = progress = 0; - targetSeconds = (int)Math.Floor((double)fsize / 176400); // PCM is 176400 bytes per second - // LOG.log("[FFMPEG] : FILE SIZE = {0}, TARGET SECONDS = {1}", fsize, targetSeconds); + return true; }// ----------------------------------------- + }// -- end class -}// -- +}// -- \ No newline at end of file diff --git a/cdcrush/lib/app/FreeArc.cs b/cdcrush/lib/app/FreeArc.cs index 25381c1..4b66d05 100644 --- a/cdcrush/lib/app/FreeArc.cs +++ b/cdcrush/lib/app/FreeArc.cs @@ -8,8 +8,8 @@ namespace cdcrush.lib.app { /// /// FreeArc Wrapper /// -/// "onComplete" will be called automatically whenever the operation ends -/// "onProgress" is BROKEN +/// "onComplete" will be called automatically when the operation ends +/// "onProgress" callbacks progress percent /// /// class FreeArc : AbArchiver @@ -19,7 +19,8 @@ class FreeArc : AbArchiver // -- public FreeArc(string exePath = "") { - proc = new CliApp(Path.Combine(exePath,EXECUTABLE_NAME),true); + proc = new CliApp(Path.Combine(exePath,EXECUTABLE_NAME)); + proc.flag_stdout_word_mode = true; proc.onComplete = (code) => { @@ -30,7 +31,7 @@ public FreeArc(string exePath = "") else { ERROR = proc.stdErrLog; - onComplete(false); + onComplete?.Invoke(false); } }; diff --git a/cdcrush/lib/app/ICliReport.cs b/cdcrush/lib/app/IProcessStatus.cs similarity index 78% rename from cdcrush/lib/app/ICliReport.cs rename to cdcrush/lib/app/IProcessStatus.cs index 735eff2..ef393b0 100644 --- a/cdcrush/lib/app/ICliReport.cs +++ b/cdcrush/lib/app/IProcessStatus.cs @@ -5,7 +5,7 @@ namespace cdcrush.lib.app /** * An ASYNC CLI program that reports progress and complete statuses */ - public interface ICliReport + public interface IProcessStatus { // # USER READ string ERROR { get; } @@ -17,5 +17,8 @@ public interface ICliReport // # USER SET // OnComplete(Success), read ERROR for errors Action onComplete { get; set; } + + // Call this to force stop the current process + void kill(); } } diff --git a/cdcrush/lib/app/Tak.cs b/cdcrush/lib/app/Tak.cs new file mode 100644 index 0000000..f7523fc --- /dev/null +++ b/cdcrush/lib/app/Tak.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; + +namespace cdcrush.lib.app { + + +/// +/// +/// Tak audio encoder/decoder +/// +/// "onComplete" will be called automatically when the operation ends. +/// "onProgress" doesn't work +/// +/// +class Tak : IProcessStatus +{ + const string EXECUTABLE_NAME = "Takc.exe"; + private CliApp proc; + + // # USER SET :: + public Action onProgress { get; set; } + public Action onComplete { get; set; } + public string ERROR {get; private set;} + + // -- + public Tak(string exePath = "") + { + proc = new CliApp(Path.Combine(exePath,EXECUTABLE_NAME)); + + proc.onComplete = (code) => + { + if (code == 0) { + onComplete?.Invoke(true); + } + else + { + ERROR = proc.stdErrLog; + onComplete?.Invoke(false); + } + }; + }// ----------------------------------------- + + public void kill() => proc?.kill(); + + /// + /// Will encode a WAV file to TAK + /// + /// PAth of the `.wav` file + /// If ommited, it will be created on same dir as source file + public bool encode(string input,string output = "") + { + LOG.log("[TAK] : Encoding [{0}]",input); + + if(string.IsNullOrEmpty(output)) + { + proc.start($"-e \"{input}\""); + }else + { + proc.start($"-e \"{input}\" \"{output}\""); + } + return true; + }// ----------------------------------------- + + + /// + /// Restores a TAK file back to a WAV file + /// + /// Path of the `.tak` file + /// If ommited, it will be created on same dir as source file + public bool decode(string input,string output = "") + { + LOG.log("[TAK] : Decoding [{0}]",input); + + if(string.IsNullOrEmpty(output)) + { + proc.start($"-d \"{input}\""); + }else + { + proc.start($"-d \"{input}\" \"{output}\""); + } + return true; + }// ----------------------------------------- + + /// + /// Take a WAV Stream and Convert it to a TAK file + /// Callbacks the `onReady` when it is ready to receive stdIn data + /// + /// + /// + /// + public bool encodeFromStream(string output,Action onReady) + { + // E.G : c:\temp\TAK\Takc.exe -e -ihs - c:\temp\out.TAK + proc.onStdIOReady = ()=> { + onReady(proc.stdIn); + }; + + LOG.log("[TAK] : Encoding Stream to [{0}]",output); + proc.start($"-e -ihs - \"{output}\""); + return true; + }// ----------------------------------------- + + /// + /// Decode a .TAK file and output to stdout (as WAV) + /// Listen to Callback onReady and get stream from there + /// + /// + /// + /// + public bool decodeToStream(string input,Action onReady) + { + //E.G : Takc.exe -d input.tak - | flac.exe -8 - -o output.flac + proc.flag_disable_user_stdout = true; + proc.onStdIOReady = ()=> { + onReady(proc.stdOut); + }; + LOG.log("[TAK] : Decoding [{0}] to Stream",input); + proc.start($"-d \"{input}\" -"); + return true; + }// ----------------------------------------- + +}// -- end class +}// -- end namespace \ No newline at end of file diff --git a/cdcrush/lib/task/CTask.cs b/cdcrush/lib/task/CTask.cs index 93d4df2..1f9134a 100644 --- a/cdcrush/lib/task/CTask.cs +++ b/cdcrush/lib/task/CTask.cs @@ -179,7 +179,7 @@ public override string ToString() /// Quickly handle a CLI app completion /// /// - public void handleCliReport(app.ICliReport cli) + public void handleProcessStatus(app.IProcessStatus cli) { cli.onComplete = (s) => { diff --git a/cdcrush/prog/AudioMaster.cs b/cdcrush/prog/AudioMaster.cs new file mode 100644 index 0000000..d637065 --- /dev/null +++ b/cdcrush/prog/AudioMaster.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using cdcrush.lib.app; + +namespace cdcrush.prog +{ + +/// +/// An audio wrapper for all audio encoders used Ffmpeg, Tak +/// Provides generalized functionality for encoding/decoding audio +/// - Also provides universal Audio Codec IDs for use in forms +/// +public class AudioMaster +{ + // List of all codecIDs. Every codecID is understood by this class + public static string[] codecs = new [] { "VORBIS" , "OPUS", "MP3" , "FLAC", "TAK"}; + + static string[] lossyAudioExt = new [] { ".ogg", ".mp3" }; + + /// + /// Returns Array with String Info of Each Index, Default Index + /// + /// + /// Array with Quality Information, Default Quality Index + public static Tuple getQualityInfos(string codecID) + { + // There are just two codecs, so I am doing this check by hand + if(codecID=="TAK") + { + return Tuple.Create(new string[0],0); + } + + // It must be a codecID from FFMPEG I am not going to check + var cod = FFmpeg.getCodecByID(codecID); + return Tuple.Create(cod.getQualityInfos(),cod.qualityDefault); + }// -- + + /// + /// Return full Codec name, based on an ID + /// + /// + /// + public static string getCodecIDName(string codecID) + { + if(codecID=="TAK") return "Tak Lossless"; + // Should be FFmpeg + return FFmpeg.getCodecByID(codecID).name; + }// -- + + + /// + /// Get an informative string with Codec Name + + /// + /// + /// + public static string getCodecSettingsInfo(Tuple A) + { + var name = getCodecIDName(A.Item1); + var qinfos = getQualityInfos(A.Item1).Item1; + if(qinfos.Length>0) { + return name + " " + qinfos[A.Item2]; + } + return name; + }// -- + + public static string getCodecExt(string codecID) + { + if(codecID=="TAK") return ".tak"; + else return FFmpeg.getCodecByID(codecID).ext; + }// -- + + + + /// + /// Is a file lossy or not + /// + /// + /// + public static bool isLossyByExt(string ext) + { + return Array.Exists(lossyAudioExt,(s) => { + return s == ext.ToLower(); + }); + } + + + +}// - end class + +}// - end namespace diff --git a/cdcrush/prog/CDCRUSH.cs b/cdcrush/prog/CDCRUSH.cs index c5d5dc8..adaa68a 100644 --- a/cdcrush/prog/CDCRUSH.cs +++ b/cdcrush/prog/CDCRUSH.cs @@ -18,7 +18,7 @@ public static class CDCRUSH // -- Program Infos public const string AUTHORNAME = "John Dimi"; public const string PROGRAM_NAME = "cdcrush"; - public const string PROGRAM_VERSION = "1.4.2"; + public const string PROGRAM_VERSION = "1.4.3"; public const string PROGRAM_SHORT_DESC = "Highy compress cd-image games"; public const string LINK_DONATE = "https://www.paypal.me/johndimi"; public const string LINK_SOURCE = "https://github.com/johndimi/cdcrush.net"; @@ -29,13 +29,15 @@ public static class CDCRUSH // When restoring a cd to a folder, put this at the end of the folder's name public const string RESTORED_FOLDER_SUFFIX = " (r)"; + public const int FREEARC_DEF_COMPRESSION_INDEX = 3; // This is the index on the form. Actual level is + 1 + // -- Global // Keep temporary files, don't delete them // Currently for debug builds only public static bool FLAG_KEEP_TEMP = false; - // Maximum concurrent tasks in CJobs + // Maximum concurrent tasks in CJobs (default value) public static int MAX_TASKS = 2; // FFmpeg executable name @@ -82,18 +84,6 @@ public static class CDCRUSH public static int HACK_CD_TRACKS = 0; // ----------------------------------------- - // :: AUDIO QUALITY :: - - // Audio codecs as they appear on the form controls - public static readonly string[] AUDIO_CODECS = { - "FLAC", - "Ogg Vorbis", - "Ogg Opus", - "MP3" - }; - - // ----------------------------------- - /// /// Init Variables program /// @@ -192,32 +182,6 @@ public static bool setTempFolder(string path = null) return true; }// ----------------------------------------- - - /// - /// Translate from an AudioSettings tuple to String Descriptor - /// - /// The index of the combobox. Or Quality passed to the crush job - public static string getAudioQualityString(Tuple A) - { - string res = AUDIO_CODECS[A.Item1]; - - switch(A.Item1) - { - case 0: // FLAC; - break; - case 1: // VORBIS - res += $" {FFmpeg.VORBIS_QUALITY[A.Item2]}k Vbr"; - break; - case 2: // OPUS - res += $" {FFmpeg.OPUS_QUALITY[A.Item2]}k Vbr"; - break; - case 3: // MP3 - res += $" {FFmpeg.MP3_QUALITY[A.Item2]}k Vbr"; - break; - } - - return res; - }// ----------------------------------------- /// /// Convert from bin/cue to encoded audio/cue @@ -228,7 +192,7 @@ public static string getAudioQualityString(Tuple A) /// Title of the CD /// Completed (completeStatus,final Size) /// - public static bool startJob_ConvertCue(string _Input, string _Output, Tuple _Audio, + public static bool startJob_ConvertCue(string _Input, string _Output, Tuple _Audio, string _Title, Action onComplete) { if (LOCKED) { ERROR="Engine is working"; return false; } @@ -269,7 +233,7 @@ public static bool startJob_ConvertCue(string _Input, string _Output, TupleTitle of the CD /// Completed (completeStatus,CrushedSize) /// - public static bool startJob_CrushCD(string _Input, string _Output, Tuple _Audio, + public static bool startJob_CrushCD(string _Input, string _Output, Tuple _Audio, string _Cover, string _Title, int compressionLevel, Action onComplete) { if (LOCKED) { ERROR="Engine is working"; return false; } @@ -352,7 +316,6 @@ public static bool startJob_RestoreCD(string _Input, string _Output, }// ----------------------------------------- - /// /// Quickly load a CUE file, read it, check for validity and report back. /// Returns a customized object with some info diff --git a/cdcrush/prog/JobConvertCue.cs b/cdcrush/prog/JobConvertCue.cs index a22cf62..7ed0262 100644 --- a/cdcrush/prog/JobConvertCue.cs +++ b/cdcrush/prog/JobConvertCue.cs @@ -83,7 +83,7 @@ public JobConvertCue(CrushParams p):base("Convert CD") } // Real quality to string name - CD.CD_AUDIO_QUALITY = CDCRUSH.getAudioQualityString(p.audioQuality); + CD.CD_AUDIO_QUALITY = AudioMaster.getCodecSettingsInfo(p.audioQuality); t.complete(); @@ -182,7 +182,7 @@ public override void start() LOG.log("- Output Dir : {0}", p.outputDir); LOG.log("- Temp Dir : {0}", p.tempDir); LOG.log("- CD Title : {0}", p.cdTitle); - LOG.log("- Audio Quality : {0}",CDCRUSH.getAudioQualityString(p.audioQuality)); + LOG.log("- Audio Quality : {0}",AudioMaster.getCodecSettingsInfo(p.audioQuality)); base.start(); }// ----------------------------------------- diff --git a/cdcrush/prog/JobCrush.cs b/cdcrush/prog/JobCrush.cs index d39f560..deee99b 100644 --- a/cdcrush/prog/JobCrush.cs +++ b/cdcrush/prog/JobCrush.cs @@ -16,7 +16,7 @@ public struct CrushParams public string inputFile; // The CUE file to compress public string outputDir; // Output Directory. public string cdTitle; // Custom CD TITLE - public Tuple audioQuality; // Tuple !! + public Tuple audioQuality; // Tuple public string cover; // Cover image for the CD, square public int compressionLevel; // Describe the compression level, 0-10? public int expectedTracks; // In order for the progress report to work. set num of CD tracks here. @@ -103,8 +103,9 @@ public JobCrush(CrushParams p):base("Compress CD") } // Real quality to string name - cd.CD_AUDIO_QUALITY = CDCRUSH.getAudioQualityString(p.audioQuality); + cd.CD_AUDIO_QUALITY = AudioMaster.getCodecSettingsInfo(p.audioQuality); + // Generate the final arc name now that I have the CD TITLE jobData.finalArcPath = Path.Combine(p.outputDir, cd.CD_TITLE + CDCRUSH.CDCRUSH_EXTENSION); @@ -154,7 +155,7 @@ public JobCrush(CrushParams p):base("Compress CD") // Compress all the track files var arc = new FreeArc(CDCRUSH.TOOLS_PATH); - t.handleCliReport(arc); + t.handleProcessStatus(arc); arc.compress((string[])files.ToArray(typeof(string)), jobData.finalArcPath, p.compressionLevel); arc.onProgress = (pr) => t.PROGRESS = pr; t.killExtra = () => arc.kill(); @@ -190,7 +191,7 @@ public JobCrush(CrushParams p):base("Compress CD") // - Append the file(s) var arc = new FreeArc(CDCRUSH.TOOLS_PATH); - t.handleCliReport(arc); + t.handleProcessStatus(arc); arc.appendFiles(new string[]{path_settings, path_cover},jobData.finalArcPath); t.killExtra = () => arc.kill(); @@ -220,7 +221,7 @@ public override void start() LOG.log("- Output Dir : {0}", p.outputDir); LOG.log("- Temp Dir : {0}", p.tempDir); LOG.log("- CD Title : {0}", p.cdTitle); - LOG.log("- Audio Quality : {0}",CDCRUSH.getAudioQualityString(p.audioQuality)); + LOG.log("- Audio Quality : {0}",AudioMaster.getCodecSettingsInfo(p.audioQuality)); LOG.log("- Compression Level : {0}", p.compressionLevel); LOG.log("- Cover Image : {0}",p.cover); base.start(); diff --git a/cdcrush/prog/JobRestore.cs b/cdcrush/prog/JobRestore.cs index b4dba5a..bf41ed0 100644 --- a/cdcrush/prog/JobRestore.cs +++ b/cdcrush/prog/JobRestore.cs @@ -89,7 +89,7 @@ public JobRestore(RestoreParams p) : base("Restore CD") // ----------------------- add(new CTask((t) => { var arc = new FreeArc(CDCRUSH.TOOLS_PATH); - t.handleCliReport(arc); + t.handleProcessStatus(arc); arc.extractAll(p.inputFile, p.tempDir); arc.onProgress = (pr) => t.PROGRESS=pr; // In case the operation is aborted diff --git a/cdcrush/prog/TaskCompressTrack.cs b/cdcrush/prog/TaskCompressTrack.cs index eef9668..2ccee13 100644 --- a/cdcrush/prog/TaskCompressTrack.cs +++ b/cdcrush/prog/TaskCompressTrack.cs @@ -23,8 +23,9 @@ class TaskCompressTrack : lib.task.CTask CrushParams p; // Pointer to working track cd.CDTrack track; - // Temp name, Autogenerated - string sourceTrackFile; + + string INPUT; // File that is going to be encoded + string OUTPUT; // File that is going to be created from INPUT // -- public TaskCompressTrack(cd.CDTrack tr):base(null,"Encoding") @@ -42,12 +43,13 @@ public override void start() p = (CrushParams) jobData; // Working file is already set and points to either TEMP or INPUT folder - sourceTrackFile = track.workingFile; - + INPUT = track.workingFile; + // NOTE: OUTPUT path is generated later with the setupfiles() function + // Before compressing the tracks, get and store the MD5 of the track using(var md5 = System.Security.Cryptography.MD5.Create()) { - using(var str = File.OpenRead(sourceTrackFile)) + using(var str = File.OpenRead(INPUT)) { track.md5 = BitConverter.ToString(md5.ComputeHash(str)).Replace("-","").ToLower(); } @@ -57,75 +59,72 @@ public override void start() if(track.isData) { var ecm = new EcmTools(CDCRUSH.TOOLS_PATH); - ecm.onProgress = handleProgress; - ecm.onComplete = (s) => { - if(s) { - deleteOldFile(); - complete(); - }else{ - fail(msg:ecm.ERROR); - } - }; - - // In case the task ends abruptly - killExtra = () => ecm.kill(); + setupHandlers(ecm); // New filename that is going to be generated: setupFiles(".bin.ecm"); - ecm.ecm(sourceTrackFile,track.workingFile); // old .bin file from wherever it was to temp/bin.ecm + ecm.ecm(INPUT,OUTPUT); // old .bin file from wherever it was to temp/bin.ecm } else // AUDIO TRACK : { - var ffmp = new FFmpeg(CDCRUSH.FFMPEG_PATH); - ffmp.onProgress = handleProgress; - ffmp.onComplete = (s) => { - if(s) { - deleteOldFile(); - complete(); - }else { - fail(msg:ffmp.ERROR); - } - }; - - // In case the task ends abruptly - killExtra = () => ffmp.kill(); - - // Cast for easy coding - Tuple audioQ = jobData.audioQuality; - - // NOTE: I know this redundant, but it works : - switch(audioQ.Item1) - { - case 0: // FLAC - setupFiles(".flac"); - ffmp.audioPCMToFlac(sourceTrackFile, track.workingFile); - break; - - case 1: // VORBIS - setupFiles(".ogg"); - ffmp.audioPCMToOggVorbis(sourceTrackFile, audioQ.Item2, track.workingFile); - break; - - case 2: // OPUS - setupFiles(".ogg"); - // Opus needs an actual bitrate, not an index - ffmp.audioPCMToOggOpus(sourceTrackFile, FFmpeg.OPUS_QUALITY[audioQ.Item2], track.workingFile); - break; - - case 3: // MP3 - setupFiles(".mp3"); - ffmp.audioPCMToMP3(sourceTrackFile, audioQ.Item2, track.workingFile); - break; - - }//- end switch + // Get Audio Data. (codecID, codecQuality) + Tuple audioQ = jobData.audioQuality; + // New filename that is going to be generated: + setupFiles(AudioMaster.getCodecExt(audioQ.Item1)); + + // I need ffmpeg for both occations + var ffmp = new FFmpeg(CDCRUSH.FFMPEG_PATH); + setupHandlers(ffmp); + + if(audioQ.Item1=="TAK") + { + var tak = new Tak(CDCRUSH.TOOLS_PATH); + + // This will make FFMPEG read the PCM file, convert it to WAV on the fly + // and feed it to TAK, which will convert and save it. + ffmp.convertPCMStreamToWavStream( (ffmpegIn,ffmpegOut) => { + var sourceFile = File.OpenRead(INPUT); + tak.encodeFromStream(OUTPUT , (takIn) => { + ffmpegOut.CopyTo(takIn); + takIn.Close(); + }); + sourceFile.CopyTo(ffmpegIn); // Feed PCM to FFMPEG + ffmpegIn.Close(); + }); + + }else{ + // It must be FFMPEG + ffmp.encodePCM(audioQ.Item1, audioQ.Item2, INPUT, OUTPUT); + } + }//- end if (track.isData) }// ----------------------------------------- + + // Quickly add handlers to a process + // -- + void setupHandlers(IProcessStatus o) + { + o.onProgress = (p) => PROGRESS = p; // Uses setter + o.onComplete = (s) => { + if(s) { + deleteOldFile(); + complete(); + }else { + fail(msg:o.ERROR); + } + }; + + killExtra = () => o.kill(); // In case the task ends abruptly + }// -- + + // Qucikly set : // + storedFileName // + workingFile + // Ext must have the period (.) e.g. ".flac" void setupFiles(string ext) { track.storedFileName = track.getTrackName() + ext; @@ -137,12 +136,8 @@ void setupFiles(string ext) // Convert files to temp folder, since they are going to be archived later track.workingFile = Path.Combine(jobData.tempDir, track.storedFileName); } - }// ----------------------------------------- - // -- - void handleProgress(int p) - { - PROGRESS = p; // uses setter + OUTPUT = track.workingFile; }// ----------------------------------------- // -- @@ -154,7 +149,7 @@ void deleteOldFile() // Make sure the file is in the TEMP folder :: if(jobData.flag_sourceTracksOnTemp) { - File.Delete(sourceTrackFile); + File.Delete(INPUT); } }// ----------------------------------------- diff --git a/cdcrush/prog/TaskRestoreTrack.cs b/cdcrush/prog/TaskRestoreTrack.cs index 3d31634..bfbb5dc 100644 --- a/cdcrush/prog/TaskRestoreTrack.cs +++ b/cdcrush/prog/TaskRestoreTrack.cs @@ -18,9 +18,11 @@ class TaskRestoreTrack : lib.task.CTask RestoreParams p; cd.CDTrack track; - bool isFlac = false; + // Lossy audio codecs don't revert back to exact byte + bool requirePostSizeFix = false; - string crushedTrackPath; // Autocalculated + string INPUT; // The file that is going to be restored + string OUTPUT; // The file that is going to be created/restored to // -- public TaskRestoreTrack(cd.CDTrack tr) @@ -37,68 +39,50 @@ public override void start() p = (RestoreParams)jobData; - // -- - crushedTrackPath = Path.Combine(p.tempDir, track.storedFileName); - // Set the final track pathname now, I need this for later. - track.workingFile = Path.Combine(p.tempDir, track.getFilenameRaw()); + INPUT = Path.Combine(p.tempDir, track.storedFileName); + OUTPUT = Path.Combine(p.tempDir, track.getFilenameRaw()); + track.workingFile = OUTPUT; // Point to the new file + + // - + var fileExt = Path.GetExtension(track.storedFileName); + requirePostSizeFix = AudioMaster.isLossyByExt( fileExt ); // -- if(track.isData) { var ecm = new EcmTools(CDCRUSH.TOOLS_PATH); - ecm.onComplete = (s) => { - ecm.onProgress = handleProgress; - if(s){ - deleteOldFile(); - if(!checkTrackMD5()) { - fail(msg:"MD5 checksum is wrong!"); - return; - } - complete(); - }else{ - fail(msg:ecm.ERROR); - } - }; - ecm.unecm(crushedTrackPath); - killExtra = () => ecm.kill(); + setupHandlers(ecm); + ecm.unecm(INPUT); } else { - // No need to convert back + // No need to convert back, end the task if(p.flag_encCue) { - // Fix current file - track.workingFile = crushedTrackPath; + // Point to correct file + track.workingFile = INPUT; complete(); return; } - // -- - isFlac = (Path.GetExtension(track.storedFileName) == ".flac"); - var ffmp = new FFmpeg(CDCRUSH.FFMPEG_PATH); - ffmp.onProgress = handleProgress; - ffmp.onComplete = (s) => { - if(s){ - deleteOldFile(); // Don't need it - if(!isFlac) { - // OGG and MP3 don't restore to the exact byte length - correctPCMSize(); - }else - { - // FLAC restores to exact bytes - if(!checkTrackMD5()){ - fail(msg:"MD5 checksum is wrong!"); - return; - } - } - complete(); - }else{ - fail(msg:ffmp.ERROR); - } - }; - ffmp.audioToPCM(crushedTrackPath); - killExtra = () => ffmp.kill(); + if(fileExt.ToLower()==".tak") + { + var tak = new Tak(CDCRUSH.TOOLS_PATH); + setupHandlers(tak); + + tak.decodeToStream(INPUT,(_out) => { + ffmp.convertWavStreamToPCM(OUTPUT,(_in)=>{ + _out.CopyTo(_in); + _in.Close(); + }); + }); + + }else + { + setupHandlers(ffmp); + ffmp.audioToPCM(INPUT,track.workingFile); + } } log("Restoring track -" + track.storedFileName); @@ -106,22 +90,38 @@ public override void start() }// ----------------------------------------- // -- - void handleProgress(int p) + void setupHandlers(IProcessStatus o) { - PROGRESS = p; - }// ----------------------------------------- + killExtra = () => o.kill(); + o.onProgress = (p) => PROGRESS = p; // Note: Uses setter + o.onComplete = (s) => { + if(s){ + deleteOldFile(); + if(requirePostSizeFix) + { + correctPCMSize(); // Note: OGG and MP3 don't restore to the exact byte length + } + complete(); + }else{ + fail(msg:o.ERROR); + } + }; + } - // -- + + // - + // NOTE: Input files were created from the .ARC into TEMP folder, so I can delete them + // as soon as I am done with them void deleteOldFile() { if(CDCRUSH.FLAG_KEEP_TEMP) return; - File.Delete(crushedTrackPath); + File.Delete(INPUT); }// ----------------------------------------- // - // Fix the filesize of the restored track - // This is only when restoring from .OGG files, .FLAC seems to be fine by default. + // This is only when restoring from lossy encoders. Lossless restore to exact bytes void correctPCMSize() { using(FileStream fileStream = new FileStream(track.workingFile,FileMode.Open, FileAccess.Write)) diff --git a/tools/Takc.exe b/tools/Takc.exe new file mode 100644 index 0000000..76ed6c6 Binary files /dev/null and b/tools/Takc.exe differ