Beispiel #1
0
        }        // -----------------------------------------

        /// <summary>
        /// Convert from bin/cue to encoded audio/cue
        /// </summary>
        /// <param name="_Input">Input file, must be `.cue`</param>
        /// <param name="_Output">Output folder, If null, it will be same as input file folder</param>
        /// <param name="_Audio">Audio Quality to encode the audio tracks with.</param>
        /// <param name="_Title">Title of the CD</param>
        /// <param name="onComplete">Completed (completeStatus,final Size)</param>
        /// <returns></returns>
        public static bool startJob_ConvertCue(string _Input, string _Output, Tuple <int, int> _Audio,
                                               string _Title, Action <bool, int> onComplete)
        {
            if (LOCKED)
            {
                ERROR = "Engine is working"; return(false);
            }
            if (!FFMPEG_OK)
            {
                ERROR = "FFmpeg is not set"; return(false);
            }

            LOCKED = true;

            var par = new CrushParams();                // CovertCue shares params with Crush job

            par.inputFile    = _Input;
            par.outputDir    = _Output;
            par.audioQuality = _Audio;
            par.cdTitle      = _Title;

            var j = new JobConvertCue(par);

            j.MAX_CONCURRENT = MAX_TASKS;
            j.onComplete     = (s) => {
                LOCKED = false;
                ERROR  = j.ERROR[1];
                onComplete(s, 0);                         // Disregard final filesize
            };

            j.onJobStatus = jobStatusHandler;                           // For status and progress updates
            j.start();

            return(true);
        }        // -----------------------------------------
Beispiel #2
0
        }// -----------------------------------------

        // -
        public override void start()
        {
            CrushParams p = jobData;

            LOG.line();
            LOG.log("=== CONVERTING A CD with the following parameters :");
            LOG.log("- Input : {0}", p.inputFile);
            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));
            base.start();
        }// -----------------------------------------
Beispiel #3
0
        }        // -----------------------------------------

        /// <summary>
        /// Compress a CD to output folder
        /// </summary>
        /// <param name="_Input">Input file, must be `.cue`</param>
        /// <param name="_Output">Output folder, If null, it will be same as input file folder</param>
        /// <param name="_Audio">Audio Quality to encode the audio tracks with.</param>
        /// <param name="_Cover">Cover Image to store in the archive</param>
        /// <param name="_Title">Title of the CD</param>
        /// <param name="onComplete">Completed (completeStatus,CrushedSize)</param>
        /// <returns></returns>
        public static bool startJob_CrushCD(string _Input, string _Output, Tuple <int, int> _Audio,
                                            string _Cover, string _Title, int compressionLevel, Action <bool, int, CueReader> onComplete)
        {
            if (LOCKED)
            {
                ERROR = "Engine is working"; return(false);
            }
            if (!FFMPEG_OK)
            {
                ERROR = "FFmpeg is not set"; return(false);
            }

            LOCKED = true;

            // Set the running parameters for the Crush (compress) job
            var par = new CrushParams {
                inputFile        = _Input,
                outputDir        = _Output,
                audioQuality     = _Audio,
                cover            = _Cover,
                cdTitle          = _Title,
                compressionLevel = compressionLevel,
                expectedTracks   = HACK_CD_TRACKS
            };

            // Create the job and set it up
            var j = new JobCrush(par);

            j.MAX_CONCURRENT = MAX_TASKS;
            j.onComplete     = (s) => {
                LOCKED = false;
                ERROR  = j.ERROR[1];
                // Note: job.jobData is a general use object that was set up in the job
                //		 I can read it and get things that I want from it
                if (s)
                {
                    onComplete(s, j.jobData.crushedSize, j.jobData.cd);                            // Hack, send CDINFO and SIZE as well
                }
                else
                {
                    onComplete(s, 0, null);
                }
            };

            j.onJobStatus = jobStatusHandler;                           // For status and progress updates
            j.start();

            return(true);
        }        // -----------------------------------------
