diff --git a/src/BizHawk.Common/FFmpegService.cs b/src/BizHawk.Common/FFmpegService.cs new file mode 100644 index 0000000000..1763f5a25d --- /dev/null +++ b/src/BizHawk.Common/FFmpegService.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; + +namespace BizHawk.Emulation.DiscSystem +{ + public class FFMpeg + { + public static string FFMpegPath; + + public class AudioQueryResult + { + public bool IsAudio; + } + + private static string[] Escape(IEnumerable args) + { + return args.Select(s => s.Contains(" ") ? $"\"{s}\"" : s).ToArray(); + } + + //note: accepts . or : in the stream stream/substream separator in the stream ID format, since that changed at some point in FFMPEG history + //if someone has a better idea how to make the determination of whether an audio stream is available, I'm all ears + private static readonly Regex rxHasAudio = new Regex(@"Stream \#(\d*(\.|\:)\d*)\: Audio", RegexOptions.Compiled); + public AudioQueryResult QueryAudio(string path) + { + var ret = new AudioQueryResult(); + string stdout = Run("-i", path).Text; + ret.IsAudio = rxHasAudio.Matches(stdout).Count > 0; + return ret; + } + + /// + /// queries whether this service is available. if ffmpeg is broken or missing, then you can handle it gracefully + /// + public bool QueryServiceAvailable() + { + try + { + string stdout = Run("-version").Text; + if (stdout.Contains("ffmpeg version")) return true; + } + catch + { + } + return false; + } + + public struct RunResults + { + public string Text; + public int ExitCode; + } + + public RunResults Run(params string[] args) + { + args = Escape(args); + StringBuilder sbCmdline = new StringBuilder(); + for (int i = 0; i < args.Length; i++) + { + sbCmdline.Append(args[i]); + if (i != args.Length - 1) sbCmdline.Append(' '); + } + + ProcessStartInfo oInfo = new ProcessStartInfo(FFMpegPath, sbCmdline.ToString()) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + Process proc = Process.Start(oInfo); + string result = proc.StandardOutput.ReadToEnd(); + result += proc.StandardError.ReadToEnd(); + proc.WaitForExit(); + + return new RunResults + { + ExitCode = proc.ExitCode, + Text = result + }; + } + + /// FFmpeg exited with non-zero exit code or produced no output + public byte[] DecodeAudio(string path) + { + string tempfile = Path.GetTempFileName(); + try + { + var runResults = Run("-i", path, "-xerror", "-f", "wav", "-ar", "44100", "-ac", "2", "-acodec", "pcm_s16le", "-y", tempfile); + if(runResults.ExitCode != 0) + throw new InvalidOperationException($"Failure running ffmpeg for audio decode. here was its output:\r\n{runResults.Text}"); + byte[] ret = File.ReadAllBytes(tempfile); + if (ret.Length == 0) + throw new InvalidOperationException($"Failure running ffmpeg for audio decode. here was its output:\r\n{runResults.Text}"); + return ret; + } + finally + { + File.Delete(tempfile); + } + } + } + + internal class AudioDecoder + { + [Serializable] + public class AudioDecoder_Exception : Exception + { + public AudioDecoder_Exception(string message) + : base(message) + { + } + } + + public AudioDecoder() + { + } + + private bool CheckForAudio(string path) + { + FFMpeg ffmpeg = new FFMpeg(); + var qa = ffmpeg.QueryAudio(path); + return qa.IsAudio; + } + + /// + /// finds audio at a path similar to the provided path (i.e. finds Track01.mp3 for Track01.wav) + /// TODO - isnt this redundant with CueFileResolver? + /// + private string FindAudio(string audioPath) + { + string basePath = Path.GetFileNameWithoutExtension(audioPath); + //look for potential candidates + var di = new DirectoryInfo(Path.GetDirectoryName(audioPath)); + var fis = di.GetFiles(); + //first, look for the file type we actually asked for + foreach (var fi in fis) + { + if (fi.FullName.ToUpper() == audioPath.ToUpper()) + if (CheckForAudio(fi.FullName)) + return fi.FullName; + } + //then look for any other type + foreach (var fi in fis) + { + if (Path.GetFileNameWithoutExtension(fi.FullName).ToUpper() == basePath.ToUpper()) + { + if (CheckForAudio(fi.FullName)) + { + return fi.FullName; + } + } + } + return null; + } + + /// could not find source audio for + public byte[] AcquireWaveData(string audioPath) => new FFMpeg() + .DecodeAudio(FindAudio(audioPath) ?? throw new AudioDecoder_Exception($"Could not find source audio for: {Path.GetFileName(audioPath)}")); + } +} \ No newline at end of file