Beispiel #4
0
        }        // -----------------------------------------

        /// <summary>
        /// Compress a CD to output folder
        /// </summary>
        /// <param name="_Input">Input file, must be `.cue`</param>
        /// <param name="_Output">Output folder, If null, it will be same as input file folder</param>
        /// <param name="_Audio">Audio Quality to encode the audio tracks with.</param>
        /// <param name="_Cover">Cover Image to store in the archive</param>
        /// <param name="_Title">Title of the CD</param>
        /// <param name="onComplete">Completed (completeStatus,MD5,CrushedSize)</param>
        /// <returns></returns>
        public static bool crushCD(string _Input, string _Output, int _Audio, string _Cover, string _Title,
                                   Action <bool, string, int> onComplete)
        {
            // NOTE : JOB checks for input file
            if (LOCKED)
            {
                ERROR = "Engine is working"; return(false);
            }
            if (!FFMPEG_OK)
            {
                ERROR = "FFmpeg is not set"; return(false);
            }

            LOCKED = true;

            var par = new CrushParams();

            par.inputFile    = _Input;
            par.outputDir    = _Output;
            par.audioQuality = _Audio;
            par.cover        = _Cover;
            par.cdTitle      = _Title;

            var j = new JobCrush(par);

            j.MAX_CONCURRENT = MAX_TASKS;

            j.onComplete = (s) =>
            {
                LOCKED = false;
                ERROR  = j.ERROR[1];
                if (s)
                {
                    CueReader cd = (CueReader)j.jobData.cd;
                    onComplete(s, cd.getFirstDataTrackMD5(), j.jobData.crushedSize);                           // Hack, send CDINFO and SIZE as well
                }
                else
                {
                    onComplete(s, "", 0);
                }
            };

            j.onJobStatus = jobStatusHandler;                           // For status and progress updates
            j.start();

            return(true);
        }        // -----------------------------------------
Beispiel #5
0
        }        // -----------------------------------------

        /// <summary>
        /// Starts either a CRUSH or CONVERT Job
        /// - Crush will encode all tracks and then create a .CRUSHED archive that can be restored lates
        /// - Convert will just encode all audio tracks and create new .CUE/.BIN/.ENCODED AUDIO FILES
        /// - [CONVERT] can create files in a folder or inside an Archive
        ///
        /// </summary>
        /// <param name="_Mode">0:Crush, 1:Convert, 2:Convert + ARCHIVE</param>
        /// <param name="_Input">Must be a valid CUE file </param>
        /// <param name="_Output">Output Directory - IF EMPTY will be same dir as INPUT</param>
        /// <param name="_Audio">Audio Settings TUPLE. Check `AudioMaster`</param>
        /// <param name="_ArchSet">Archive Settings Index. Check `ArchiveMaster` -1 for no archive</param>
        /// <param name="_Title">Name of the CD</param>
        /// <param name="_Cover">Path of Cover Image to store in the archive - CAN BE EMPTY</param>
        /// <param name="onComplete">Complete Calback (completeStatus,jobData object)</param>
        /// <returns>Preliminary Success</returns>
        public static bool startJob_Convert_Crush(
            int _Mode,
            string _Input,
            string _Output,
            Tuple <string, int> _Audio,
            int _ArchSet,
            string _Title,
            string _Cover,
            Action <bool, CrushParams> onComplete)
        {
            if (LOCKED)
            {
                ERROR = "Engine is working"; return(false);
            }
            if (!FFMPEG_OK)
            {
                ERROR = "FFmpeg is not set"; return(false);
            }
            LOCKED = true;

            // Set the running parameters for the job
            var par = new CrushParams {
                inputFile          = _Input,
                outputDir          = _Output,
                audioQuality       = _Audio,
                cover              = _Cover,
                cdTitle            = _Title,
                archiveSettingsInd = _ArchSet,
                expectedTracks     = HACK_CD_TRACKS,
                mode = _Mode
            };

            var job = new JobCrush(par);

            job.MAX_CONCURRENT = MAX_TASKS;
            job.onJobStatus    = jobStatusHandler;                      // For status and progress updates, FORM sets this.
            job.onComplete     = (s) => {
                LOCKED = false;
                ERROR  = job.ERROR[1];
                onComplete(s, job.jobData);
            };

            job.start();

            return(true);
        }
Beispiel #6
0
        }// -----------------------------------------

        /// <summary>
        /// Called on FAIL / COMPLETE / PROGRAM EXIT
        /// Clean up temporary files
        /// </summary>
        protected override void kill()
        {
            base.kill();

            if (CDCRUSH.FLAG_KEEP_TEMP)
            {
                return;
            }

            // - Cleanup
            CrushParams p = jobData;

            if (p.tempDir != p.outputDir)      // NOTE: This is always a subdir of the master temp dir
            {
                try {
                    Directory.Delete(p.tempDir, true);
                } catch (IOException) {
                    // do nothing
                }
            }    // --
        }// -----------------------------------------
Beispiel #7
0
        }        // -----------------------------------------

        /// <summary>
        /// Convert from bin/cue to encoded audio/cue
        /// </summary>
        /// <param name="_Input">Input file, must be `.cue`</param>
        /// <param name="_Output">Output folder, If null, it will be same as input file folder</param>
        /// <param name="_Audio">Audio Quality to encode the audio tracks with.</param>
        /// <param name="_Title">Title of the CD</param>
        /// <param name="onComplete">Completed (completeStatus,final Size)</param>
        /// <returns></returns>
        public static bool startJob_ConvertCue(string _Input, string _Output, Tuple <int, int> _Audio,
                                               string _Title, Action <bool, int, CueReader> onComplete)
        {
            if (LOCKED)
            {
                ERROR = "Engine is working"; return(false);
            }
            if (!FFMPEG_OK)
            {
                ERROR = "FFmpeg is not set"; return(false);
            }

            LOCKED = true;

            var par = new CrushParams {
                inputFile      = _Input,
                outputDir      = _Output,
                audioQuality   = _Audio,
                cdTitle        = _Title,
                expectedTracks = HACK_CD_TRACKS
            };              // CovertCue shares params with Crush job

            var j = new JobConvertCue(par);

            j.MAX_CONCURRENT = MAX_TASKS;
            j.onComplete     = (s) => {
                LOCKED = false;
                ERROR  = j.ERROR[1];
                onComplete(s, 0, j.jobData.cd);                         // Disregard final filesize, because it's not an archive
            };

            j.onJobStatus = jobStatusHandler;                           // For status and progress updates
            j.start();

            return(true);
        }        // -----------------------------------------
Beispiel #8
0
        // --
        public JobCrush(CrushParams p) : base("Compress CD")
        {
            // Check for input files
            // :: --------------------
            if (!CDCRUSH.check_file_(p.inputFile, ".cue"))
            {
                fail(msg: CDCRUSH.ERROR);
                return;
            }

            if (string.IsNullOrEmpty(p.outputDir))
            {
                p.outputDir = Path.GetDirectoryName(p.inputFile);
            }

            if (!FileTools.createDirectory(p.outputDir))
            {
                fail(msg: "Can't create Output Dir " + p.outputDir);
                return;
            }

            p.tempDir = Path.Combine(CDCRUSH.TEMP_FOLDER, Guid.NewGuid().ToString().Substring(0, 12));
            if (!FileTools.createDirectory(p.tempDir))
            {
                fail(msg: "Can't create TEMP dir");
                return;
            }

            // IMPORTANT!! sharedData gets set by value, NOT A POINTER, do not make changes to p after this
            jobData = p;

            // --
            // - Read the CUE file ::
            add(new CTask((t) =>
            {
                var cd     = new CueReader();
                jobData.cd = cd;

                if (!cd.load(p.inputFile))
                {
                    t.fail(msg: cd.ERROR);
                    return;
                }

                // Post CD CUE load ::

                // In case user named the CD, otherwise it's going to be the same
                if (!string.IsNullOrWhiteSpace(p.cdTitle))
                {
                    cd.CD_TITLE = FileTools.sanitizeFilename(p.cdTitle);
                }

                // Real quality to string name
                cd.CD_AUDIO_QUALITY = CDCRUSH.getAudioQualityString(p.audioQuality);

                // This flag notes that all files will go to the TEMP folder
                jobData.workFromTemp = !cd.MULTIFILE;

                // Generate the final arc name now that I have the CD TITLE
                jobData.finalArcPath = Path.Combine(p.outputDir, cd.CD_TITLE + ".arc");

                t.complete();
            }, "Reading", true));


            // - Cut tracks
            // ---------------------------
            add(new TaskCutTrackFiles());

            // - Compress tracks
            // ---------------------
            add(new CTask((t) =>
            {
                CueReader cd = jobData.cd;
                foreach (CueTrack tr in cd.tracks)
                {
                    addNextAsync(new TaskCompressTrack(tr));
                }        //--
                t.complete();
            }, "Preparing"));


            // Create Archive
            // Add all tracks to the final archive
            // ---------------------
            add(new CTask((t) => {
                CueReader cd = jobData.cd;

                // -- Get list of files::
                System.Collections.ArrayList files = new System.Collections.ArrayList();
                foreach (var tr in cd.tracks)
                {
                    files.Add(tr.workingFile);             // Working file is valid, was set earlier
                }

                // Compress all the track files
                var arc = new FreeArc(CDCRUSH.TOOLS_PATH);
                t.handleCliReport(arc);
                arc.compress((string[])files.ToArray(typeof(string)), jobData.finalArcPath, p.compressionLevel);

                t.killExtra = () => arc.kill();
            }, "Compressing"));


            // - Create CD SETTINGS and push it to the final archive
            // ( I am appending these files so that they can be quickly loaded later )
            // --------------------
            add(new CTask((t) =>
            {
                CueReader cd = jobData.cd;

                        #if DEBUG
                cd.debugInfo();
                        #endif

                string path_settings = Path.Combine(p.tempDir, CDCRUSH.CDCRUSH_SETTINGS);
                if (!cd.saveJson(path_settings))
                {
                    t.fail(msg: cd.ERROR);
                    return;
                }

                // - Cover Image Set?
                string path_cover;
                if (p.cover != null)
                {
                    path_cover = Path.Combine(p.tempDir, CDCRUSH.CDCRUSH_COVER);
                    File.Copy(p.cover, path_cover);
                }
                else
                {
                    path_cover = null;
                }

                // - Append the file(s)
                var arc = new FreeArc(CDCRUSH.TOOLS_PATH);
                t.handleCliReport(arc);
                arc.appendFiles(new string[] { path_settings, path_cover }, jobData.finalArcPath);

                t.killExtra = () => arc.kill();
            }, "Finalizing"));

            // - Get post data
            add(new CTask((t) =>
            {
                var finfo           = new FileInfo(jobData.finalArcPath);
                jobData.crushedSize = (int)finfo.Length;
                t.complete();
            }, "Finalizing"));

            // -- COMPLETE --
        }// -----------------------------------------
Beispiel #9
0
        }// -----------------------------------------

        // --
        public override void start()
        {
            base.start();

            p = (CrushParams)jobData;

            // Working file is already set and points to either TEMP or INPUT folder
            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(INPUT))
                {
                    track.md5 = BitConverter.ToString(md5.ComputeHash(str)).Replace("-", "").ToLower();
                }
            }

            // --
            if (track.isData)
            {
                var ecm = new EcmTools(CDCRUSH.TOOLS_PATH);
                setupHandlers(ecm);

                // New filename that is going to be generated:
                setupFiles(".bin.ecm");
                ecm.ecm(INPUT, OUTPUT); // old .bin file from wherever it was to temp/bin.ecm
            }
            else                        // AUDIO TRACK :
            {
                // Get Audio Data. (codecID, codecQuality)
                Tuple <string, int> audioQ = p.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)
        }// -----------------------------------------
        // --
        public JobConvertCue(CrushParams p) : base("Convert CD")
        {
            // Check for input files
            // :: --------------------
            if (!CDCRUSH.check_file_(p.inputFile, ".cue"))
            {
                fail(msg: CDCRUSH.ERROR);
                return;
            }

            if (string.IsNullOrEmpty(p.outputDir))
            {
                p.outputDir = Path.GetDirectoryName(p.inputFile);
            }

            // : NEW :
            // : ALWAYS Create a subfolder to avoid overwriting the source files
            p.outputDir = CDCRUSH.checkCreateUniqueOutput(p.outputDir, p.cdTitle + CDCRUSH.RESTORED_FOLDER_SUFFIX);
            if (p.outputDir == null)
            {
                fail("Output Dir Error " + p.outputDir);
                return;
            }

            // -
            p.tempDir = Path.Combine(CDCRUSH.TEMP_FOLDER, Guid.NewGuid().ToString().Substring(0, 12));
            if (!FileTools.createDirectory(p.tempDir))
            {
                fail(msg: "Can't create TEMP dir");
                return;
            }

            // Useful to know.
            p.flag_convert_only = true;

            // IMPORTANT!! sharedData gets set by value, NOT A POINTER, do not make changes to p after this
            jobData = p;

            hack_setExpectedProgTracks(p.expectedTracks + 2);

            // --
            // - Read the CUE file ::
            add(new CTask((t) =>
            {
                var cd     = new CueReader();
                jobData.cd = cd;

                if (!cd.load(p.inputFile))
                {
                    t.fail(msg: cd.ERROR);
                    return;
                }

                // --
                if (cd.tracks.Count == 1)
                {
                    t.fail(msg: "No point in converting. No audio tracks on the cd.");
                    return;
                }

                // Meaning the tracks are going to be extracted in the temp folder
                jobData.flag_sourceTracksOnTemp = (!cd.MULTIFILE && cd.tracks.Count > 1);

                // In case user named the CD, otherwise it's going to be the same
                if (!string.IsNullOrWhiteSpace(p.cdTitle))
                {
                    cd.CD_TITLE = FileTools.sanitizeFilename(p.cdTitle);
                }

                // Real quality to string name
                cd.CD_AUDIO_QUALITY = CDCRUSH.getAudioQualityString(p.audioQuality);

                t.complete();
            }, "-Reading"));


            // - Cut tracks
            // ---------------------------
            add(new TaskCutTrackFiles());

            // - Compress tracks
            // ---------------------
            add(new CTask((t) =>
            {
                // Only encode the audio tracks
                foreach (CueTrack tr in (jobData.cd as CueReader).tracks)
                {
                    if (!tr.isData)
                    {
                        addNextAsync(new TaskCompressTrack(tr));
                    }
                }
                t.complete();
            }, "-Preparing"));

            // - Create new CUE file
            // --------------------
            add(new CTask((t) =>
            {
                CueReader cd = jobData.cd;

                // DEV: So far :
                // track.trackFile is UNSET. cd.saveCue needs it to be set.
                // track.workingFile points to a valid file, some might be in TEMP folder and some in input folder (data tracks)

                int stepProgress = (int)Math.Round(100.0f / cd.tracks.Count);

                // -- Move files to output folder
                foreach (var track in cd.tracks)
                {
                    if (!cd.MULTIFILE)
                    {
                        // Fix the index times to start with 00:00:00
                        track.setNewTimesReset();
                    }

                    string ext = Path.GetExtension(track.workingFile);

                    track.trackFile = $"{cd.CD_TITLE} (track {track.trackNo}){ext}";

                    // Data track was not cut or encoded.
                    // It's in the input folder, don't move it
                    if (track.isData && cd.MULTIFILE)
                    {
                        FileTools.tryCopy(track.workingFile, Path.Combine(p.outputDir, track.trackFile));
                    }
                    else
                    {
                        // TaskCompress already put the audio files on the output folder
                        // But it's no big deal calling it again
                        // This is for the data tracks that are on the temp folder
                        FileTools.tryMove(track.workingFile, Path.Combine(p.outputDir, track.trackFile));
                    }

                    t.PROGRESS += stepProgress;
                }

                //-- Create the new CUE file
                if (!cd.saveCUE(Path.Combine(p.outputDir, cd.CD_TITLE + ".cue")))
                {
                    t.fail(cd.ERROR); return;
                }

                t.complete();
            }, "Finalizing"));

            // -- COMPLETE --
        }// -----------------------------------------
Beispiel #11
0
        // --
        public JobCrush(CrushParams par) : base("Compress CD")
        {
            p = par;

            // Hack to fix progress
            hack_setExpectedProgTracks(p.expectedTracks + 3);

            // - Read CUE and some init
            // ---------------------------
            add(new CTask((t) => {
                var cd = new cd.CDInfos();

                p.cd = cd;

                try{
                    cd.cueLoad(p.inputFile);
                }catch (haxe.lang.HaxeException e) {
                    t.fail(msg: e.Message); return;
                }

                // Meaning the tracks are going to be CUT in the temp folder, so they are safe to be removed
                p.flag_sourceTracksOnTemp = (!cd.MULTIFILE && cd.tracks.length > 1);

                // In case user named the CD, otherwise it's going to be the same
                if (!string.IsNullOrWhiteSpace(p.cdTitle))
                {
                    cd.CD_TITLE = FileTools.sanitizeFilename(p.cdTitle);
                }

                // Real quality to string name
                cd.CD_AUDIO_QUALITY = AudioMaster.getCodecSettingsInfo(p.audioQuality);

                if (p.mode != 1)      // Convert only does not require an archive
                {
                    // Generate the final ARCHIVE path now that I have the CD TITLE
                    p.finalArcPath = Path.Combine(p.outputDir, cd.CD_TITLE + p.archiver_ext);

                    // Try to create a new archive in case it exists?
                    while (File.Exists(p.finalArcPath))
                    {
                        LOG.log("{0} already exists, adding (_) until unique", p.finalArcPath);
                        // S is entire path without (.ext)
                        string S       = p.finalArcPath.Substring(0, p.finalArcPath.Length - p.archiver_ext.Length);
                        p.finalArcPath = S + "_" + p.archiver_ext;
                    }

                    LOG.log("- Destination Archive : {0}", p.finalArcPath);
                }

                if (p.mode == 1)
                {
                    // : ALWAYS Create a subfolder (when converting) to avoid overwriting the source files
                    p.outputDir = CDCRUSH.checkCreateUniqueOutput(p.outputDir, p.cdTitle + CDCRUSH.RESTORED_FOLDER_SUFFIX);
                    if (p.outputDir == null)
                    {
                        fail("Output Dir Error " + p.outputDir);
                        return;
                    }
                }

                jobData = p;         // Some TASKS read jobData
                t.complete();
            }, "-Reading", "Reading CUE data and preparing"));

            // - Cut tracks
            // ---------------------------
            add(new TaskCutTrackFiles());

            // - Encode tracks
            // ---------------------
            add(new CTask((t) =>
            {
                for (int i = 0; i < p.cd.tracks.length; i++)
                {
                    cd.CDTrack tr = p.cd.tracks[i] as cd.CDTrack;

                    // Do not encode DATA TRACKS to ECM when converting.
                    if (p.mode > 0 && tr.isData)
                    {
                        continue;
                    }

                    addNextAsync(new TaskCompressTrack(tr));
                }

                t.complete();
            }, "-Preparing", "Preparing to compress tracks"));



            // - Prepare Tracks on CONVERT modes
            // - Needed for the new .CUE to be created
            // - if CONVERT MODE, move all files to output
            if (p.mode > 0)
            {
                add(new CTask((t) =>
                {
                    // DEV: So far :
                    // track.trackFile is UNSET. cd.saveCue needs it to be set.
                    // track.workingFile points to a valid file, some might be in TEMP folder and some in input folder (data tracks)

                    int stepProgress = (int)Math.Round(100.0f / p.cd.tracks.length);

                    // -- Move files to output folder
                    for (int i = 0; i < p.cd.tracks.length; i++)
                    {
                        cd.CDTrack track = p.cd.tracks[i] as cd.CDTrack;

                        if (!p.cd.MULTIFILE)
                        {
                            // Fix the index times to start with 00:00:00
                            track.rewriteIndexes_forMultiFile();
                        }

                        string ext = Path.GetExtension(track.workingFile);

                        // This tells what the files should be named in the `.cue` file:
                        track.trackFile = $"{p.cd.CD_TITLE} (track {track.trackNo}){ext}";

                        // Data track was not cut or encoded.
                        // It's in the input folder, don't move it
                        if (track.isData && (p.cd.MULTIFILE || p.cd.tracks.length == 1))
                        {
                            if (p.mode == 1)
                            {
                                FileTools.tryCopy(track.workingFile, Path.Combine(p.outputDir, track.trackFile));
                                track.workingFile = Path.Combine(p.outputDir, track.trackFile);
                            }
                            else
                            {
                                // I need to copy all files to TEMP, so that they can be renamed and archived
                                FileTools.tryCopy(track.workingFile, Path.Combine(p.tempDir, track.trackFile));
                                track.workingFile = Path.Combine(p.tempDir, track.trackFile);
                            }
                        }
                        else         // encoded file that is on TEMP or OUTPUT
                        {
                            if (p.mode == 1)
                            {
                                // TaskCompress already put the audio files on the output folder
                                // But it's no big deal calling it again
                                // This is for the data tracks that are on the temp folder
                                FileTools.tryMove(track.workingFile, Path.Combine(p.outputDir, track.trackFile));
                                track.workingFile = Path.Combine(p.outputDir, track.trackFile);
                            }
                            else
                            {
                                // Track that has been encoded and is on TEMP
                                // It is currently named as "track_xx.xx" so rename it
                                FileTools.tryMove(track.workingFile, Path.Combine(p.tempDir, track.trackFile));
                                track.workingFile = Path.Combine(p.tempDir, track.trackFile);
                            }
                        }

                        t.PROGRESS += stepProgress;
                    }     // -- end processing tracks


                    if (p.mode == 1)
                    {
                        p.new_cue_path = Path.Combine(p.outputDir, p.cd.CD_TITLE + ".cue");
                    }
                    else
                    {
                        p.new_cue_path = Path.Combine(p.tempDir, p.cd.CD_TITLE + ".cue");
                    }

                    //. Create the new CUE file
                    try{
                        p.cd.cueSave(
                            p.new_cue_path,
                            new haxe.root.Array <object>(new [] {
                            "CDCRUSH (dotNet) version : " + CDCRUSH.PROGRAM_VERSION,
                            CDCRUSH.LINK_SOURCE
                        }));
                    }catch (haxe.lang.HaxeException e) {
                        t.fail(msg: e.Message); return;
                    }

                    t.complete();
                }, "Converting"));
            }


            // - Create an Archive
            // Add all tracks to the final archive
            // ---------------------
            if (p.mode != 1)
            {
                add(new CTask((t) =>
                {
                    // -- Get list of files to compress
                    // . Tracks
                    System.Collections.ArrayList files = new System.Collections.ArrayList();
                    for (var i = 0; i < p.cd.tracks.length; i++)
                    {
                        files.Add((p.cd.tracks[i] as cd.CDTrack).workingFile);         // Working file is valid, was set earlier
                    }

                    if (p.mode == 0)  // Only on CDCRUSH add cover and json data
                    {
                        // . Settings
                        string path_settings = Path.Combine(p.tempDir, CDCRUSH.CDCRUSH_SETTINGS);
                        try{
                            p.cd.jsonSave(path_settings);
                            files.Add(path_settings);
                        }catch (haxe.lang.HaxeException e) {
                            t.fail(msg: e.Message); return;
                        }

                        // . Cover Image
                        string path_cover;
                        if (p.cover != null)
                        {
                            path_cover = Path.Combine(p.tempDir, CDCRUSH.CDCRUSH_COVER);
                            File.Copy(p.cover, path_cover);
                            files.Add(path_cover);
                        }
                        else
                        {
                            path_cover = null;
                        }
                    }
                    else
                    {
                        // It must be CONVERT + ARCHIVE
                        files.Add(p.new_cue_path);
                    }

                    // -. Compress whatever files are on
                    var arc = ArchiveMaster.getArchiver(p.finalArcPath);

                    string arcStr = ArchiveMaster.getCompressionSettings(p.archiveSettingsInd).Item2;

                    arc.compress((string[])files.ToArray(typeof(string)), jobData.finalArcPath, -1, arcStr);
                    arc.onProgress = (pr) => t.PROGRESS = pr;
                    arc.onComplete = (s) => {
                        if (s)
                        {
                            // NOTE: This var is autowritten whenever a compress operation is complete
                            p.crushedSize = (int)arc.COMPRESSED_SIZE;
                            // IMPORTANT to write to jobdata, because it is not a pointer and this needs to be read externally
                            jobData = p;
                            t.complete();
                        }
                        else
                        {
                            fail(arc.ERROR);
                        }
                    };

                    t.killExtra = () => arc.kill();
                }, "Compressing", "Compressing everything into an archive"));
            }


            // -- COMPLETE --

            add(new CTask((t) =>
            {
                LOG.log("== Detailed CD INFOS ==");
                LOG.log(p.cd.getDetailedInfo());
                t.complete();
            }, "-complete"));
        }// -----------------------------------------
        }// -----------------------------------------

        // --
        public override void start()
        {
            base.start();

            p = (CrushParams)jobData;

            // Working file is already set and points to either TEMP or INPUT folder
            sourceTrackFile = track.workingFile;

            // 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))
                {
                    track.md5 = BitConverter.ToString(md5.ComputeHash(str)).Replace("-", "").ToLower();
                }
            }

            // --
            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();

                // 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
            }
            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 <int, int> 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
            }     //- end if (track.isData)
        }// -----------------------------------------
        // --
        public JobConvertCue(CrushParams p) : base("Convert CD")
        {
            // Check for input files
            // :: --------------------
            if (!CDCRUSH.check_file_(p.inputFile, ".cue"))
            {
                fail(msg: CDCRUSH.ERROR);
                return;
            }

            if (string.IsNullOrEmpty(p.outputDir))
            {
                p.outputDir = Path.GetDirectoryName(p.inputFile);
            }

            // : NEW :
            // : ALWAYS Create a subfolder to avoid overwriting the source files
            try {
                p.outputDir = Path.Combine(p.outputDir, p.cdTitle + FOLDER_SUFFIX);
            }
            catch (ArgumentException) {
                fail("Output Dir Error " + p.outputDir);
                return;
            }

            if (!FileTools.createDirectory(p.outputDir))
            {
                fail(msg: "Can't create Output Dir " + p.outputDir);
                return;
            }

            p.tempDir = Path.Combine(CDCRUSH.TEMP_FOLDER, Guid.NewGuid().ToString().Substring(0, 12));
            if (!FileTools.createDirectory(p.tempDir))
            {
                fail(msg: "Can't create TEMP dir");
                return;
            }

            // IMPORTANT!! sharedData gets set by value, NOT A POINTER, do not make changes to p after this
            jobData = p;

            // --
            // - Read the CUE file ::
            add(new CTask((t) =>
            {
                var cd     = new CueReader();
                jobData.cd = cd;

                if (!cd.load(p.inputFile))
                {
                    t.fail(msg: cd.ERROR);
                    return;
                }

                // Post CD CUE load ::

                // In case user named the CD, otherwise it's going to be the same
                if (!string.IsNullOrWhiteSpace(p.cdTitle))
                {
                    cd.CD_TITLE = FileTools.sanitizeFilename(p.cdTitle);
                }

                // Real quality to string name
                cd.CD_AUDIO_QUALITY = CDCRUSH.getAudioQualityString(p.audioQuality);

                // This flag notes that all files will go to the TEMP folder
                jobData.workFromTemp = !cd.MULTIFILE;

                t.complete();
            }, "Reading", true));


            // - Cut tracks
            // ---------------------------
            add(new TaskCutTrackFiles());

            // - Compress tracks
            // ---------------------
            add(new CTask((t) =>
            {
                // Only encode the audio tracks
                foreach (CueTrack tr in (jobData.cd as CueReader).tracks)
                {
                    if (!tr.isData)
                    {
                        addNextAsync(new TaskCompressTrack(tr));
                    }
                }
                t.complete();
            }, "Preparing"));

            // - Create new CUE file
            // --------------------
            add(new CTask((t) =>
            {
                CueReader cd = jobData.cd;

                // DEV: So far :
                // track.trackFile is UNSET. cd.saveCue needs it to be set.
                // track.workingFile points to a valid file, some might be in TEMP folder and some in input folder (data tracks)

                // -- Move files to output folder
                foreach (var track in cd.tracks)
                {
                    if (!cd.MULTIFILE)
                    {
                        // Fix the index times to start with 00:00:00
                        track.setNewTimesReset();
                    }

                    string ext = Path.GetExtension(track.workingFile);

                    track.trackFile = $"{cd.CD_TITLE} (track {track.trackNo}){ext}";

                    // Data track was not cut or encoded.
                    // It's in the input folder, don't move it
                    if (track.isData && cd.MULTIFILE)
                    {
                        FileTools.tryCopy(track.workingFile, Path.Combine(p.outputDir, track.trackFile));
                    }
                    else
                    {
                        FileTools.tryMove(track.workingFile, Path.Combine(p.outputDir, track.trackFile));
                    }
                }

                //-- Create the new CUE file
                if (!cd.saveCUE(Path.Combine(p.outputDir, cd.CD_TITLE + ".cue")))
                {
                    t.fail(cd.ERROR); return;
                }

                t.complete();
            }, "Finalizing"));

            // -- COMPLETE --
        }// -----------------------------------------
        }// -----------------------------------------

        // --
        public override void start()
        {
            base.start();

            p = (CrushParams)jobData;

            // Working file is already set and points to either TEMP or INPUT folder
            trackFile = track.workingFile;

            // 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(trackFile))
                {
                    track.md5 = BitConverter.ToString(md5.ComputeHash(str)).Replace("-", "").ToLower();
                }
            }

            // --
            if (track.isData)
            {
                var ecm = new EcmTools(CDCRUSH.TOOLS_PATH);
                ecm.onComplete = (s) => {
                    if (s)
                    {
                        deleteOldFile();
                        complete();
                    }
                    else
                    {
                        fail(msg: ecm.ERROR);
                    }
                };

                // In case the task ends abruptly
                killExtra = () => ecm.kill();

                // New filename that is going to be generated:
                track.storedFileName = track.getTrackName() + ".bin.ecm";
                track.workingFile    = Path.Combine(jobData.tempDir, track.storedFileName);
                ecm.ecm(trackFile, track.workingFile); // old .bin file from wherever it was to temp/bin.ecm
            }
            else                                       // AUDIO TRACK :
            {
                var ffmp = new FFmpeg(CDCRUSH.FFMPEG_PATH);
                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 <int, int> audioQ = jobData.audioQuality;

                switch (audioQ.Item1)
                {
                case 0:                 // FLAC
                    track.storedFileName = track.getTrackName() + ".flac";
                    track.workingFile    = Path.Combine(jobData.tempDir, track.storedFileName);
                    ffmp.audioPCMToFlac(trackFile, track.workingFile);
                    break;

                case 1:                 // VORBIS
                    track.storedFileName = track.getTrackName() + ".ogg";
                    track.workingFile    = Path.Combine(jobData.tempDir, track.storedFileName);
                    ffmp.audioPCMToOggVorbis(trackFile, audioQ.Item2, track.workingFile);
                    break;

                case 2:                 // OPUS
                    track.storedFileName = track.getTrackName() + ".ogg";
                    track.workingFile    = Path.Combine(jobData.tempDir, track.storedFileName);
                    ffmp.audioPCMToOggOpus(trackFile, CDCRUSH.OPUS_QUALITY[audioQ.Item2], track.workingFile);
                    break;
                } //- end switch
            }     //- end if (track.isData)
        }// -----------------------------------------