├── .gitignore ├── Unai.ExtendedBinaryWaterfall.Cli ├── Program.cs └── Unai.ExtendedBinaryWaterfall.Cli.csproj ├── Unai.ExtendedBinaryWaterfall.Gui.GtkSharp ├── MainWindow.cs ├── MainWindow.glade ├── Program.cs ├── RenderDialog.cs ├── RenderDialog.glade └── Unai.ExtendedBinaryWaterfall.Gui.GtkSharp.csproj ├── Unai.ExtendedBinaryWaterfall.sln ├── Unai.ExtendedBinaryWaterfall ├── AudioBuffer.cs ├── AudioFrameResizer.cs ├── AudioSampleFormat.cs ├── BuildInfo.cs ├── CliParameterAttribute.cs ├── ExporterAttribute.cs ├── Exporters │ ├── FfmpegExporter.cs │ ├── IExporter.cs │ ├── NullExporter.cs │ └── SdlExporter.cs ├── Extensions.cs ├── FfmpegUtils.cs ├── Generator.cs ├── Logger.cs ├── Parsers │ ├── Custom │ │ └── CustomParser.cs │ ├── Elf │ │ ├── ElfParser.cs │ │ ├── ElfSectionHeader.cs │ │ └── ElfSectionType.cs │ ├── GameMaker │ │ ├── GameMakerChunk.cs │ │ ├── GameMakerChunkForm.cs │ │ └── GameMakerParser.cs │ ├── IParser.cs │ ├── Iso9660 │ │ ├── Iso9660Parser.cs │ │ ├── IsoDirectoryEntry.cs │ │ └── IsoPathTableEntry.cs │ ├── MiniDump │ │ └── MiniDumpParser.cs │ ├── ParserAttribute.cs │ ├── PortableExecutable │ │ └── PortableExecutableParser.cs │ ├── Wad │ │ └── WadParser.cs │ ├── WindowsIcon │ │ └── WindowsIconParser.cs │ ├── WindowsImage │ │ └── WindowsImageParser.cs │ └── Zip │ │ └── ZipParser.cs ├── SubFile.cs ├── Unai.ExtendedBinaryWaterfall.csproj └── Utils.cs ├── readme.md └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | **/[bB]in/** 2 | **/[oO]bj/** 3 | *.bup 4 | *.mkv 5 | .vscode/** 6 | *.glade~ -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using Unai.ExtendedBinaryWaterfall.Exporters; 9 | using Unai.ExtendedBinaryWaterfall.Parsers; 10 | 11 | namespace Unai.ExtendedBinaryWaterfall.Cli; 12 | 13 | class Program 14 | { 15 | static bool _helpMode = false; 16 | 17 | static readonly Generator _generator = new(); 18 | 19 | static void Main(string[] args) 20 | { 21 | // Make decimals use "." instead of other characters. 22 | CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; 23 | 24 | Logger.Info($"{BuildInfo.ApplicationName} {BuildInfo.SemVer ?? "unknown"}"); 25 | 26 | if (args.Length < 1) 27 | { 28 | Logger.Error("At least one argument must be specified."); 29 | PrintHelp(); 30 | return; 31 | } 32 | 33 | if (!ParseCommandLineArguments(args)) 34 | { 35 | Logger.Fail("Invalid command line arguments. Exiting…"); 36 | return; 37 | } 38 | 39 | if (_helpMode) 40 | { 41 | PrintHelp(); 42 | return; 43 | } 44 | 45 | try 46 | { 47 | _generator.Initialize(); 48 | _generator.Generate(); 49 | } 50 | catch (Exception ex) 51 | { 52 | Logger.Fail($"Unhandled exception while generating binary waterfall: {ex}"); 53 | } 54 | } 55 | 56 | private static bool ParseCommandLineArguments(IEnumerable args = null) 57 | { 58 | Logger.Info("Parsing command line arguments…"); 59 | 60 | foreach (var arg in args ?? Environment.GetCommandLineArgs()[1..]) 61 | { 62 | Logger.Debug($"Parsing command line argument: `{arg}`"); 63 | 64 | if (!arg.StartsWith('-')) 65 | { 66 | if (_generator.InputFilePath != null) 67 | { 68 | Logger.Error("Cannot specify more than two input files."); 69 | return false; 70 | } 71 | _generator.InputFilePath = arg; 72 | continue; 73 | } 74 | 75 | var argKvp = arg.Split('='); 76 | 77 | if (argKvp[0] == "--help" || argKvp[0] == "-h" || argKvp[0] == "-?") 78 | { 79 | _helpMode = true; 80 | continue; 81 | } 82 | 83 | var targetParam = Utils.GetPropertyFromCliArgument(argKvp[0]); 84 | 85 | if (targetParam == null) 86 | { 87 | Logger.Error($"Unknown argument: `{argKvp[0]}`."); 88 | return false; 89 | } 90 | 91 | // Can't do a `switch` statement here. :( 92 | if (targetParam.DeclaringType == typeof(Generator)) 93 | { 94 | if (!CliParameterAttribute.SetPropertyFromCliArgument(targetParam, _generator, argKvp[1])) 95 | { 96 | return false; 97 | } 98 | } 99 | else if (targetParam.DeclaringType.GetInterfaces().Contains(typeof(IExporter))) 100 | { 101 | _generator.AdditionalCliArguments.Add(argKvp[0], argKvp[1]); 102 | continue; 103 | } 104 | else 105 | { 106 | Logger.Error($"Cannot set property `{targetParam.Name}` because the instance of its declaring type is unknown."); 107 | return false; 108 | } 109 | } 110 | 111 | return true; 112 | } 113 | 114 | private static void PrintHelp() 115 | { 116 | StringBuilder helpStrBld = new(); 117 | helpStrBld.AppendLine("Usage:"); 118 | helpStrBld.AppendLine($" {Path.GetFileName(Environment.GetCommandLineArgs()[0])} [options]"); 119 | helpStrBld.AppendLine(); 120 | helpStrBld.AppendLine("Options:"); 121 | helpStrBld.AppendLine($" -h, -?, --help\n Print this help text and exit"); 122 | 123 | void AppendCommandLineArgument(PropertyInfo prop, int indentation = 1) 124 | { 125 | var cliParamAttr = prop.GetCustomAttribute(); 126 | helpStrBld.Append(new string('\t', indentation)); 127 | if (cliParamAttr.ShortParameterName.HasValue) 128 | { 129 | helpStrBld.Append($"-{cliParamAttr.ShortParameterName}, "); 130 | } 131 | helpStrBld.Append($"--{cliParamAttr.LongParameterName}=<{prop.PropertyType.Name}> ".PadRight(cliParamAttr.ShortParameterName.HasValue ? 28 : 32)); 132 | helpStrBld.AppendLine(cliParamAttr.Name); 133 | if (cliParamAttr.Description != null) 134 | { 135 | helpStrBld.AppendLine($"{new string('\t', indentation + 1)}{cliParamAttr.Description}"); 136 | } 137 | } 138 | 139 | foreach (var prop in Utils.GetPropertiesWithAttribute(typeof(Generator))) 140 | { 141 | AppendCommandLineArgument(prop); 142 | } 143 | helpStrBld.AppendLine(); 144 | 145 | helpStrBld.AppendLine("Available parsers/input formats:"); 146 | foreach (var parserKvp in Utils.GetTypesWithAttribute()) 147 | { 148 | var parserAttr = parserKvp.Key; 149 | helpStrBld.AppendLine($" {parserAttr.Id.PadRight(16)} {parserAttr.Name}"); 150 | } 151 | helpStrBld.AppendLine(); 152 | 153 | helpStrBld.AppendLine("Available exporters:"); 154 | foreach (var exporterKvp in Utils.GetTypesWithAttribute()) 155 | { 156 | var exporterAttr = exporterKvp.Key; 157 | helpStrBld.AppendLine($" {exporterAttr.Id.PadRight(16)} {exporterAttr.Name} – {exporterAttr.Description}"); 158 | 159 | var cliParams = Utils.GetPropertiesWithAttribute(exporterKvp.Value).ToList(); 160 | if (cliParams.Count > 0) 161 | { 162 | helpStrBld.AppendLine($" Options:"); 163 | foreach (var cliParam in cliParams) 164 | { 165 | AppendCommandLineArgument(cliParam, 3); 166 | } 167 | helpStrBld.AppendLine(); 168 | } 169 | } 170 | 171 | Console.Error.WriteLine(helpStrBld); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Cli/Unai.ExtendedBinaryWaterfall.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | exe 5 | net9.0 6 | True 7 | true 8 | 9 | v 10 | dev 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Gui.GtkSharp/MainWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Unai.ExtendedBinaryWaterfall.Parsers; 5 | using Unai.ExtendedBinaryWaterfall.Parsers.Custom; 6 | using static Gtk.Builder; 7 | using System.Threading.Tasks; 8 | using Gtk; 9 | using Unai.ExtendedBinaryWaterfall.Exporters; 10 | 11 | namespace Unai.ExtendedBinaryWaterfall.Gui.GtkSharp; 12 | 13 | public class MainWindow : Window 14 | { 15 | #pragma warning disable CS0649 16 | 17 | [Object] private FileChooserButton _uiInputFileChooser; 18 | [Object] private FileChooserButton _uiAuxInputFileChooser; 19 | [Object] private FileFilter _uiSaveFileDialogFilter; 20 | [Object] private ComboBox _uiParserComboBox; 21 | [Object] private ListStore _uiParserListStore; 22 | [Object] private ToolButton _uiPreviewBtn; 23 | 24 | private FileChooserDialog _uiSaveFileDialog = null; 25 | 26 | private readonly string _nullParserId = typeof(CustomParser).GetCustomAttribute().Id; 27 | private string _inputFilePath = null; 28 | private string _inputAuxFilePath = null; 29 | 30 | private static Generator _generator = null; 31 | private static Task _genTask = null; 32 | private static bool _genTaskFinished = false; 33 | private static float _genProgress = 0f; 34 | 35 | public MainWindow() : this(new Builder("MainWindow.glade")) 36 | { 37 | 38 | } 39 | 40 | private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("MainWindow")) 41 | { 42 | builder.Autoconnect(this); 43 | 44 | Title = BuildInfo.ApplicationName; 45 | 46 | foreach (var parser in Utils.GetTypesWithAttribute()) 47 | { 48 | _uiParserListStore.AppendValues(parser.Key.Id, parser.Key.Name); 49 | } 50 | 51 | var cellRen = new CellRendererText(); 52 | _uiParserComboBox.PackStart(cellRen, true); 53 | _uiParserComboBox.AddAttribute(cellRen, "text", 1); 54 | 55 | // This property is not showing on Glade so :/ 56 | _uiSaveFileDialogFilter.Name = "Matroska Video File (*.mkv)"; 57 | } 58 | 59 | private void UpdateBasedOnInputFile(object sender, EventArgs e) 60 | { 61 | _inputFilePath = _uiInputFileChooser.Filename; 62 | 63 | // TODO: Use Utils common method for this. 64 | var inputFileExtension = System.IO.Path.GetExtension(_inputFilePath); 65 | bool formatDetected = false; 66 | foreach (var parser in Utils.GetTypesWithAttribute()) 67 | { 68 | if (parser.Key.FileExtensions.Contains(inputFileExtension)) 69 | { 70 | _uiParserComboBox.SetActiveId(parser.Key.Id); 71 | formatDetected = true; 72 | break; 73 | } 74 | } 75 | 76 | if (!formatDetected) 77 | { 78 | var msgBox = new MessageDialog( 79 | this, 80 | DialogFlags.Modal, 81 | MessageType.Warning, 82 | ButtonsType.Ok, 83 | false, 84 | "Cannot detect file format.\n\nYou can manually specify a parser if you know the actual file format.\n\nAlternatively, you can use the “unknown format” parser and supply a custom CSV file with the desired subfile listing at the “Custom Subfile Listing File” option.\n\nAs a last resort, you can use the parser mentioned above without any subfile listing at all."); 85 | msgBox.Run(); 86 | msgBox.Destroy(); 87 | _uiParserComboBox.SetActiveId(_nullParserId); 88 | } 89 | 90 | UpdateBasedOnParser(sender, e); 91 | } 92 | 93 | private void UpdateBasedOnParser(object sender, EventArgs e) 94 | { 95 | // No action required yet. 96 | } 97 | 98 | private void UpdateBasedOnAuxiliaryInputFile(object sender, EventArgs e) 99 | { 100 | _inputAuxFilePath = _uiAuxInputFileChooser.Filename; 101 | } 102 | 103 | private void OnWaterfallPreviewClick(object sender, EventArgs e) 104 | { 105 | PrepareGenerator(); 106 | 107 | Application.Invoke((_, _) => 108 | { 109 | _generator.Initialize(); 110 | _generator.Generate(); 111 | // This should not be necessary, but here we are… 112 | ((SdlExporter)_generator.Exporter).Finish(); 113 | }); 114 | } 115 | 116 | private void OnWaterfallRenderClick(object sender, EventArgs e) 117 | { 118 | _uiSaveFileDialog = new FileChooserDialog("Output File", this, FileChooserAction.Save, "Save", ResponseType.Accept) 119 | { 120 | DoOverwriteConfirmation = true, 121 | CurrentName = $"{System.IO.Path.GetFileNameWithoutExtension(_inputFilePath)}.mkv" 122 | }; 123 | _uiSaveFileDialog.AddFilter(_uiSaveFileDialogFilter); 124 | 125 | int result = _uiSaveFileDialog.Run(); 126 | string outputFilePath = _uiSaveFileDialog.Filename; 127 | _uiSaveFileDialog.Destroy(); 128 | 129 | // Console.Error.WriteLine(result); 130 | if (result != (int)ResponseType.Accept) 131 | { 132 | return; 133 | } 134 | 135 | PrepareGenerator("ffmpeg"); 136 | _generator.OutputFilePath = outputFilePath; 137 | 138 | _genTaskFinished = false; 139 | // _generator.OnFinish += () => Program._renderDialog.Hide(); 140 | _generator.OnProgress += (p) => _genProgress = p; 141 | _genTask = Task.Run(() => 142 | { 143 | _generator.Initialize(); 144 | _generator.Generate(); 145 | _genTaskFinished = true; 146 | }); 147 | _genTask.ContinueWith(t => 148 | { 149 | if (t.IsFaulted) 150 | { 151 | Program.ShowUnhandledExceptionMessageBox(t.Exception); 152 | foreach (var innerEx in t.Exception.InnerExceptions) 153 | { 154 | Program.ShowUnhandledExceptionMessageBox(innerEx); 155 | } 156 | } 157 | Program._renderDialog.Hide(); 158 | }); 159 | 160 | Program._renderDialog = new(); 161 | Program._renderDialog.Show(); 162 | 163 | GLib.Idle.Add(new(() => 164 | { 165 | Program._renderDialog._uiStatusProgBar.Fraction = _genProgress; 166 | Program._renderDialog._uiStatusProgBar.Text = $"{_genProgress * 100:N2} %"; 167 | return !_genTaskFinished; 168 | })); 169 | } 170 | 171 | private void OnAboutBoxClick(object sender, EventArgs e) 172 | { 173 | var aboutBox = new AboutDialog() 174 | { 175 | Title = $"About {BuildInfo.ApplicationName}", 176 | ProgramName = BuildInfo.ApplicationName, 177 | Website = "https://github.com/unai-d/extended-binary-waterfall", 178 | WebsiteLabel = "GitHub Repository", 179 | Authors = [ "Unai Domínguez" ], 180 | Version = BuildInfo.SemVer, 181 | Modal = true, 182 | }; 183 | aboutBox.Show(); 184 | } 185 | 186 | private void PrepareGenerator(string exporterId = "sdl") 187 | { 188 | _generator = new() 189 | { 190 | ExporterId = exporterId, 191 | InputFilePath = _inputFilePath, 192 | InputFileFormatId = _uiParserComboBox.ActiveId, 193 | InputAuxiliaryFilePath = _inputAuxFilePath, 194 | }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Gui.GtkSharp/MainWindow.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | video/matroska 16 | 17 | 18 | *.mkv 19 | 20 | 21 | 22 | True 23 | False 24 | document-properties-symbolic 25 | 26 | 27 | True 28 | False 29 | emblem-documents-symbolic 30 | 31 | 32 | False 33 | Extended Binary Waterfall 34 | 480 35 | 220 36 | 37 | 38 | True 39 | False 40 | vertical 41 | 42 | 43 | 44 | True 45 | False 46 | 8 47 | 8 48 | 8 49 | 8 50 | 8 51 | 8 52 | True 53 | 54 | 55 | True 56 | False 57 | Target File 58 | 59 | 60 | 61 | 1 62 | 0 63 | 64 | 65 | 66 | 67 | True 68 | False 69 | Input Format / Parser 70 | 71 | 72 | 0 73 | 1 74 | 75 | 76 | 77 | 78 | True 79 | False 80 | _uiParserListStore 81 | 0 82 | 83 | 84 | 85 | 1 86 | 1 87 | 88 | 89 | 90 | 91 | True 92 | False 93 | Custom Subfile Listing File 94 | 95 | 96 | 0 97 | 2 98 | 99 | 100 | 101 | 102 | True 103 | True 104 | False 105 | Custom Subfile Listing File 106 | 107 | 108 | 109 | 1 110 | 2 111 | 112 | 113 | 114 | 115 | Settings… 116 | True 117 | False 118 | True 119 | True 120 | image1 121 | True 122 | 123 | 124 | 0 125 | 3 126 | 127 | 128 | 129 | 130 | Preview Subfiles… 131 | True 132 | False 133 | True 134 | True 135 | image2 136 | True 137 | 138 | 139 | 1 140 | 3 141 | 142 | 143 | 144 | 145 | True 146 | False 147 | Input File 148 | 149 | 150 | 0 151 | 0 152 | 153 | 154 | 155 | 156 | True 157 | True 158 | 1 159 | 160 | 161 | 162 | 163 | True 164 | False 165 | both 166 | 2 167 | 168 | 169 | True 170 | False 171 | _Preview 172 | True 173 | media-playback-start 174 | 175 | 176 | 177 | False 178 | True 179 | 180 | 181 | 182 | 183 | True 184 | False 185 | _Render 186 | True 187 | document-save 188 | 189 | 190 | 191 | False 192 | True 193 | 194 | 195 | 196 | 197 | True 198 | False 199 | 200 | 201 | False 202 | True 203 | 204 | 205 | 206 | 207 | True 208 | False 209 | About box 210 | True 211 | help-about 212 | 213 | 214 | 215 | False 216 | True 217 | 218 | 219 | 220 | 221 | False 222 | True 223 | 2 224 | 225 | 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Gui.GtkSharp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Gtk; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Gui.GtkSharp; 6 | 7 | static class Program 8 | { 9 | internal static Application _gtkApp = null; 10 | internal static Window _mainWin = null; 11 | internal static RenderDialog _renderDialog = null; 12 | 13 | [STAThread] 14 | static void Main(string[] args) 15 | { 16 | // Make decimals use "." instead of other characters. 17 | CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; 18 | 19 | GLib.ExceptionManager.UnhandledException += ShowUnhandledExceptionMessageBox; 20 | GLib.Global.ApplicationName = BuildInfo.ApplicationName; 21 | 22 | Logger.Debug($"Initializing GTK…"); 23 | 24 | Application.Init(); 25 | 26 | _gtkApp = new Application("io.github.unai-d.extended-binary-waterfall", GLib.ApplicationFlags.None); 27 | 28 | _gtkApp.Startup += (_, _) => 29 | { 30 | _mainWin = new MainWindow(); 31 | _gtkApp.AddWindow(_mainWin); 32 | 33 | _mainWin.ShowAll(); 34 | }; 35 | 36 | _gtkApp.Activated += (_, _) => 37 | { 38 | _gtkApp.Windows[0].Present(); 39 | }; 40 | 41 | ((GLib.Application)_gtkApp).Run(); 42 | } 43 | 44 | private static void ShowUnhandledExceptionMessageBox(GLib.UnhandledExceptionArgs args) 45 | { 46 | ShowUnhandledExceptionMessageBox((Exception)args.ExceptionObject); 47 | } 48 | 49 | internal static void ShowUnhandledExceptionMessageBox(Exception ex) 50 | { 51 | var errorMsgBox = new MessageDialog(null, 0, MessageType.Error, ButtonsType.Ok, false, $"Unhandled exception: {ex.Message}\n\n{ex.StackTrace}"); 52 | 53 | errorMsgBox.Run(); 54 | errorMsgBox.Destroy(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Gui.GtkSharp/RenderDialog.cs: -------------------------------------------------------------------------------- 1 | using Gtk; 2 | using static Gtk.Builder; 3 | 4 | namespace Unai.ExtendedBinaryWaterfall.Gui.GtkSharp; 5 | 6 | public class RenderDialog : Dialog 7 | { 8 | [Object] 9 | internal Label _uiStatusLabel; 10 | [Object] 11 | internal ProgressBar _uiStatusProgBar; 12 | 13 | public RenderDialog() : this(new Builder("RenderDialog.glade")) 14 | { 15 | 16 | } 17 | 18 | private RenderDialog(Builder builder) : base(builder.GetRawOwnedObject("RenderDialog")) 19 | { 20 | builder.Autoconnect(this); 21 | } 22 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Gui.GtkSharp/RenderDialog.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | Render Status 8 | False 9 | True 10 | center 11 | 320 12 | 120 13 | dialog 14 | 15 | 16 | False 17 | 8 18 | 8 19 | 8 20 | 8 21 | vertical 22 | 8 23 | 24 | 25 | False 26 | end 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | False 36 | False 37 | 0 38 | 39 | 40 | 41 | 42 | True 43 | False 44 | center 45 | True 46 | vertical 47 | 8 48 | 49 | 50 | True 51 | False 52 | Rendering… 53 | 54 | 55 | False 56 | True 57 | 0 58 | 59 | 60 | 61 | 62 | True 63 | False 64 | 0 % 65 | True 66 | 67 | 68 | False 69 | True 70 | 1 71 | 72 | 73 | 74 | 75 | False 76 | True 77 | 1 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.Gui.GtkSharp/Unai.ExtendedBinaryWaterfall.Gui.GtkSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %(Filename)%(Extension) 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.2.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unai.ExtendedBinaryWaterfall", "Unai.ExtendedBinaryWaterfall\Unai.ExtendedBinaryWaterfall.csproj", "{A62F55CB-160E-49D6-BD67-E09A97E13736}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unai.ExtendedBinaryWaterfall.Cli", "Unai.ExtendedBinaryWaterfall.Cli\Unai.ExtendedBinaryWaterfall.Cli.csproj", "{EFD64170-682A-4AD8-9BA9-69CF27171D7B}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unai.ExtendedBinaryWaterfall.Gui.GtkSharp", "Unai.ExtendedBinaryWaterfall.Gui.GtkSharp\Unai.ExtendedBinaryWaterfall.Gui.GtkSharp.csproj", "{11C19031-12AF-48DD-8DD8-5F7338178C5A}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Debug|x64.ActiveCfg = Debug|Any CPU 25 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Debug|x64.Build.0 = Debug|Any CPU 26 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Debug|x86.ActiveCfg = Debug|Any CPU 27 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Debug|x86.Build.0 = Debug|Any CPU 28 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Release|x64.ActiveCfg = Release|Any CPU 31 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Release|x64.Build.0 = Release|Any CPU 32 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Release|x86.ActiveCfg = Release|Any CPU 33 | {A62F55CB-160E-49D6-BD67-E09A97E13736}.Release|x86.Build.0 = Release|Any CPU 34 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Debug|x64.ActiveCfg = Debug|Any CPU 37 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Debug|x64.Build.0 = Debug|Any CPU 38 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Debug|x86.ActiveCfg = Debug|Any CPU 39 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Debug|x86.Build.0 = Debug|Any CPU 40 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Release|x64.ActiveCfg = Release|Any CPU 43 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Release|x64.Build.0 = Release|Any CPU 44 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Release|x86.ActiveCfg = Release|Any CPU 45 | {EFD64170-682A-4AD8-9BA9-69CF27171D7B}.Release|x86.Build.0 = Release|Any CPU 46 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Debug|x64.ActiveCfg = Debug|Any CPU 49 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Debug|x64.Build.0 = Debug|Any CPU 50 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Debug|x86.ActiveCfg = Debug|Any CPU 51 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Debug|x86.Build.0 = Debug|Any CPU 52 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Release|x64.ActiveCfg = Release|Any CPU 55 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Release|x64.Build.0 = Release|Any CPU 56 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Release|x86.ActiveCfg = Release|Any CPU 57 | {11C19031-12AF-48DD-8DD8-5F7338178C5A}.Release|x86.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {57D9CD7F-2CDB-4B09-BF06-6D491F7B2104} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/AudioBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Linq; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall; 6 | 7 | public class AudioBuffer 8 | { 9 | float[][] Samples { get; set; } 10 | public int ChannelCount => Samples.Length; 11 | public int SampleCount => Samples[0].Length; 12 | public int TotalSampleCount => SampleCount * ChannelCount; 13 | 14 | public AudioBuffer(int sampleCount, int channelCount) 15 | { 16 | Clear(sampleCount, channelCount); 17 | } 18 | 19 | public AudioBuffer(AudioBuffer source) 20 | { 21 | Clear(source.SampleCount, source.ChannelCount); 22 | for (int ch = 0; ch < ChannelCount; ch++) 23 | { 24 | Array.Copy(source.Samples[ch], Samples[ch], SampleCount); 25 | } 26 | } 27 | 28 | public AudioBuffer Clear(int? sampleCount = null, int? channelCount = null) 29 | { 30 | sampleCount ??= SampleCount; 31 | channelCount ??= ChannelCount; 32 | 33 | Samples = new float[channelCount.Value][]; 34 | for (int ch = 0; ch < channelCount; ch++) 35 | { 36 | Samples[ch] = new float[sampleCount.Value]; 37 | } 38 | 39 | return this; 40 | } 41 | 42 | public AudioBuffer LoadFromByteArray(byte[] buffer, AudioSampleFormat sampleFormat = AudioSampleFormat.Unsigned8) 43 | { 44 | float[] floatPcmBuffer = new float[buffer.Length / sampleFormat.GetByteSize()]; 45 | 46 | switch (sampleFormat) 47 | { 48 | case AudioSampleFormat.Unsigned8: 49 | floatPcmBuffer = buffer.Select(x => (x / 128f) - 1f).ToArray(); 50 | break; 51 | 52 | case AudioSampleFormat.Signed8: 53 | floatPcmBuffer = buffer.Select(x => x / 128f).ToArray(); 54 | break; 55 | 56 | case AudioSampleFormat.Unsigned16LE: 57 | for (int i = 0; i < floatPcmBuffer.Length; i++) 58 | { 59 | var srcIdx = i * 2; 60 | var srcSample = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(srcIdx, 2)); 61 | floatPcmBuffer[i] = (srcSample / 32768f) - 1f; 62 | } 63 | break; 64 | 65 | case AudioSampleFormat.Signed16LE: 66 | for (int i = 0; i < floatPcmBuffer.Length; i++) 67 | { 68 | var srcIdx = i * 2; 69 | var srcSample = BinaryPrimitives.ReadInt16LittleEndian(buffer.AsSpan(srcIdx, 2)); 70 | floatPcmBuffer[i] = srcSample / 32768f; 71 | } 72 | break; 73 | 74 | default: 75 | throw new InvalidOperationException("Audio sample format not implemented yet."); 76 | } 77 | 78 | for (int i = 0; i < floatPcmBuffer.Length; i++) 79 | { 80 | if (i / ChannelCount >= Samples[0].Length) 81 | { 82 | Logger.Warning($"PCM sample at index {i} does not fit inside sample buffer at index {i / ChannelCount}."); 83 | return this; 84 | } 85 | Samples[i % ChannelCount][i / ChannelCount] = floatPcmBuffer[i]; 86 | } 87 | 88 | return this; 89 | } 90 | 91 | public AudioBuffer Resample(int newSampleCount) 92 | { 93 | if (newSampleCount == SampleCount) return this; 94 | 95 | for (int ch = 0; ch < ChannelCount; ch++) 96 | { 97 | var newSampleBuffer = Samples[ch].LinearResample(newSampleCount); 98 | Samples[ch] = newSampleBuffer.ToArray(); 99 | } 100 | 101 | return this; 102 | } 103 | 104 | public AudioBuffer RemixChannels(int newChannelCount) 105 | { 106 | if (newChannelCount == ChannelCount) return this; 107 | 108 | var newSamples = new float[newChannelCount][]; 109 | 110 | for (int dch = 0; dch < newChannelCount; dch++) 111 | { 112 | var sch = (int)((dch / (float)newChannelCount) * ChannelCount); 113 | Array.Copy(newSamples[dch], Samples[sch], SampleCount); 114 | } 115 | 116 | Samples = newSamples; 117 | 118 | return this; 119 | } 120 | 121 | public float[] ToArray(bool planar = false) 122 | { 123 | float[] ret = new float[SampleCount * ChannelCount]; 124 | 125 | if (planar) 126 | { 127 | throw new NotImplementedException(); 128 | } 129 | 130 | for (int i = 0; i < ret.Length; i++) 131 | { 132 | ret[i] = Samples[i % ChannelCount][i / ChannelCount]; 133 | } 134 | 135 | return ret; 136 | } 137 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/AudioFrameResizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall; 4 | 5 | public class AudioFrameResizer 6 | { 7 | private T[] _outputBuffer = null; 8 | private int _bufOfs = 0; 9 | 10 | public int BufferLength 11 | { 12 | get => _outputBuffer.Length; 13 | set => _outputBuffer = new T[value]; 14 | } 15 | public Action OutputCallback { get; set; } = null; 16 | 17 | public void Push(T[] input) 18 | { 19 | var newBufOfs = _bufOfs + input.Length; 20 | if (newBufOfs >= BufferLength) 21 | { 22 | var firstHalfSize = BufferLength - _bufOfs; 23 | var secondHalfSize = Math.Abs(BufferLength - newBufOfs); 24 | Logger.Trace($"Input audio buffer of size {input.Length} will be divided like this: 0–{firstHalfSize} {firstHalfSize}+{firstHalfSize + secondHalfSize}"); 25 | Array.Copy(input, 0, _outputBuffer, _bufOfs, firstHalfSize); 26 | 27 | OutputCallback?.Invoke(_outputBuffer); 28 | 29 | Array.Copy(input, firstHalfSize, _outputBuffer, 0, secondHalfSize); 30 | _bufOfs = secondHalfSize; 31 | 32 | if (_bufOfs > BufferLength) 33 | { 34 | Logger.Error($"buffer overrun {_bufOfs} > {BufferLength}"); 35 | _bufOfs %= BufferLength; 36 | } 37 | } 38 | else 39 | { 40 | Array.Copy(input, 0, _outputBuffer, _bufOfs, input.Length); 41 | _bufOfs += input.Length; 42 | } 43 | Logger.Trace($"audio buf status: filled {_bufOfs,4}/{BufferLength,4} {BufferLength - _bufOfs} bytes left"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/AudioSampleFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Unai.ExtendedBinaryWaterfall; 2 | 3 | public enum AudioSampleFormat 4 | { 5 | Unsigned8, Signed8, 6 | Unsigned16LE, Unsigned16BE, Signed16LE, Signed16BE, 7 | Unsigned32LE, Unsigned32BE, Signed32LE, Signed32BE, 8 | Float16, Float32, Float64, 9 | } 10 | 11 | public static class AudioSampleFormatExtensions 12 | { 13 | public static int GetByteSize(this AudioSampleFormat asf) 14 | { 15 | return asf switch 16 | { 17 | AudioSampleFormat.Unsigned8 or AudioSampleFormat.Signed8 => 1, 18 | AudioSampleFormat.Unsigned16LE or AudioSampleFormat.Unsigned16BE or AudioSampleFormat.Signed16LE or AudioSampleFormat.Signed16BE or AudioSampleFormat.Float16 => 2, 19 | AudioSampleFormat.Unsigned32LE or AudioSampleFormat.Unsigned32BE or AudioSampleFormat.Signed32LE or AudioSampleFormat.Signed32BE or AudioSampleFormat.Float32 => 4, 20 | AudioSampleFormat.Float64 => 8, 21 | _ => 0 22 | }; 23 | } 24 | 25 | public static bool IsSigned(this AudioSampleFormat asf) 26 | { 27 | return asf switch 28 | { 29 | AudioSampleFormat.Signed8 or AudioSampleFormat.Signed16LE or AudioSampleFormat.Signed16BE or AudioSampleFormat.Signed32LE or AudioSampleFormat.Float16 or AudioSampleFormat.Float32 or AudioSampleFormat.Float64 => true, 30 | _ => false, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/BuildInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reflection; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall; 6 | 7 | public static class BuildInfo 8 | { 9 | public static string ApplicationName { get; } = "Extended Binary Waterfall"; 10 | 11 | public static string FullSemVer { get; private set; } = null; 12 | public static string SemVer { get; private set; } = null; 13 | public static string GitCommit { get; private set; } = null; 14 | 15 | static BuildInfo() 16 | { 17 | try 18 | { 19 | var asmInfVerAttr = Assembly.GetExecutingAssembly().GetCustomAttribute(); 20 | if (asmInfVerAttr != null) 21 | { 22 | FullSemVer = asmInfVerAttr.InformationalVersion; 23 | } 24 | else 25 | { 26 | FullSemVer = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; 27 | } 28 | 29 | if (FullSemVer != null) 30 | { 31 | if (FullSemVer.Contains('+')) 32 | { 33 | SemVer = FullSemVer[..FullSemVer.IndexOf('+')]; 34 | GitCommit = FullSemVer[(FullSemVer.IndexOf('+')+1)..]; 35 | } 36 | else 37 | { 38 | SemVer = FullSemVer; 39 | } 40 | } 41 | } 42 | catch (Exception ex) 43 | { 44 | Logger.Error($"Cannot get or compute program version: {ex.Message}"); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/CliParameterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Unai.ExtendedBinaryWaterfall; 5 | 6 | [AttributeUsage(AttributeTargets.Property)] 7 | public class CliParameterAttribute : Attribute 8 | { 9 | public string LongParameterName { get; set; } = null; 10 | public char? ShortParameterName { get; set; } = null; 11 | public string Name { get; set; } = null; 12 | public string Description { get; set; } = null; 13 | 14 | public CliParameterAttribute(string name, string longParamName, char shortParamName, string desc = null) 15 | { 16 | Name = name; 17 | LongParameterName = longParamName; 18 | ShortParameterName = shortParamName; 19 | Description = desc; 20 | } 21 | 22 | public CliParameterAttribute(string name, string longParamName, string desc = null) 23 | { 24 | Name = name; 25 | LongParameterName = longParamName; 26 | Description = desc; 27 | } 28 | 29 | public CliParameterAttribute() {} 30 | 31 | public static bool SetPropertyFromCliArgument(PropertyInfo targetProp, object targetObject, string value) 32 | { 33 | if (targetProp.PropertyType == typeof(string)) 34 | { 35 | targetProp.SetValue(targetObject, value.Replace("\\n", "\n")); 36 | } 37 | else if (targetProp.PropertyType == typeof(int)) 38 | { 39 | targetProp.SetValue(targetObject, int.Parse(value)); 40 | } 41 | else if (targetProp.PropertyType.IsEnum) 42 | { 43 | var ok = Enum.TryParse(targetProp.PropertyType, value, true, out var pval); 44 | if (!ok) 45 | { 46 | Logger.Error($"Cannot parse value '{value}' to enumeration '{targetProp.PropertyType.Name}'."); 47 | Logger.Info("Valid values:"); 48 | foreach (var enumVal in Enum.GetValues(targetProp.PropertyType)) 49 | { 50 | Logger.Info($" {enumVal}"); 51 | } 52 | return false; 53 | } 54 | targetProp.SetValue(targetObject, pval); 55 | } 56 | else if (targetProp.PropertyType == typeof(bool)) 57 | { 58 | targetProp.SetValue(targetObject, bool.Parse(value)); 59 | } 60 | else 61 | { 62 | Logger.Error($"Cannot convert string representation of value of property `{targetProp.Name}` because it is not implemented yet."); 63 | } 64 | return true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/ExporterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall; 4 | 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public class ExporterAttribute(string id, string name = null, string description = null) : Attribute 7 | { 8 | public string Id { get; set; } = id; 9 | public string Name { get; set; } = name; 10 | public string Description { get; set; } = description; 11 | 12 | public ExporterAttribute() : this(null) { } 13 | } 14 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Exporters/FfmpegExporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using FFmpeg.AutoGen; 4 | using SixLabors.ImageSharp; 5 | using SixLabors.ImageSharp.PixelFormats; 6 | 7 | namespace Unai.ExtendedBinaryWaterfall.Exporters; 8 | 9 | [Exporter("ffmpeg", "FFmpeg Stream", "Use FFmpeg libraries to encode audio and video data and output it in Matroska format.")] 10 | public class FfmpegExporter : IExporter 11 | { 12 | private bool _init = false; 13 | 14 | private unsafe AVFormatContext* _fmtCtx; 15 | 16 | private unsafe AVStream* _videoStream; 17 | private unsafe AVCodecContext* _videoCtx; 18 | private unsafe AVFrame* _videoAvFrame; 19 | private unsafe AVFrame* _videoAvFramePre; 20 | private unsafe AVPacket* _videoAvPacket; 21 | 22 | private unsafe AVStream* _audioStream; 23 | private unsafe AVCodecContext* _audioCtx; 24 | private unsafe AVFrame* _audioAvFrame; 25 | private unsafe AVPacket* _audioAvPacket; 26 | 27 | private unsafe SwsContext* _swsCtx; 28 | 29 | private readonly AudioFrameResizer _audioQueue = new(); 30 | 31 | private int _frameNum = 0; 32 | 33 | public Generator Generator { get; set; } 34 | 35 | #region User-defined properties 36 | 37 | [CliParameter("FFmpeg Log Level", "ffloglevel")] 38 | public int LogLevel { get; set; } = ffmpeg.AV_LOG_INFO; 39 | // [CliParameter("Output Video File Path", "output", 'o')] 40 | // public string OutputPath { get; set; } = null; 41 | [CliParameter("Output Video Bitrate", "output-bitrate")] 42 | public uint OutputVideoBitRate { get; set; } = 9_000_000; 43 | 44 | #endregion 45 | 46 | public void InitializeFfmpeg() 47 | { 48 | unsafe 49 | { 50 | ffmpeg.RootPath = FfmpegUtils.GetFfmpegLibraryPath(); 51 | Logger.Debug($"FFmpeg library path: '{ffmpeg.RootPath}'."); 52 | 53 | ffmpeg.av_log_set_level(LogLevel); 54 | av_log_set_callback_callback logCb = (p0, level, format, v1) => 55 | { 56 | if (level > ffmpeg.av_log_get_level()) return; 57 | var messageBufferLen = 1024; // is this too much for the stack? 58 | var messageBuffer = stackalloc byte[messageBufferLen]; 59 | var printPrefix = 1; 60 | ffmpeg.av_log_format_line(p0, level, format, v1, messageBuffer, messageBufferLen, &printPrefix); 61 | var message = Marshal.PtrToStringAnsi((nint)messageBuffer); 62 | Console.Error.Write(message); 63 | }; 64 | ffmpeg.av_log_set_callback(logCb); 65 | 66 | // format 67 | // ====== 68 | 69 | { 70 | AVFormatContext* fmtCtx = null; 71 | ffmpeg.avformat_alloc_output_context2(&fmtCtx, null, "matroska", Generator.OutputFilePath ?? "/dev/stdout"); 72 | if (fmtCtx == null) Console.Error.WriteLine("cannot allocate AVFormatContext"); 73 | _fmtCtx = fmtCtx; 74 | } 75 | if ((_fmtCtx->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0) 76 | { 77 | Logger.Debug("Format requested global stream headers."); 78 | } 79 | 80 | // encoders 81 | // ======== 82 | 83 | AVRational videoFps; videoFps.num = 60; videoFps.den = 1; 84 | 85 | var videoEnc = ffmpeg.avcodec_find_encoder(AVCodecID.AV_CODEC_ID_H264); 86 | var audioEnc = ffmpeg.avcodec_find_encoder(AVCodecID.AV_CODEC_ID_AAC); 87 | 88 | _videoCtx = ffmpeg.avcodec_alloc_context3(videoEnc); 89 | _videoCtx->codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO; 90 | _videoCtx->pix_fmt = AVPixelFormat.AV_PIX_FMT_YUV420P; 91 | _videoCtx->width = 1920; 92 | _videoCtx->height = 1080; 93 | _videoCtx->time_base.num = 1; 94 | _videoCtx->time_base.den = videoFps.num; 95 | _videoCtx->framerate.num = videoFps.num; 96 | _videoCtx->framerate.den = videoFps.den; 97 | _videoCtx->bit_rate = OutputVideoBitRate; 98 | // _videoCtx->thread_count = Environment.ProcessorCount / 2; 99 | // Console.Error.WriteLine($"using {_videoCtx->thread_count} threads"); 100 | if ((_fmtCtx->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0) 101 | { 102 | _videoCtx->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER; 103 | } 104 | // ffmpeg.av_opt_set(_videoCtx->priv_data, "crf", "23", 0); 105 | // h264 codec fails with EINVAL/11 if extradata does not get allocated manually. 106 | if (_videoCtx->codec->id == AVCodecID.AV_CODEC_ID_H264) 107 | { 108 | _videoCtx->extradata = (byte*)ffmpeg.av_malloc(32); 109 | _videoCtx->extradata_size = 24; 110 | } 111 | AVDictionary* videoEncOpts; 112 | var ret = ffmpeg.avcodec_open2(_videoCtx, videoEnc, &videoEncOpts); 113 | FfmpegUtils.LogIfAvError(ret, "cannot open video codec"); 114 | 115 | _audioCtx = ffmpeg.avcodec_alloc_context3(audioEnc); 116 | _audioCtx->codec_type = AVMediaType.AVMEDIA_TYPE_AUDIO; 117 | _audioCtx->sample_fmt = AVSampleFormat.AV_SAMPLE_FMT_FLTP; 118 | _audioCtx->sample_rate = Generator.AudioOutputSampleRate; 119 | _audioCtx->time_base.num = 1; 120 | _audioCtx->time_base.den = Generator.AudioOutputSampleRate; 121 | _audioCtx->ch_layout.nb_channels = 2; 122 | _audioCtx->ch_layout.order = AVChannelOrder.AV_CHANNEL_ORDER_NATIVE; 123 | _audioCtx->ch_layout.u.mask = ffmpeg.AV_CH_LAYOUT_STEREO; 124 | _audioCtx->bit_rate = 128_000; 125 | _audioCtx->extradata = (byte*)ffmpeg.av_mallocz(32); 126 | _audioCtx->extradata_size = 24; 127 | if ((_fmtCtx->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0) 128 | { 129 | _audioCtx->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER; 130 | } 131 | ret = ffmpeg.avcodec_open2(_audioCtx, audioEnc, null); 132 | FfmpegUtils.LogIfAvError(ret, "cannot open audio codec"); 133 | 134 | // streams 135 | // ======= 136 | 137 | _videoStream = ffmpeg.avformat_new_stream(_fmtCtx, null); 138 | if (_videoStream == null) Logger.Error("cannot allocate video output stream"); 139 | _videoStream->index = (int)(_fmtCtx->nb_streams - 1); 140 | _videoStream->time_base = _videoCtx->time_base; 141 | _videoStream->r_frame_rate = videoFps; 142 | 143 | ret = ffmpeg.avcodec_parameters_from_context(_videoStream->codecpar, _videoCtx); 144 | FfmpegUtils.LogIfAvError(ret, "cannot set video codec params from codec context"); 145 | 146 | _audioStream = ffmpeg.avformat_new_stream(_fmtCtx, null); 147 | if (_videoStream == null) Logger.Error("cannot allocate audio output stream"); 148 | _audioStream->index = (int)(_fmtCtx->nb_streams - 1); 149 | _audioStream->time_base = FfmpegUtils.GetRational(1, _audioCtx->sample_rate); 150 | 151 | ret = ffmpeg.avcodec_parameters_from_context(_audioStream->codecpar, _audioCtx); 152 | FfmpegUtils.LogIfAvError(ret, "cannot set audio codec params from codec context"); 153 | if (_audioStream->codecpar->extradata == null) 154 | { 155 | Logger.Error("audio codec did not create extradata buffer"); 156 | } 157 | 158 | // output file/stream 159 | // ================== 160 | 161 | ret = ffmpeg.avio_open(&_fmtCtx->pb, Generator.OutputFilePath ?? "pipe:", Generator.OutputFilePath != null ? ffmpeg.AVIO_FLAG_READ_WRITE : ffmpeg.AVIO_FLAG_WRITE); 162 | FfmpegUtils.LogIfAvError(ret, "cannot open stdout"); 163 | AVDictionary* fmtOpts; 164 | ret = ffmpeg.avformat_write_header(_fmtCtx, &fmtOpts); 165 | FfmpegUtils.LogIfAvError(ret, "cannot write header"); 166 | 167 | byte* dictBuf = (byte*)ffmpeg.av_malloc(1024); 168 | ffmpeg.av_dict_get_string(fmtOpts, &dictBuf, (byte)'=', (byte)':'); 169 | 170 | // video frames 171 | // ============ 172 | 173 | _videoAvFrame = ffmpeg.av_frame_alloc(); 174 | _videoAvFrame->format = (int)AVPixelFormat.AV_PIX_FMT_YUV420P; 175 | _videoAvFrame->width = 1920; 176 | _videoAvFrame->height = 1080; 177 | _videoAvFrame->time_base = _videoStream->time_base; 178 | 179 | ret = ffmpeg.av_frame_get_buffer(_videoAvFrame, 0); 180 | FfmpegUtils.LogIfAvError(ret, "cannot allocate video pixel buffer"); 181 | 182 | _videoAvFramePre = ffmpeg.av_frame_alloc(); 183 | _videoAvFramePre->format = (int)AVPixelFormat.AV_PIX_FMT_RGBA; 184 | _videoAvFramePre->width = 1920; 185 | _videoAvFramePre->height = 1080; 186 | _videoAvFramePre->time_base = _videoStream->time_base; 187 | 188 | ret = ffmpeg.av_frame_get_buffer(_videoAvFramePre, 0); 189 | FfmpegUtils.LogIfAvError(ret, "cannot allocate video pixel buffer"); 190 | 191 | // audio frames 192 | // ============ 193 | 194 | _audioAvFrame = ffmpeg.av_frame_alloc(); 195 | _audioAvFrame->format = (int)AVSampleFormat.AV_SAMPLE_FMT_FLTP; 196 | ffmpeg.av_channel_layout_copy(&_audioAvFrame->ch_layout, &_audioCtx->ch_layout); 197 | _audioAvFrame->sample_rate = _audioCtx->sample_rate; 198 | _audioAvFrame->nb_samples = _audioCtx->frame_size; 199 | _audioAvFrame->ch_layout.nb_channels = 2; 200 | _audioAvFrame->ch_layout.u.mask = 3; 201 | _audioAvFrame->time_base.num = _audioCtx->time_base.num; 202 | _audioAvFrame->time_base.den = _audioCtx->time_base.den; 203 | 204 | if ((_audioCtx->codec->capabilities & ffmpeg.AV_CODEC_CAP_VARIABLE_FRAME_SIZE) == 0) 205 | { 206 | Logger.Warning("audio codec does not support variable frame size"); 207 | } 208 | 209 | ret = ffmpeg.av_frame_get_buffer(_audioAvFrame, 0); 210 | FfmpegUtils.LogIfAvError(ret, "cannot allocate audio sample buffer"); 211 | _audioQueue.BufferLength = _audioAvFrame->nb_samples * _audioAvFrame->ch_layout.nb_channels; 212 | _audioQueue.OutputCallback = (buf) => 213 | { 214 | float* ab0 = (float*)_audioAvFrame->data[0]; 215 | float* ab1 = (float*)_audioAvFrame->data[1]; 216 | for (int i = 0; i < _audioAvFrame->linesize[0] / sizeof(float); i++) 217 | { 218 | ab0[i] = buf[i * 2]; 219 | ab1[i] = buf[i * 2 + 1]; 220 | } 221 | DoEncode(_audioCtx, _audioStream, _audioAvFrame, _audioAvPacket); 222 | }; 223 | 224 | Logger.Debug($"video original linesize = {_videoAvFramePre->linesize[0]} {_videoAvFramePre->linesize[1]}"); 225 | Logger.Debug($"video target linesize = {_videoAvFrame->linesize[0]} {_videoAvFrame->linesize[1]} {_videoAvFrame->linesize[2]}"); 226 | Logger.Debug($"req. audio frame size = {_audioCtx->frame_size} * {_audioCtx->ch_layout.nb_channels}ch"); 227 | Logger.Debug($"audio ch layout = {_audioAvFrame->ch_layout.nb_channels} {_audioAvFrame->ch_layout.order} {_audioAvFrame->ch_layout.u.mask}"); 228 | Logger.Debug($"audio linesizes = {_audioAvFrame->linesize[0]} {_audioAvFrame->linesize[1]} {_audioAvFrame->linesize[2]} {_audioAvFrame->linesize[3]} {_audioAvFrame->linesize[4]} {_audioAvFrame->linesize[5]} {_audioAvFrame->linesize[6]} {_audioAvFrame->linesize[7]}"); 229 | 230 | _videoAvPacket = ffmpeg.av_packet_alloc(); 231 | _audioAvPacket = ffmpeg.av_packet_alloc(); 232 | 233 | ffmpeg.av_dump_format(_fmtCtx, 0, Generator.OutputFilePath ?? "pipe:", 1); 234 | } 235 | _init = true; 236 | } 237 | 238 | private unsafe void DoEncode(AVCodecContext* cCtx, AVStream* stream, AVFrame* frame, AVPacket* packet) 239 | { 240 | int ret; 241 | 242 | FfmpegUtils.LogFrameData(frame); 243 | 244 | ret = ffmpeg.avcodec_send_frame(cCtx, frame); 245 | FfmpegUtils.LogIfAvError(ret, "cannot send frame to encoder"); 246 | 247 | while (ret >= 0) 248 | { 249 | ret = ffmpeg.avcodec_receive_packet(cCtx, packet); 250 | 251 | if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) 252 | { 253 | break; 254 | } 255 | else if (ret < 0) 256 | { 257 | // if frame is null, `EOF` code is expected, don't treat it as an error. 258 | if (frame != null) 259 | { 260 | FfmpegUtils.LogIfAvError(ret, "cannot encode"); 261 | } 262 | break; 263 | } 264 | 265 | ffmpeg.av_packet_rescale_ts(packet, cCtx->time_base, stream->time_base); 266 | packet->stream_index = stream->index; 267 | packet->time_base.num = stream->time_base.num; 268 | packet->time_base.den = stream->time_base.den; 269 | FfmpegUtils.LogPacketData(packet); 270 | 271 | ret = ffmpeg.av_interleaved_write_frame(_fmtCtx, packet); 272 | FfmpegUtils.LogIfAvError(ret, "cannot write packet"); 273 | if (ret == -32) Generator._exitRequested = true; 274 | } 275 | } 276 | 277 | public void PushNewFrame(Image videoFrame, AudioBuffer audioFrame, double delta) 278 | { 279 | PushNewFrame((Image)videoFrame, audioFrame, delta); 280 | } 281 | 282 | public unsafe void PushNewFrame(Image videoFrame, AudioBuffer audioFrame, double delta) 283 | { 284 | if (!_init) 285 | { 286 | InitializeFfmpeg(); 287 | } 288 | 289 | var ret = ffmpeg.av_frame_make_writable(_videoAvFrame); 290 | FfmpegUtils.LogIfAvError(ret, "cannot make video pixel data writable"); 291 | ret = ffmpeg.av_frame_make_writable(_audioAvFrame); 292 | FfmpegUtils.LogIfAvError(ret, "cannot make audio sample buffer writable"); 293 | 294 | _audioAvFrame->pts = (long)(_audioAvFrame->sample_rate * (_frameNum / (float)Generator.OutputFps)); 295 | _audioAvFrame->duration = Generator.AudioOutputSamplesPerFrame; 296 | 297 | // TODO: move to init method 298 | if (_swsCtx == null) 299 | { 300 | _swsCtx = ffmpeg.sws_getContext(videoFrame.Width, videoFrame.Height, (AVPixelFormat)_videoAvFramePre->format, videoFrame.Width, videoFrame.Height, (AVPixelFormat)_videoAvFrame->format, ffmpeg.SWS_BILINEAR, null, null, null); 301 | if (_swsCtx == null) 302 | { 303 | Logger.Error("cannot initialize sws context"); 304 | } 305 | } 306 | 307 | if (_swsCtx != null) 308 | { 309 | var pixelData = new byte[videoFrame.Width * videoFrame.Height * 4]; 310 | videoFrame.CopyPixelDataTo(pixelData); 311 | 312 | Marshal.Copy(pixelData, 0, (nint)_videoAvFramePre->data[0], pixelData.Length); 313 | ffmpeg.sws_scale(_swsCtx, _videoAvFramePre->data, _videoAvFramePre->linesize, 0, _videoAvFramePre->height, _videoAvFrame->data, _videoAvFrame->linesize); 314 | } 315 | else if (_videoAvFrame->format == (int)AVPixelFormat.AV_PIX_FMT_GBRP) 316 | { 317 | // Unoptimized pixel copy. 318 | videoFrame.ProcessPixelRows((pa) => 319 | { 320 | for (int y = 0; y < pa.Height; y++) 321 | { 322 | var row = pa.GetRowSpan(y); 323 | 324 | for (int x = 0; x < pa.Width; x++) 325 | { 326 | var p = row[x]; 327 | _videoAvFrame->data[0][_videoAvFrame->linesize[0] * y + x] = p.G; 328 | _videoAvFrame->data[1][_videoAvFrame->linesize[1] * y + x] = p.B; 329 | _videoAvFrame->data[2][_videoAvFrame->linesize[2] * y + x] = p.R; 330 | } 331 | } 332 | }); 333 | } 334 | 335 | _videoAvFrame->time_base.num = _videoCtx->time_base.num; 336 | _videoAvFrame->time_base.den = _videoCtx->time_base.den; 337 | _videoAvFrame->pts = _frameNum; 338 | _videoAvFrame->duration = 1; 339 | DoEncode(_videoCtx, _videoStream, _videoAvFrame, _videoAvPacket); 340 | 341 | _audioQueue.Push(audioFrame.ToArray()); 342 | 343 | if (_frameNum % 10 == 0) 344 | { 345 | Logger.Trace($"frame {_frameNum}, ts {_frameNum / Generator.OutputFps}, framegen speed {(int)(1/delta)} fps\x1b[K\x1b[G"); 346 | } 347 | 348 | _frameNum++; 349 | } 350 | 351 | public unsafe void Finish() 352 | { 353 | Logger.Debug("Flushing streams…"); 354 | DoEncode(_videoCtx, _videoStream, null, _videoAvPacket); 355 | DoEncode(_audioCtx, _audioStream, null, _audioAvPacket); 356 | 357 | Logger.Debug("Freeing FFmpeg resources…"); 358 | ffmpeg.sws_freeContext(_swsCtx); 359 | var videoCtx = _videoCtx; 360 | ffmpeg.avcodec_free_context(&videoCtx); 361 | var audioCtx = _audioCtx; 362 | ffmpeg.avcodec_free_context(&audioCtx); 363 | 364 | ffmpeg.av_write_trailer(_fmtCtx); 365 | 366 | if ((_fmtCtx->flags & ffmpeg.AVFMT_NOFILE) == 0) 367 | { 368 | ffmpeg.avio_closep(&_fmtCtx->pb); 369 | } 370 | 371 | ffmpeg.avformat_free_context(_fmtCtx); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Exporters/IExporter.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall.Exporters; 4 | 5 | public interface IExporter 6 | { 7 | public Generator Generator { get; set; } 8 | public abstract void PushNewFrame(Image videoFrame, AudioBuffer audioFrame, double delta = 0.04); 9 | public abstract void Finish(); 10 | } 11 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Exporters/NullExporter.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall.Exporters; 4 | 5 | [Exporter("null", "Null/Dummy Output", "Do nothing with the generated video. Useful for debugging purposes.")] 6 | public class NullExporter : IExporter 7 | { 8 | public Generator Generator { get; set; } 9 | 10 | public void Finish() 11 | { 12 | 13 | } 14 | 15 | public void PushNewFrame(Image videoFrame, AudioBuffer audioFrame, double delta = 0.04) 16 | { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Exporters/SdlExporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using SDL_Sharp; 5 | using SDL_Sharp.Loader; 6 | using SixLabors.ImageSharp; 7 | using SixLabors.ImageSharp.PixelFormats; 8 | 9 | namespace Unai.ExtendedBinaryWaterfall.Exporters; 10 | 11 | [Exporter("sdl", "SDL Window", "Show the generated audio and video data in a window.")] 12 | public class SdlExporter : IExporter 13 | { 14 | public Generator Generator { get; set; } 15 | 16 | private bool _init = false; 17 | private bool _isFullscreen = false; 18 | 19 | private Window _win; 20 | private Renderer _ren; 21 | private PSurface _surface; 22 | private uint _audioDeviceId; 23 | 24 | private byte[] _framebuffer = null; 25 | private readonly Stopwatch _sw = new(); 26 | private int _frameCount = 0; 27 | private double _ts = 0; 28 | 29 | [CliParameter("Adaptive Output Video Resolution", "sdl:adaptive-video-resolution", "Change generator parameters based on window size")] 30 | public bool AdaptiveFramebufferSize { get; set; } = false; 31 | 32 | public void InitializeSdl() 33 | { 34 | SdlLoader.LoadDefault(); 35 | unsafe 36 | { 37 | _ = SDL.Init(SdlInitFlags.Video | SdlInitFlags.Audio); 38 | _win = SDL.CreateWindow( 39 | "Extended Binary Waterfall", 40 | SDL.WINDOWPOS_UNDEFINED, 41 | SDL.WINDOWPOS_UNDEFINED, 42 | (int)(Generator.OutputVideoWidth * .75f), 43 | (int)(Generator.OutputVideoHeight * .75f), 44 | WindowFlags.Shown | WindowFlags.Resizable 45 | ); 46 | if (_win.IsNull) throw new Exception("SDL cannot create a window."); 47 | _ren = SDL.CreateRenderer(_win, -1, RendererFlags.Accelerated); 48 | if (_ren.IsNull) throw new Exception("SDL cannot create a renderer."); 49 | SDL.CreateRGBSurface(0, Generator.OutputVideoWidth, Generator.OutputVideoHeight, 32, 0xff, 0xff00, 0xff0000, 0, out _surface); 50 | if (_surface.IsNull) throw new Exception("SDL cannot create a surface."); 51 | _framebuffer = new byte[Generator.OutputVideoWidth * Generator.OutputVideoHeight * 4]; 52 | 53 | AudioSpec audioSpec; 54 | audioSpec.Frequency = Generator.AudioOutputSampleRate; 55 | audioSpec.Format = 0x8120; // 32-bit float LE 56 | audioSpec.Channels = 2; 57 | _audioDeviceId = SDL.OpenAudioDevice(null, 0, &audioSpec, null, 0); 58 | Logger.Debug($"OpenAudioDevice() = {_audioDeviceId}"); 59 | _ = SDL.PauseAudioDevice(_audioDeviceId, false); 60 | } 61 | _sw.Start(); 62 | _init = true; 63 | } 64 | 65 | public void PushNewFrame(Image videoFrame, AudioBuffer audioFrame, double delta) 66 | { 67 | PushNewFrame((Image)videoFrame, audioFrame, delta); 68 | } 69 | 70 | public void PushNewFrame(Image videoFrame, AudioBuffer audioFrame, double delta) 71 | { 72 | if (!_init) 73 | { 74 | InitializeSdl(); 75 | } 76 | 77 | // Handle Video 78 | 79 | unsafe 80 | { 81 | SDL.RenderClear(_ren); 82 | 83 | videoFrame.CopyPixelDataTo(_framebuffer); 84 | SDL.SetRenderDrawColor(_ren, 0, 32, 0, 255); 85 | Marshal.Copy(_framebuffer, 0, (nint)((Surface*)_surface)->Pixels, _framebuffer.Length); 86 | 87 | var tex = SDL.CreateTextureFromSurface(_ren, _surface); 88 | if (tex.IsNull) 89 | { 90 | Logger.Error("Texture is null!"); 91 | } 92 | SDL.RenderCopy(_ren, tex, 0, 0); 93 | 94 | SDL.RenderPresent(_ren); 95 | 96 | SDL.DestroyTexture(tex); 97 | } 98 | 99 | _frameCount++; 100 | _ts = _sw.Elapsed.TotalSeconds; 101 | var wcFrameCount = (int)(_ts * Generator.OutputFps); 102 | var framediff = _frameCount - wcFrameCount; // positive = too fast 103 | var deltaFps = 1 / delta; 104 | var renderSpeedRatio = deltaFps / Generator.OutputFps; 105 | 106 | if (framediff > 1) 107 | { 108 | SDL.Delay((uint)(((1 / (float)Generator.OutputFps) - delta) * 1000)); 109 | } 110 | 111 | // Handle Audio 112 | 113 | var audioQueue = SDL.GetQueuedAudioSize(_audioDeviceId); 114 | 115 | if (audioQueue < audioFrame.TotalSampleCount * 4) 116 | { 117 | unsafe 118 | { 119 | fixed (float* audioBufPtr = audioFrame.ToArray()) 120 | { 121 | var ret = SDL.QueueAudio(_audioDeviceId, (byte*)audioBufPtr, audioFrame.TotalSampleCount * sizeof(float)); 122 | if (ret != 0) Logger.Error($"Cannot queue audio buffer (code {ret}): {SDL.GetError()}"); 123 | } 124 | } 125 | } 126 | 127 | // Handle Window Events 128 | 129 | while (SDL.PollEvent(out Event e) == 1) 130 | { 131 | switch (e.Type) 132 | { 133 | case EventType.Quit: 134 | Generator._exitRequested = true; 135 | return; 136 | 137 | case EventType.KeyDown: 138 | var scancode = e.Keyboard.Keysym.Scancode; 139 | switch (scancode) 140 | { 141 | case Scancode.Escape: 142 | Generator._exitRequested = true; 143 | return; 144 | 145 | case Scancode.F11: 146 | _ = SDL.SetWindowFullscreen(_win, _isFullscreen ? 0 : WindowFlags.Fullscreen); 147 | _isFullscreen = !_isFullscreen; 148 | break; 149 | } 150 | break; 151 | 152 | case EventType.WindowEvent: 153 | SDL.GetWindowSize(_win, out int width, out int height); 154 | if (AdaptiveFramebufferSize) 155 | { 156 | if (width != Generator.OutputVideoWidth || height != Generator.OutputVideoHeight) 157 | { 158 | Generator.OutputVideoWidth = width; 159 | Generator.OutputVideoHeight = height; 160 | _framebuffer = new byte[Generator.OutputVideoWidth * Generator.OutputVideoHeight * 4]; 161 | SDL.FreeSurface(_surface); 162 | SDL.CreateRGBSurface(0, Generator.OutputVideoWidth, Generator.OutputVideoHeight, 32, 0xff, 0xff00, 0xff0000, 0, out _surface); 163 | Generator.UpdateValues(); 164 | } 165 | } 166 | break; 167 | } 168 | } 169 | 170 | if (renderSpeedRatio < 1) 171 | { 172 | Logger.Warning($"Render too slow! Generator is rendering at {renderSpeedRatio:N2}× speed."); 173 | } 174 | 175 | Console.Error.Write($"frame={_frameCount,6} wcframe={wcFrameCount,6} diff={framediff,6} — {(int)deltaFps} fps aqueue={audioQueue}\x1b[K\x1b[G"); 176 | } 177 | 178 | public void Finish() 179 | { 180 | _ = SDL.CloseAudioDevice(_audioDeviceId); 181 | SDL.FreeSurface(_surface); 182 | SDL.DestroyRenderer(_ren); 183 | SDL.DestroyWindow(_win); 184 | SDL.Quit(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using SixLabors.Fonts; 7 | using SixLabors.ImageSharp; 8 | using SixLabors.ImageSharp.Drawing.Processing; 9 | using SixLabors.ImageSharp.PixelFormats; 10 | using SixLabors.ImageSharp.Processing; 11 | 12 | namespace Unai.ExtendedBinaryWaterfall; 13 | 14 | public static class Extensions 15 | { 16 | public static void DrawProgressBar(this IImageProcessingContext ictx, float percent, int x1, int x2, float y) 17 | { 18 | percent = Math.Clamp(percent, 0, 1); 19 | 20 | ictx.DrawLine(new(), new SolidBrush(Color.FromRgb(32, 32, 32)), 8, 21 | new PointF(x1, y), 22 | new PointF(x2, y) 23 | ).DrawLine(new(), new SolidBrush(Color.Silver), 8, 24 | new PointF(x1, y), 25 | new PointF(x1 + (x2 - x1) * percent, y) 26 | ); 27 | } 28 | 29 | public static string ReadString(this BinaryReader br, int length, Encoding textEncoding = null) 30 | { 31 | textEncoding ??= Encoding.ASCII; 32 | return textEncoding.GetString(br.ReadBytes(length)); 33 | } 34 | 35 | public static string ReadCString(this BinaryReader br) 36 | { 37 | StringBuilder sb = new(); 38 | 39 | byte b; 40 | while ((b = br.ReadByte()) != 0) 41 | { 42 | sb.Append((char)b); 43 | } 44 | 45 | return sb.ToString(); 46 | } 47 | 48 | public static BinaryReader SkipCString(this BinaryReader br, int count = 1) 49 | { 50 | int skipCount = 0; 51 | while (skipCount < count) 52 | { 53 | Console.Error.WriteLine($"{skipCount}/{count} {Utils.GetBufferHexString(br, 16)}"); 54 | if (br.ReadByte() == 0) skipCount++; 55 | } 56 | return br; 57 | } 58 | 59 | static Dictionary> _textRenderCache = []; 60 | 61 | public static IImageProcessingContext DrawTextAndCache(this IImageProcessingContext ctx, DrawingOptions drawingOptions, RichTextOptions textOptions, string text, Brush brush, Pen pen) 62 | { 63 | if (string.IsNullOrEmpty(text)) return ctx; 64 | 65 | int hash = 0x91f_c28a; 66 | if (brush != null) hash ^= brush.GetHashCode(); 67 | if (pen != null) hash ^= pen.StrokeFill.GetHashCode(); 68 | foreach (var c in text) hash ^= c * 0xc10_48f1; 69 | hash ^= 0x183 * (int)textOptions.Font.Size; 70 | 71 | if (!_textRenderCache.TryGetValue(hash, out var cachedTextRender)) 72 | { 73 | Logger.Trace($"Generating cached version of text '{text}'…"); 74 | 75 | // Avoiding an `ArgumentNullException` from `TextOptions..ctor`. Blame this line of code: 76 | // https://github.com/SixLabors/Fonts/blob/d74f3fae7250cf3a76f43780abea6e15ec40b75e/src/SixLabors.Fonts/TextOptions.cs#L32C66-L32C86 77 | textOptions.FallbackFontFamilies ??= []; 78 | 79 | var newTextOpts = new RichTextOptions(textOptions) 80 | { 81 | Origin = new System.Numerics.Vector2(0, 0), 82 | HorizontalAlignment = HorizontalAlignment.Left, 83 | VerticalAlignment = VerticalAlignment.Top, 84 | }; 85 | 86 | var imgBounds = TextMeasurer.MeasureBounds(text, newTextOpts); 87 | Logger.Trace($" Measured raster bounds: {imgBounds}"); 88 | cachedTextRender = new Image((int)Math.Ceiling(imgBounds.Width + imgBounds.X), (int)Math.Ceiling(imgBounds.Height + imgBounds.Y)); 89 | cachedTextRender.Mutate(ctx2 => ctx2.DrawText(drawingOptions, newTextOpts, text, brush, pen)); 90 | 91 | _textRenderCache.Add(hash, cachedTextRender); 92 | } 93 | 94 | var x = textOptions.Origin.X; 95 | var y = textOptions.Origin.Y; 96 | 97 | if (textOptions.HorizontalAlignment == HorizontalAlignment.Center) 98 | { 99 | x -= cachedTextRender.Width / 2; 100 | } 101 | else if (textOptions.HorizontalAlignment == HorizontalAlignment.Right) 102 | { 103 | x -= cachedTextRender.Width; 104 | } 105 | if (textOptions.VerticalAlignment == VerticalAlignment.Center) 106 | { 107 | y -= cachedTextRender.Height / 2; 108 | } 109 | else if (textOptions.VerticalAlignment == VerticalAlignment.Bottom) 110 | { 111 | y -= cachedTextRender.Height; 112 | } 113 | 114 | return ctx.DrawImage(cachedTextRender, new Point((int)x, (int)y), 1f); 115 | } 116 | 117 | public static IImageProcessingContext DrawTextAndCache(this IImageProcessingContext ctx, RichTextOptions textOptions, string text, Color color) 118 | { 119 | return ctx.DrawTextAndCache(new(), textOptions, text, new SolidBrush(color), null); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/FfmpegUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using FFmpeg.AutoGen; 7 | 8 | namespace Unai.ExtendedBinaryWaterfall; 9 | 10 | public static class FfmpegUtils 11 | { 12 | private static readonly string[] _ffmpegSearchPathsLinux = 13 | [ 14 | "/usr/lib", 15 | "/usr/lib64", 16 | "/usr/lib32", 17 | "/lib", 18 | "/lib64", 19 | "/lib32" 20 | ]; 21 | 22 | private static readonly string[] _ffmpegSearchPathsWindows = 23 | [ 24 | "C:\\ffmpeg" 25 | ]; 26 | 27 | public static string GetFfmpegLibraryPath() 28 | { 29 | Logger.Debug("Guessing FFmpeg library path…"); 30 | IEnumerable ret; 31 | 32 | if (Environment.OSVersion.Platform == PlatformID.Win32NT) 33 | { 34 | ret = _ffmpegSearchPathsWindows 35 | .Where(Directory.Exists) 36 | .Where(x => Directory.GetFiles(x, "*avcodec-*.dll").Length > 0); 37 | } 38 | else 39 | { 40 | ret = _ffmpegSearchPathsLinux 41 | .Where(Directory.Exists) 42 | .Where(x => File.Exists($"{x}/libavcodec.so")); 43 | } 44 | 45 | Logger.Debug($"{ret.Count()} detected library paths."); 46 | if (ret.Any()) 47 | { 48 | return ret.FirstOrDefault(); 49 | } 50 | 51 | if (Environment.OSVersion.Platform == PlatformID.Win32NT) 52 | { 53 | Logger.Debug("Search via predefined paths failed. Trying PATH environment variable…"); 54 | ret = Environment.GetEnvironmentVariable("PATH") 55 | .Split(';') 56 | .Where(Directory.Exists) 57 | .Where(p => Directory.GetFiles(p, "avcodec*.dll").Length > 0); 58 | 59 | if (ret.Any()) 60 | { 61 | return ret.FirstOrDefault(); 62 | } 63 | } 64 | 65 | Logger.Error("Cannot determine folder path containing FFmpeg libraries."); 66 | if (Environment.OSVersion.Platform == PlatformID.Win32NT) 67 | { 68 | Logger.Info("Please enter the following command to install FFmpeg libraries:"); 69 | Logger.Info(" winget install \"FFmpeg (Shared)\""); 70 | Logger.Info("Once installed, restart the command line."); 71 | Logger.Info("Alternatively, you can download the FFmpeg libraries and save them to the following location:"); 72 | Logger.Info($" {_ffmpegSearchPathsWindows[0]}"); 73 | Logger.Info("Note that these libraries may start with either `libav` or just `av` (e.g: `avcodec-61.dll`)."); 74 | } 75 | return null; 76 | } 77 | 78 | public unsafe static void LogIfAvError(int errorCode, string message) 79 | { 80 | if (errorCode < 0) 81 | { 82 | byte* errbuf = (byte*)ffmpeg.av_malloc(1024); 83 | ffmpeg.av_make_error_string(errbuf, 1024, errorCode); 84 | Logger.Error($"FFmpeg error {errorCode}: {message}: {Marshal.PtrToStringUTF8((nint)errbuf)}"); 85 | ffmpeg.av_free(errbuf); 86 | } 87 | } 88 | 89 | public static AVRational GetRational(int num, int den) 90 | { 91 | AVRational ret; 92 | ret.num = num; 93 | ret.den = den; 94 | return ret; 95 | } 96 | 97 | public unsafe static void LogFrameData(AVFrame* frame) 98 | { 99 | if (frame != null) 100 | { 101 | Logger.Trace($"frm: pts={frame->pts} dur={frame->duration} tb={frame->time_base.num}/{frame->time_base.den}"); 102 | } 103 | } 104 | 105 | public unsafe static void LogPacketData(AVPacket* packet) 106 | { 107 | Logger.Trace($"pkt: str={packet->stream_index} pts={packet->pts} dts={packet->dts} dur={packet->duration} tb={packet->time_base.num}/{packet->time_base.den}"); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Numerics; 7 | using System.Reflection; 8 | using SixLabors.Fonts; 9 | using SixLabors.ImageSharp; 10 | using SixLabors.ImageSharp.Drawing.Processing; 11 | using SixLabors.ImageSharp.PixelFormats; 12 | using SixLabors.ImageSharp.Processing; 13 | using SixLabors.ImageSharp.Processing.Processors.Transforms; 14 | using Unai.ExtendedBinaryWaterfall.Exporters; 15 | using Unai.ExtendedBinaryWaterfall.Parsers; 16 | 17 | namespace Unai.ExtendedBinaryWaterfall; 18 | 19 | public class Generator 20 | { 21 | #region Main Fields 22 | 23 | public FileStream InputFileStream { get; set; } 24 | public Stream InputAuxiliaryFileStream { get; set; } 25 | public IParser Parser { get; set; } 26 | public IExporter Exporter { get; set; } 27 | private readonly Stopwatch _timer = new(); 28 | private List _subfiles = []; 29 | 30 | public Dictionary AdditionalCliArguments { get; } = []; 31 | 32 | #endregion 33 | 34 | #region Events 35 | 36 | public event Action OnFinish; 37 | public event Action OnProgress; 38 | 39 | #endregion 40 | 41 | #region Generator Registers 42 | 43 | private Image _frameContent = null; 44 | private Image _viewportFramebuf = null; 45 | private AudioBuffer _inputAudioBuffer = null; 46 | private AudioBuffer _outputAudioBuffer = null; 47 | private int _videoFrameX1, _videoFrameX2, _videoFrameY1, _videoFrameY2; 48 | internal bool _exitRequested = false; 49 | 50 | #endregion 51 | 52 | #region ImageSharp-specific 53 | 54 | private FontCollection _fontCollection; 55 | private FontFamily _fontFamily, _emojiFontFamily; 56 | private Font _font16, _font24, _font32, _font48; 57 | private readonly DrawingOptions _drawOpts = new() 58 | { 59 | GraphicsOptions = new() 60 | { 61 | Antialias = true, 62 | AntialiasSubpixelDepth = 0, // coalesced value 63 | } 64 | }; 65 | 66 | #endregion 67 | 68 | #region General Parameters 69 | 70 | public string InputFilePath { get; set; } = null; 71 | [CliParameter("Input File Listing File Path", "file-listing", "Set the file path that contains a text-based file listing if the input file format cannot be parsed entirely by this program")] 72 | public string InputAuxiliaryFilePath { get; set; } = null; 73 | [CliParameter("Output File Path", "output", 'o', "Set the output video file path")] 74 | public string OutputFilePath { get; set; } = null; 75 | [CliParameter("Title", "title", 't', "Set the title that will be shown during the binary waterfall describing the target file")] 76 | public string Title { get; set; } = null; 77 | [CliParameter("Author", "author", 'a', "Set the author of the generated binary waterfall")] 78 | public string Author { get; set; } = null; 79 | [CliParameter("Input File Parser", "parser", 'p', "Force a specific parser for the input file")] 80 | public string InputFileFormatId { get; set; } = null; 81 | [CliParameter("Exporter", "exporter", 'e', "Set the exporter to be used to export the generated binary waterfall")] 82 | public string ExporterId { get; set; } = null; 83 | [CliParameter("Input Bytes per Second", "input-bps", "Set the amount of bytes that will be read per audio/video second")] 84 | public int InputBytesPerSecond { get; set; } = 48000 * 2; 85 | [CliParameter("Font Name", "font", "Set the font name to render the on-screen text")] 86 | public string FontName { get; set; } = null; 87 | [CliParameter("Font Antialiasing", "font-antialiasing")] 88 | public bool FontAntialiasing { get => _drawOpts.GraphicsOptions.Antialias; set => _drawOpts.GraphicsOptions.Antialias = value; } 89 | 90 | #endregion 91 | 92 | public int InputBytesPerFrame => InputBytesPerSecond / OutputFps; 93 | 94 | #region Video Parameters 95 | 96 | [CliParameter("Output Video Width", "output-width")] 97 | public int OutputVideoWidth { get; set; } = 1920; 98 | [CliParameter("Output Video Height", "output-height")] 99 | public int OutputVideoHeight { get; set; } = 1080; 100 | [CliParameter("Output Framerate", "output-fps")] 101 | public int OutputFps { get; set; } = 60; 102 | public int WaterfallScaledWidth { get; set; } = 768; 103 | public int WaterfallScaledHeight { get; set; } = 768; 104 | [CliParameter("Input Video Width", "input-width")] 105 | public int WaterfallWidth { get; set; } = 256; 106 | [CliParameter("Input Video Height", "input-height")] 107 | public int WaterfallHeight { get; set; } = 256; 108 | public int WaterfallFrameLength => WaterfallWidth * WaterfallHeight * 4; 109 | 110 | #endregion 111 | 112 | #region Audio Parameters 113 | 114 | [CliParameter("Input Sample Format", "sample-format")] 115 | public AudioSampleFormat AudioInputSampleFormat { get; set; } = AudioSampleFormat.Unsigned8; 116 | [CliParameter("Input Audio Channel Count", "channel-count")] 117 | public int AudioInputChannelCount { get; set; } = 2; 118 | public int AudioInputSamplesPerFrame => InputBytesPerFrame / AudioInputSampleFormat.GetByteSize(); 119 | public int AudioInputSamplesPerFramePerChannel => AudioInputSamplesPerFrame / AudioInputChannelCount; 120 | public int AudioInputSampleRate => (InputBytesPerSecond / AudioInputSampleFormat.GetByteSize()) / AudioInputChannelCount; 121 | public int AudioInputBytesPerFrame => InputBytesPerFrame; 122 | 123 | [CliParameter("Output Sample Format", "output-sample-format")] 124 | public AudioSampleFormat AudioOutputSampleFormat { get; set; } = AudioSampleFormat.Float32; 125 | [CliParameter("Output Audio Channel Count", "output-channel-count")] 126 | public int AudioOutputChannelCount { get; set; } = 2; 127 | [CliParameter("Output Sample Rate", "output-sample-rate")] 128 | public int AudioOutputSampleRate { get; set; } = 48000; 129 | public int AudioOutputSamplesPerFramePerChannel => AudioOutputSampleRate / OutputFps; 130 | public int AudioOutputSamplesPerFrame => AudioOutputSamplesPerFramePerChannel * AudioOutputChannelCount; 131 | public int AudioOutputBytesPerFrame => AudioOutputSampleFormat.GetByteSize() * AudioOutputSamplesPerFrame; 132 | 133 | #endregion 134 | 135 | #region Debug Flags 136 | 137 | public bool LogAllSubfiles { get; set; } = false; 138 | 139 | #endregion 140 | 141 | #region Initialization Methods 142 | 143 | public void Initialize() 144 | { 145 | if (InputFileStream == null) 146 | { 147 | Logger.Info("Opening files…"); 148 | InputFileStream = File.OpenRead(InputFilePath); 149 | } 150 | 151 | if (InputAuxiliaryFileStream != null) 152 | { 153 | if (InputAuxiliaryFilePath != null) 154 | { 155 | InputAuxiliaryFileStream = File.OpenRead(InputAuxiliaryFilePath); 156 | } 157 | } 158 | 159 | InitializeParser(); 160 | 161 | ParseSubfiles(); 162 | 163 | InitializeExporter(); 164 | 165 | InitializeFonts(); 166 | 167 | Logger.Info("Preparing audio/video generation…"); 168 | 169 | UpdateValues(); 170 | 171 | _inputAudioBuffer = new(AudioInputSamplesPerFramePerChannel, AudioInputChannelCount); 172 | _outputAudioBuffer = new(AudioOutputSamplesPerFramePerChannel, AudioOutputChannelCount); 173 | if (_drawOpts.GraphicsOptions.Antialias) 174 | { 175 | if (_drawOpts.GraphicsOptions.AntialiasSubpixelDepth < 0) 176 | { 177 | _drawOpts.GraphicsOptions.AntialiasSubpixelDepth = 1; 178 | } 179 | } 180 | 181 | LogGeneratorStatus(); 182 | } 183 | 184 | [Conditional("DEBUG")] 185 | private void LogGeneratorStatus() 186 | { 187 | Logger.Debug($"Selected parser: {Parser?.GetType().GetCustomAttribute()?.Name ?? ""}"); 188 | Logger.Debug($"Selected exporter: {Exporter?.GetType().GetCustomAttribute()?.Name ?? ""}"); 189 | Logger.Debug($"Selected font: {_fontFamily.Name ?? ""}"); 190 | Logger.Debug($"Read speed: {InputBytesPerFrame} bytes/frame ({InputBytesPerSecond} bytes/second)"); 191 | Logger.Debug($"Waterfall duration will be around {TimeSpan.FromSeconds(InputFileStream.Length / InputBytesPerSecond)}."); 192 | Logger.Debug($"Video input: {WaterfallWidth}×{WaterfallHeight}"); 193 | Logger.Debug($"Audio input: {AudioInputBytesPerFrame}bpf {AudioInputSamplesPerFrame}spf → {AudioInputSampleRate}Hz {AudioInputChannelCount}ch {8 * AudioInputSampleFormat.GetByteSize()}-bit"); 194 | Logger.Debug($"Audio output: {AudioOutputBytesPerFrame}bpf {AudioOutputSamplesPerFrame}spf → {AudioOutputSampleRate}Hz {AudioOutputChannelCount}ch {8 * AudioOutputSampleFormat.GetByteSize()}-bit"); 195 | } 196 | 197 | private void InitializeFonts() 198 | { 199 | Logger.Info("Loading fonts…"); 200 | Logger.Debug($"Requested font: '{FontName}'."); 201 | 202 | if (_fontCollection == null) 203 | { 204 | _fontCollection = new(); 205 | _fontCollection.AddSystemFonts(); 206 | } 207 | 208 | if (FontName != null) 209 | { 210 | // Try getting the font by the font name specified by the user 211 | if (!_fontCollection.TryGet(FontName, out _fontFamily)) 212 | { 213 | Logger.Error($"Cannot find font '{FontName}'."); 214 | } 215 | } 216 | 217 | if (_fontFamily.Name == null) 218 | { 219 | if (_fontCollection.TryGet("unifont", out _fontFamily)) 220 | { 221 | _fontCollection.TryGet("unifont upper", out _emojiFontFamily); 222 | } 223 | else 224 | { 225 | _fontFamily = _fontCollection.Get(Environment.OSVersion.Platform == PlatformID.Win32NT ? "Consolas" : "Source Code Pro"); 226 | } 227 | } 228 | 229 | _font48 = _fontFamily.CreateFont(48f, FontStyle.Regular); 230 | _font32 = _fontFamily.CreateFont(32f, FontStyle.Regular); 231 | _font24 = _fontFamily.CreateFont(24f, FontStyle.Regular); 232 | _font16 = _fontFamily.CreateFont(16f, FontStyle.Regular); 233 | } 234 | 235 | private void InitializeExporter() 236 | { 237 | Logger.Info("Setting up exporter…"); 238 | Logger.Debug($"Requested exporter: '{ExporterId}'."); 239 | 240 | if (ExporterId != null) 241 | { 242 | var availableExporters = Utils.GetTypesWithAttribute(); 243 | foreach (var exporterKvp in availableExporters) 244 | { 245 | var exporterAttr = exporterKvp.Key; 246 | if (exporterAttr.Id != ExporterId) 247 | { 248 | continue; 249 | } 250 | Exporter = (IExporter)Activator.CreateInstance(exporterKvp.Value); 251 | } 252 | if (Exporter == null) 253 | { 254 | Logger.Fail($"Unknown exporter ID: '{ExporterId}'."); 255 | return; 256 | } 257 | } 258 | else 259 | { 260 | Logger.Debug("No exporter requested. Using SDL…"); 261 | Exporter = new SdlExporter(); 262 | } 263 | 264 | Exporter.Generator = this; 265 | 266 | if (AdditionalCliArguments.Count > 0) 267 | { 268 | Logger.Info($"Setting exporter properties from command line arguments…"); 269 | foreach (var argKvp in AdditionalCliArguments) 270 | { 271 | var targetProp = Utils.GetPropertyFromCliArgument(argKvp.Key); 272 | 273 | if (targetProp.DeclaringType.GetInterfaces().Contains(typeof(IExporter))) 274 | { 275 | CliParameterAttribute.SetPropertyFromCliArgument(targetProp, Exporter, argKvp.Value); 276 | } 277 | else 278 | { 279 | Logger.Error($"Unrecognized CLI argument name: '{argKvp.Key}'."); 280 | } 281 | } 282 | } 283 | } 284 | 285 | private void ParseSubfiles() 286 | { 287 | IEnumerable subFiles = null; 288 | 289 | if (Parser != null) 290 | { 291 | Logger.Info("Parsing subfiles…"); 292 | 293 | Parser.InputStream = InputFileStream; 294 | Parser.AuxiliaryInputStream = InputAuxiliaryFileStream; 295 | subFiles = Parser.GetSubFiles(); 296 | 297 | _subfiles = 298 | [ 299 | .. subFiles 300 | .OrderBy(sf => sf.StartOffset) 301 | .Select(sf => Utils.ParseSubfile(InputFileStream, sf)) 302 | ]; 303 | } 304 | 305 | Logger.Debug($"Total number of subfiles: {_subfiles.Count}"); 306 | 307 | if (LogAllSubfiles) 308 | { 309 | foreach (var sf in _subfiles) 310 | { 311 | Logger.Debug($"\t{sf.IconString ?? "–"} '{sf.Path}' {sf.StartOffset:X8}–{sf.EndOffset:X8}"); 312 | } 313 | } 314 | } 315 | 316 | private void InitializeParser() 317 | { 318 | Logger.Info("Setting up parser…"); 319 | var availableParsers = Utils.GetTypesWithAttribute(); 320 | 321 | if (InputFileFormatId != null) 322 | { 323 | Logger.Debug($"Requested parser: '{InputFileFormatId}'."); 324 | foreach (var parserKvp in availableParsers) 325 | { 326 | var parserAttr = parserKvp.Key; 327 | if (parserAttr.Id != InputFileFormatId) 328 | { 329 | continue; 330 | } 331 | Parser = (IParser)Activator.CreateInstance(parserKvp.Value); 332 | } 333 | if (Parser == null) 334 | { 335 | Logger.Warning($"Unknown parser ID: '{InputFileFormatId}'. Skipping subfile listing."); 336 | } 337 | } 338 | else 339 | { 340 | Logger.Info("Guessing input format from file extension…"); 341 | var inputFileExt = Path.GetExtension(InputFilePath).ToLower(); 342 | 343 | foreach (var parserKvp in availableParsers) 344 | { 345 | var parserAttr = parserKvp.Key; 346 | if (parserAttr.FileExtensions.Contains(inputFileExt)) 347 | { 348 | Logger.Debug($"Parser '{parserAttr.Id}' recognizes '{inputFileExt}' as a valid file extension."); 349 | Parser = (IParser)Activator.CreateInstance(parserKvp.Value); 350 | break; 351 | } 352 | } 353 | if (Parser == null) 354 | { 355 | Logger.Warning($"Unknown input format. Skipping subfile listing."); 356 | } 357 | } 358 | } 359 | 360 | #endregion 361 | 362 | internal void UpdateValues() 363 | { 364 | if (_frameContent == null || _frameContent.Width != OutputVideoWidth || _frameContent.Height != OutputVideoHeight) 365 | { 366 | _frameContent = new(OutputVideoWidth, OutputVideoHeight); 367 | var pixelCount = OutputVideoWidth * OutputVideoHeight; 368 | 369 | WaterfallScaledWidth = (int)(WaterfallWidth * (pixelCount / 691200f)); 370 | WaterfallScaledHeight = (int)(WaterfallHeight * (pixelCount / 691200f)); 371 | 372 | _videoFrameX1 = OutputVideoWidth / (_subfiles.Count > 0 ? 4 : 2) - WaterfallScaledWidth / 2; 373 | if (_videoFrameX2 == 0) _videoFrameX2 = _videoFrameX1 + WaterfallScaledWidth; 374 | _videoFrameY1 = OutputVideoHeight / 2 - WaterfallScaledHeight / 2; 375 | if (_videoFrameY2 == 0) _videoFrameY2 = _videoFrameY1 + WaterfallScaledHeight; 376 | } 377 | } 378 | 379 | public void Generate() 380 | { 381 | _timer.Start(); 382 | 383 | // 1. Intro 384 | 385 | GenerateIntro(); 386 | if (_exitRequested) 387 | { 388 | OnFinish?.Invoke(); 389 | return; 390 | } 391 | 392 | // 2. Main Video 393 | 394 | GenerateMainVideo(); 395 | 396 | OnFinish?.Invoke(); 397 | } 398 | 399 | private void GenerateIntro() 400 | { 401 | Logger.Info("Generating introduction…"); 402 | 403 | var totalFrames = 5 * OutputFps; // 60FPS = 300 404 | 405 | for (long frameNumber = 0; frameNumber < totalFrames; frameNumber++) 406 | { 407 | _frameContent.Mutate(ctx => ctx.Clear(new Rgba32(16, 16, 16, 255))); 408 | 409 | _frameContent.Mutate(av => av 410 | .DrawText(new RichTextOptions(_font48) 411 | { 412 | Origin = new Vector2(OutputVideoWidth / 2, OutputVideoHeight / 2), 413 | HorizontalAlignment = HorizontalAlignment.Center, 414 | TextAlignment = TextAlignment.Center, 415 | }, "DISCLAIMER\n\nThis video contains\nhigh speed flashing lights\nand loud noises", Color.White) 416 | .DrawText(new RichTextOptions(_font24) 417 | { 418 | Origin = new Vector2(OutputVideoWidth / 2, OutputVideoHeight - 128), 419 | HorizontalAlignment = HorizontalAlignment.Center, 420 | }, $"Starting in {(totalFrames - frameNumber) / (float)OutputFps:N1} seconds…", Color.White) 421 | .DrawProgressBar(frameNumber / (float)totalFrames, (int)(OutputVideoWidth * 0.3), (int)(OutputVideoWidth * 0.7), OutputVideoHeight - 64)); 422 | 423 | Exporter.PushNewFrame(_frameContent, _outputAudioBuffer, _timer.Elapsed.TotalSeconds); 424 | _timer.Restart(); 425 | 426 | OnProgress?.Invoke(frameNumber / (float)totalFrames); 427 | 428 | if (_exitRequested) 429 | { 430 | break; 431 | } 432 | } 433 | } 434 | 435 | private void GenerateMainVideo() 436 | { 437 | Logger.Info("Generating binary waterfall…"); 438 | 439 | string avSettingsString = $"{AudioInputSampleRate} Hz, PCM {(AudioInputSampleFormat.IsSigned() ? "signed" : "unsigned")} {8 * AudioInputSampleFormat.GetByteSize()}-bit, {(AudioInputChannelCount == 2 ? "stereo" : "mono")}\nRGBA (32bpp), {WaterfallWidth} px/line"; 440 | string readSpeedString = $"{InputBytesPerSecond / 1024} KiB/s"; 441 | 442 | float subfileWindowIndex = 0f; 443 | long currentOffset = 0; 444 | int playHeadRelPos = 0; 445 | 446 | using var targetFileReader = new BinaryReader(InputFileStream); 447 | 448 | while (currentOffset < InputFileStream.Length) 449 | { 450 | // Get video buffer. 451 | 452 | playHeadRelPos = 0; 453 | var frameStartByteOffset = currentOffset.Align(WaterfallWidth * 4) - (WaterfallFrameLength / 2); 454 | if (frameStartByteOffset < 0) 455 | { 456 | playHeadRelPos = (int)-(frameStartByteOffset / (WaterfallWidth * 4)); 457 | frameStartByteOffset = 0; 458 | } 459 | else if (frameStartByteOffset + WaterfallFrameLength >= InputFileStream.Length) 460 | { 461 | playHeadRelPos = (int)((InputFileStream.Length - (frameStartByteOffset + WaterfallFrameLength)) / (WaterfallWidth * 4)); 462 | frameStartByteOffset = InputFileStream.Length - WaterfallFrameLength; 463 | } 464 | var frameEndByteOffset = frameStartByteOffset + WaterfallFrameLength; 465 | 466 | InputFileStream.Position = frameStartByteOffset; 467 | var currentVideoBuffer = targetFileReader.ReadBytes(WaterfallFrameLength); 468 | 469 | // Get audio buffer. 470 | 471 | var audioFrameStartByteOffset = currentOffset.Align(AudioInputSampleFormat.GetByteSize()) - (InputBytesPerFrame / 2); 472 | if (audioFrameStartByteOffset < 0) 473 | { 474 | audioFrameStartByteOffset = 0; 475 | } 476 | else if (audioFrameStartByteOffset + InputBytesPerFrame >= InputFileStream.Length) 477 | { 478 | audioFrameStartByteOffset = InputFileStream.Length - InputBytesPerFrame; 479 | } 480 | var audioFrameEndByteOffset = audioFrameStartByteOffset + InputBytesPerFrame; 481 | 482 | InputFileStream.Position = audioFrameStartByteOffset; 483 | var currentAudioBuffer = targetFileReader.ReadBytes(InputBytesPerFrame); 484 | 485 | // Get video data. 486 | 487 | _viewportFramebuf = Image.LoadPixelData(currentVideoBuffer, WaterfallWidth, WaterfallHeight); 488 | _viewportFramebuf.ProcessPixelRows(pa => 489 | { 490 | for (int y = 0; y < pa.Height; y++) 491 | { 492 | var row = pa.GetRowSpan(y); 493 | for (int x = 0; x < row.Length; x++) 494 | { 495 | row[x].A = 255; 496 | } 497 | } 498 | }); 499 | _viewportFramebuf.Mutate(ctx => ctx.Flip(FlipMode.Vertical).Resize(WaterfallScaledWidth, WaterfallScaledHeight, new NearestNeighborResampler())); 500 | 501 | // Get audio data. 502 | 503 | _inputAudioBuffer.LoadFromByteArray(currentAudioBuffer, AudioInputSampleFormat); 504 | _outputAudioBuffer = new AudioBuffer(_inputAudioBuffer) 505 | .Resample(AudioOutputSamplesPerFramePerChannel) 506 | .RemixChannels(AudioOutputChannelCount); 507 | 508 | // Compute registers. 509 | 510 | var subfilesInFrame = _subfiles 511 | .Select((sf, i) => new { key = i, value = sf }) 512 | .Where(kvp => kvp.value.Intersects(currentOffset - (InputBytesPerFrame / 2), currentOffset + (InputBytesPerFrame / 2))) 513 | .ToList(); 514 | var currentSubfile = subfilesInFrame.LastOrDefault(); 515 | 516 | if (currentSubfile != null) 517 | { 518 | subfileWindowIndex = .2f * subfileWindowIndex + .8f * currentSubfile.key; 519 | } 520 | 521 | // Do render. 522 | 523 | _frameContent.Mutate(ctx => 524 | { 525 | // 1. Clear frame 526 | 527 | ctx.Clear(new Rgba32(16, 16, 16, 255)); 528 | 529 | // 2. Draw subfile listing 530 | 531 | int subfileX1 = OutputVideoWidth / 2; 532 | int subfileX2 = OutputVideoWidth - 32; 533 | 534 | int firstSubfileIndex = (int)(subfileWindowIndex - 7); 535 | int lastSubfileIndex = (int)Math.Ceiling(subfileWindowIndex + 7); 536 | 537 | float subfileH = 48; 538 | float subfileY = (OutputVideoHeight / 2) - (subfileWindowIndex - firstSubfileIndex) * subfileH; 539 | 540 | for (int sfi = firstSubfileIndex; sfi <= lastSubfileIndex; sfi++) 541 | { 542 | int i = sfi - (currentSubfile?.key ?? 0); 543 | 544 | if (sfi < 0 || sfi >= _subfiles.Count) 545 | { 546 | subfileY += subfileH; 547 | continue; 548 | } 549 | 550 | var subfile = _subfiles[sfi]; 551 | 552 | bool isMainSubfile = sfi == (currentSubfile?.key ?? -1); 553 | 554 | ctx.DrawText(_drawOpts, new RichTextOptions(_font32) 555 | { 556 | Origin = new Vector2(subfileX1, subfileY), 557 | VerticalAlignment = VerticalAlignment.Center, 558 | }, isMainSubfile ? "▶" : " ", new SolidBrush(Color.White), null) 559 | .DrawTextAndCache(_drawOpts, new RichTextOptions(_font32) 560 | { 561 | Origin = new Vector2(subfileX1 + 32, subfileY), 562 | VerticalAlignment = VerticalAlignment.Center, 563 | FallbackFontFamilies = _emojiFontFamily.Name != null ? [_emojiFontFamily] : null, 564 | }, $"{Utils.GetFileTypeEmoji(subfile)} {Utils.TruncateString(subfile.FileName, 40)}", new SolidBrush(Color.White), null) 565 | .DrawTextAndCache(_drawOpts, new RichTextOptions(_font32) 566 | { 567 | Origin = new Vector2(subfileX2, subfileY), 568 | HorizontalAlignment = HorizontalAlignment.Right, 569 | VerticalAlignment = VerticalAlignment.Center, 570 | }, Utils.ToByteSizeString(subfile.Length), new SolidBrush(Color.DimGray), null); 571 | 572 | if (isMainSubfile) 573 | { 574 | float percentOfSubfile = (currentOffset - subfile.StartOffset) / (float)subfile.Length; 575 | 576 | ctx.DrawText(_drawOpts, new RichTextOptions(_font16) 577 | { 578 | Origin = new PointF(subfileX1 + 48, subfileY + 20), 579 | HorizontalAlignment = HorizontalAlignment.Center, 580 | VerticalAlignment = VerticalAlignment.Center, 581 | }, $"{(int)Math.Clamp(percentOfSubfile * 100, 0, 100)} %", new SolidBrush(Color.White), null) 582 | .DrawProgressBar(percentOfSubfile, subfileX1 + 80, subfileX2, subfileY + 20); 583 | } 584 | 585 | subfileY += subfileH; 586 | } 587 | 588 | // 3. Draw binary waterfall viewport 589 | 590 | ctx.DrawImage(_viewportFramebuf, new Point(_videoFrameX1, _videoFrameY1), 1f) 591 | .DrawText(new RichTextOptions(_font32) 592 | { 593 | Origin = new Vector2(32, (OutputVideoHeight / 2) + (playHeadRelPos * (WaterfallScaledHeight / WaterfallHeight))), 594 | VerticalAlignment = VerticalAlignment.Center, 595 | }, "▶", Color.White); 596 | 597 | // 4. Draw top-bottom gradients 598 | 599 | float shadowY1 = (OutputVideoHeight / 2) - subfileH * 8.5f; 600 | float shadowY2 = (OutputVideoHeight / 2) + subfileH * 6.5f; 601 | 602 | ctx.Fill( 603 | new LinearGradientBrush( 604 | new PointF(0, shadowY1), 605 | new PointF(0, shadowY1 + subfileH * 2), 606 | GradientRepetitionMode.None, 607 | new(0.5f, Color.FromRgba(16, 16, 16, 255)), 608 | new(1, Color.FromRgba(16, 16, 16, 0)) 609 | ), 610 | new RectangleF(0, shadowY1, OutputVideoWidth, subfileH * 2) 611 | ) 612 | .Fill( 613 | new LinearGradientBrush( 614 | new PointF(0, shadowY2), 615 | new PointF(0, shadowY2 + subfileH * 2), 616 | GradientRepetitionMode.None, 617 | new(0, Color.FromRgba(16, 16, 16, 0)), 618 | new(0.5f, Color.FromRgba(16, 16, 16, 255)) 619 | ), 620 | new RectangleF(0, shadowY2, OutputVideoWidth, subfileH * 2) 621 | ) 622 | .DrawTextAndCache(new RichTextOptions(_font24) 623 | { 624 | Origin = new Vector2(subfileX1 + 40, 160), 625 | VerticalAlignment = VerticalAlignment.Center, 626 | }, Utils.TruncateString(currentSubfile?.value?.FileDirectory ?? string.Empty, 72), Color.DimGray); 627 | 628 | // 5. Draw Status and General Info 629 | 630 | ctx.DrawTextAndCache(new RichTextOptions(_font24) 631 | { 632 | Origin = new Vector2(32, 32), 633 | }, "A/V SETTINGS", Color.DimGray) 634 | .DrawText(new(_font32) 635 | { 636 | Origin = new Vector2(32, 32 + 24), 637 | }, avSettingsString, Color.White) 638 | .DrawTextAndCache(new RichTextOptions(_font24) 639 | { 640 | Origin = new Vector2(OutputVideoWidth - 32, 32), 641 | HorizontalAlignment = HorizontalAlignment.Right, 642 | }, "ABS. OFFSET", Color.DimGray) 643 | .DrawText(new(_font32) 644 | { 645 | Origin = new Vector2(OutputVideoWidth - 32, 32 + 24), 646 | HorizontalAlignment = HorizontalAlignment.Right, 647 | TextAlignment = TextAlignment.End, 648 | }, $"{currentOffset / 1048576f:N2} MiB\n0x{currentOffset:X8}", Color.White) 649 | .DrawTextAndCache(new RichTextOptions(_font24) 650 | { 651 | Origin = new Vector2(OutputVideoWidth - 256, 32), 652 | HorizontalAlignment = HorizontalAlignment.Right, 653 | }, "BITRATE", Color.DimGray) 654 | .DrawText(new(_font32) 655 | { 656 | Origin = new Vector2(OutputVideoWidth - 256, 32 + 24), 657 | HorizontalAlignment = HorizontalAlignment.Right, 658 | }, readSpeedString, Color.White); 659 | 660 | if (Author != null) 661 | { 662 | ctx.DrawTextAndCache(new(_font32) 663 | { 664 | Origin = new Vector2(OutputVideoWidth / 2, 32 + 24), 665 | VerticalAlignment = VerticalAlignment.Center, 666 | HorizontalAlignment = HorizontalAlignment.Center, 667 | }, Author, Color.White); 668 | } 669 | 670 | if (Title != null) 671 | { 672 | ctx.DrawTextAndCache(new RichTextOptions(_font24) 673 | { 674 | Origin = new Vector2(32, OutputVideoHeight - 64 - (Title.Contains('\n') ? 32 : 0)), 675 | VerticalAlignment = VerticalAlignment.Bottom, 676 | }, "TARGET", Color.DimGray) 677 | .DrawTextAndCache(new(_font32) 678 | { 679 | Origin = new Vector2(32, OutputVideoHeight - 32), 680 | VerticalAlignment = VerticalAlignment.Bottom, 681 | }, Title, Color.White); 682 | } 683 | 684 | if (currentSubfile?.value?.Icon != null) 685 | { 686 | ctx.DrawImage(currentSubfile.value.Icon, new Point(OutputVideoWidth / 2, OutputVideoHeight - 128 - 32), 1f); 687 | } 688 | 689 | if (currentSubfile?.value?.Description != null) 690 | { 691 | ctx.DrawText(new(_font32) 692 | { 693 | Origin = new Vector2(OutputVideoWidth / 2 + 128 + 32, OutputVideoHeight - 32), 694 | VerticalAlignment = VerticalAlignment.Bottom, 695 | }, currentSubfile.value.Description, Color.White); 696 | } 697 | }); 698 | 699 | Exporter.PushNewFrame(_frameContent, _outputAudioBuffer, _timer.Elapsed.TotalSeconds); 700 | _timer.Restart(); 701 | 702 | currentOffset += InputBytesPerFrame; 703 | 704 | OnProgress?.Invoke(currentOffset / (float)InputFileStream.Length); 705 | 706 | if (_exitRequested) 707 | { 708 | break; 709 | } 710 | } 711 | 712 | Exporter.Finish(); 713 | } 714 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace Unai.ExtendedBinaryWaterfall; 5 | 6 | public static class Logger 7 | { 8 | public enum LogLevel 9 | { 10 | Fail, Error, Warning, Info, Debug, Trace 11 | } 12 | 13 | public static bool UseColor { get; set; } = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NOCOLOR")); 14 | 15 | private static void Print(string message, LogLevel logLevel, StackFrame sf) 16 | { 17 | var callingMethod = sf?.GetMethod(); 18 | var source = callingMethod != null ? $"{callingMethod.DeclaringType?.Name} {callingMethod.Name}" : "?"; 19 | var logLevelAnsiColor = logLevel switch 20 | { 21 | LogLevel.Fail => "\x1b[31m", 22 | LogLevel.Error => "\x1b[91m", 23 | LogLevel.Warning => "\x1b[93m", 24 | LogLevel.Debug => "\x1b[92m", 25 | LogLevel.Trace => "\x1b[32m", 26 | _ => "\x1b[0m", 27 | }; 28 | Console.Error.WriteLine(UseColor ? $"\x1b[90m{source} {logLevelAnsiColor}{message}\x1b[0m" : $"{source} {message}"); 29 | } 30 | 31 | public static void Fail(string message) 32 | { 33 | Print(message, LogLevel.Fail, new StackFrame(1)); 34 | } 35 | 36 | public static void Error(string message) 37 | { 38 | Print(message, LogLevel.Error, new StackFrame(1)); 39 | } 40 | 41 | public static void Warning(string message) 42 | { 43 | Print(message, LogLevel.Warning, new StackFrame(1)); 44 | } 45 | 46 | public static void Info(string message) 47 | { 48 | Print(message, LogLevel.Info, new StackFrame(1)); 49 | } 50 | 51 | [Conditional("DEBUG")] 52 | public static void Debug(string message) 53 | { 54 | Print(message, LogLevel.Debug, new StackFrame(1)); 55 | } 56 | 57 | [Conditional("TRACE")] 58 | public static void Trace(string message) 59 | { 60 | Print(message, LogLevel.Trace, new StackFrame(1)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Custom/CustomParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Custom; 6 | 7 | [Parser("custom", "Unknown Format, Custom File Listing", [])] 8 | public class CustomParser : IParser 9 | { 10 | public Stream InputStream { get; set; } 11 | public Stream AuxiliaryInputStream { get; set; } 12 | 13 | public IEnumerable GetSubFiles() 14 | { 15 | if (AuxiliaryInputStream == null) yield break; 16 | 17 | using var sr = new StreamReader(AuxiliaryInputStream); 18 | var csvValues = sr.ReadToEnd() 19 | .Split('\n') 20 | .Select(line => line.Split(',')); 21 | 22 | foreach (var row in csvValues.Skip(1)) 23 | { 24 | yield return new(row[3], long.Parse(row[0]), long.Parse(row[1])); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Elf/ElfParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Elf; 6 | 7 | [Parser("elf", "Executable and Linkable Format (ELF)", [ ".elf", ".out", ".o", ".so", ".ko", ".mod", ".prx" ])] 8 | public class ElfParser : IParser 9 | { 10 | public Stream InputStream { get; set; } 11 | public Stream AuxiliaryInputStream { get; set; } 12 | 13 | private List _sectionHeaders = new(); 14 | 15 | public IEnumerable GetSubFiles() 16 | { 17 | using BinaryReader br = new(InputStream, Encoding.ASCII, true); 18 | 19 | var elfMagic = br.ReadBytes(4); // Funny var name :) 20 | var elfClass = br.ReadByte(); 21 | var is64Bit = elfClass == 2; 22 | var elfEndianness = br.ReadByte(); // 2 = big-endian 23 | if (elfEndianness == 2) 24 | { 25 | Logger.Error($"Cannot parse big-endian ELF file: not supported yet."); 26 | yield break; 27 | } 28 | var elfVersion = br.ReadByte(); // always 1 29 | var elfAbi = br.ReadByte(); 30 | var elfAbiVersion = br.ReadByte(); 31 | br.BaseStream.Position += 7; // zero-filled padding 32 | 33 | var elfObjType = br.ReadUInt16(); 34 | var elfIsa = br.ReadUInt16(); 35 | var elfVersion32 = br.ReadUInt32(); // always 1 36 | var elfEntryPointAddr = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 37 | var elfPhtOfs = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 38 | var elfShtOfs = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 39 | var elfFlags = br.ReadUInt32(); 40 | var elfHdrSize = br.ReadUInt16(); 41 | var elfPhtEntrySize = br.ReadUInt16(); 42 | var elfPhtEntryCount = br.ReadUInt16(); 43 | var elfShtEntrySize = br.ReadUInt16(); 44 | var elfShtEntryCount = br.ReadUInt16(); 45 | var elfShtStringTableIndex = br.ReadUInt16(); 46 | 47 | yield return new("ELF Header", 0, br.BaseStream.Position) { IconString = "🔶" }; 48 | 49 | for (int phIdx = 0; phIdx < elfPhtEntryCount; phIdx++) 50 | { 51 | br.BaseStream.Position = (long)elfPhtOfs + (phIdx * elfPhtEntrySize); 52 | var phOfs = br.BaseStream.Position; 53 | 54 | var phType = br.ReadUInt32(); 55 | var phFlags = 0u; if (is64Bit) phFlags = br.ReadUInt32(); 56 | var phSegOfs = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 57 | var phSegVa = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 58 | var phSegPa = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 59 | var phSegSize = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 60 | var phSegSizeInMem = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 61 | if (!is64Bit) phFlags = br.ReadUInt32(); 62 | var phAlign = is64Bit ? br.ReadUInt64() : br.ReadUInt32(); 63 | 64 | yield return new($"Program Header {phIdx}", phOfs, elfPhtEntrySize) { IconString = "🔶" }; 65 | } 66 | 67 | for (int shIdx = 0; shIdx < elfShtEntryCount; shIdx++) 68 | { 69 | br.BaseStream.Position = (long)elfShtOfs + (long)(shIdx * elfShtEntrySize); 70 | var shOfs = br.BaseStream.Position; 71 | 72 | _sectionHeaders.Add(new() 73 | { 74 | Offset = shOfs, 75 | NameStringOffset = br.ReadUInt32(), 76 | Type = (ElfSectionType)br.ReadUInt32(), 77 | Flags = is64Bit ? br.ReadUInt64() : br.ReadUInt32(), 78 | SectionVirtualAddress = is64Bit ? br.ReadUInt64() : br.ReadUInt32(), 79 | SectionOffset = is64Bit ? br.ReadUInt64() : br.ReadUInt32(), 80 | SectionSize = is64Bit ? br.ReadUInt64() : br.ReadUInt32(), 81 | LinkSectionIndex = br.ReadUInt32(), 82 | InfoSectionIndex = br.ReadUInt32(), 83 | AddressAlign = is64Bit ? br.ReadUInt64() : br.ReadUInt32(), 84 | EntrySize = is64Bit ? br.ReadUInt64() : br.ReadUInt32(), 85 | }); 86 | } 87 | 88 | var stringTableSecHdr = _sectionHeaders[elfShtStringTableIndex]; 89 | 90 | foreach (var sh in _sectionHeaders) 91 | { 92 | if (sh.Type == ElfSectionType.Null) continue; 93 | 94 | var name = sh.GetName(br, (long)stringTableSecHdr.SectionOffset) ?? $"Section {sh.Type}"; 95 | 96 | yield return new($"Section Header {name}", sh.Offset, elfShtEntrySize) { IconString = "🔶" }; 97 | yield return new(name, (long)sh.SectionOffset, (long)sh.SectionSize); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Elf/ElfSectionHeader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Elf; 4 | 5 | public class ElfSectionHeader 6 | { 7 | public long Offset; 8 | 9 | public uint NameStringOffset; 10 | public ElfSectionType Type; 11 | public ulong Flags; 12 | public ulong SectionVirtualAddress; 13 | public ulong SectionOffset; 14 | public ulong SectionSize; 15 | public uint LinkSectionIndex; 16 | public uint InfoSectionIndex; 17 | public ulong AddressAlign; 18 | public ulong EntrySize; 19 | 20 | public string GetName(BinaryReader br, long stringTableSecOfs) 21 | { 22 | if (NameStringOffset == 0) return null; 23 | 24 | var origOfs = br.BaseStream.Position; 25 | 26 | br.BaseStream.Position = stringTableSecOfs + NameStringOffset; 27 | 28 | var ret = br.ReadCString(); 29 | 30 | br.BaseStream.Position = origOfs; 31 | 32 | return ret; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Elf/ElfSectionType.cs: -------------------------------------------------------------------------------- 1 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Elf; 2 | 3 | public enum ElfSectionType 4 | { 5 | Null, 6 | ProgramData, 7 | SymbolTable, 8 | StringTable, 9 | RelocationsWithAddends, 10 | SymbolHashTable, 11 | DynamicLinkingInfo, 12 | Notes, 13 | UninitializedData, 14 | Relocations, 15 | Reserved1, 16 | DynamicLinkerSymbolTable, 17 | InitArray = 0x0e, 18 | FiniArray = 0x0f, 19 | PreInitArray = 0x10, 20 | SectionGroup, 21 | ExtendedSectionIndices, 22 | } 23 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/GameMaker/GameMakerChunk.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.GameMaker; 6 | 7 | public class GameMakerChunk 8 | { 9 | public string Name { get; set; } = null; 10 | public long Offset { get; internal set; } 11 | public uint Length { get; internal set; } 12 | public long PayloadOffset => Offset + 8; 13 | public virtual IEnumerable Subchunks { get; } = null; 14 | 15 | public static GameMakerChunk CreateFromBinaryReader(BinaryReader br) 16 | { 17 | var chunkOfs = br.BaseStream.Position; 18 | 19 | var chunkId = br.ReadString(4); 20 | uint chunkLen = br.ReadUInt32(); 21 | 22 | Logger.Trace($"[{chunkOfs:X8}] chunk {chunkId} size {chunkLen}"); 23 | 24 | // TODO: attributes can be used here. 25 | string chunkDotNetTypeName = typeof(GameMakerChunk).FullName + chunkId.ToUpper()[0] + chunkId.ToLower()[1..]; 26 | GameMakerChunk chunk = (GameMakerChunk)Activator.CreateInstance(Type.GetType(chunkDotNetTypeName) ?? typeof(GameMakerChunk)); 27 | chunk.Name = chunkId; 28 | chunk.Length = chunkLen; 29 | chunk.Offset = chunkOfs; 30 | 31 | br.BaseStream.Position += chunkLen; 32 | 33 | return chunk; 34 | } 35 | 36 | public virtual void ParseChunk(BinaryReader br) 37 | { 38 | var lastOffset = br.BaseStream.Position; 39 | 40 | br.BaseStream.Position = Offset; 41 | Name = br.ReadString(4); 42 | Length = br.ReadUInt32(); 43 | 44 | br.BaseStream.Position = lastOffset; 45 | } 46 | 47 | public void ParseAllChunks(BinaryReader br) 48 | { 49 | ParseChunk(br); 50 | if (Subchunks != null) 51 | { 52 | foreach (var sc in Subchunks) sc.ParseChunk(br); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/GameMaker/GameMakerChunkForm.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.GameMaker; 6 | 7 | public class GameMakerChunkForm : GameMakerChunk 8 | { 9 | private List _subchunks = null; 10 | public override IEnumerable Subchunks 11 | { 12 | get => _subchunks; 13 | } 14 | 15 | public IEnumerable GetSubchunks(BinaryReader br) 16 | { 17 | if (_subchunks != null) 18 | { 19 | foreach (var sc in _subchunks) yield return sc; 20 | } 21 | 22 | br.BaseStream.Position = PayloadOffset; 23 | 24 | while (br.BaseStream.Position < Offset + Length) 25 | { 26 | yield return GameMakerChunk.CreateFromBinaryReader(br); 27 | } 28 | } 29 | 30 | public override void ParseChunk(BinaryReader br) 31 | { 32 | base.ParseChunk(br); 33 | _subchunks = GetSubchunks(br).ToList(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/GameMaker/GameMakerParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.GameMaker; 6 | 7 | [Parser("gamemaker", "GameMaker Asset Archive", [ ".win", ".unx" ])] 8 | public class GameMakerParser : IParser 9 | { 10 | public Stream InputStream { get; set; } 11 | public Stream AuxiliaryInputStream { get; set; } 12 | 13 | private GameMakerChunk _rootChunk = null; 14 | 15 | private void ParseInputStream() 16 | { 17 | using BinaryReader br = new(InputStream, Encoding.ASCII, true); 18 | 19 | _rootChunk = GameMakerChunk.CreateFromBinaryReader(br); 20 | _rootChunk.ParseAllChunks(br); 21 | } 22 | 23 | public IEnumerable GetSubFiles() 24 | { 25 | ParseInputStream(); 26 | 27 | foreach (var chunk in _rootChunk.Subchunks) 28 | { 29 | switch (chunk.Name) 30 | { 31 | default: 32 | yield return new(chunk.Name, chunk.Offset, chunk.Length); 33 | break; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/IParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace Unai.ExtendedBinaryWaterfall.Parsers; 5 | 6 | public interface IParser 7 | { 8 | public Stream InputStream { get; set; } 9 | public Stream AuxiliaryInputStream { get; set; } 10 | public IEnumerable GetSubFiles(); 11 | } 12 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Iso9660/Iso9660Parser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Iso9660; 8 | 9 | [Parser("iso9660", "ISO 9660 Disc File System", [ ".iso" ])] 10 | public class Iso9660Parser : IParser 11 | { 12 | public Stream InputStream { get; set; } 13 | public Stream AuxiliaryInputStream { get; set; } 14 | 15 | private static IsoDirectoryEntry ParseIsoDirectoryEntry(BinaryReader br, Encoding encoding = null) 16 | { 17 | encoding ??= Encoding.ASCII; 18 | 19 | var dentLoc = br.BaseStream.Position; 20 | 21 | var dentLen = br.ReadByte(); 22 | if (dentLen == 0) return null; 23 | var dentExtAttrRecLen = br.ReadByte(); 24 | var dentLba = br.ReadInt32(); br.BaseStream.Position += 4; 25 | var dentDataLen = br.ReadInt32(); br.BaseStream.Position += 4; 26 | var dentDateTime = br.ReadBytes(7); 27 | var dentFlags = br.ReadByte(); 28 | var dentInterUnitSize = br.ReadByte(); 29 | var dentInterGapSize = br.ReadByte(); 30 | var dentVolSeqNum = br.ReadInt16(); br.BaseStream.Position += 2; 31 | var dentNameLen = br.ReadByte(); 32 | var dentName = encoding.GetString(br.ReadBytes(dentNameLen)); 33 | // Console.Error.WriteLine($"{dentLoc:X8} {dentNameLen} '{dentName}'"); 34 | 35 | if (dentNameLen % 2 == 0) br.BaseStream.Position++; 36 | 37 | return new() { Name = dentName, DataLba = dentLba, DataLength = dentDataLen, Flags = dentFlags, TextEncoding = encoding }; 38 | } 39 | 40 | private static IEnumerable TraverseIsoDirectoryEntry(BinaryReader br, IsoDirectoryEntry dir, string parentPath = "", int recursiveCount = 0) 41 | { 42 | Logger.Debug($"Traversing ISO directory {(string.IsNullOrEmpty(parentPath) ? "/" : parentPath)}"); 43 | if (recursiveCount > 16) 44 | { 45 | yield break; 46 | } 47 | br.BaseStream.Position = dir.DataLba * 2048; 48 | while (br.BaseStream.Position < dir.DataLba * 2048 + dir.DataLength) 49 | { 50 | var dent = ParseIsoDirectoryEntry(br, dir.TextEncoding); 51 | if (dent == null) continue; // we're probably in a zero-filled sector limit gap and we need to seek further. 52 | 53 | if (dent.Name.Length > 1 && (dent.Name[0] != 0 && dent.Name[0] != 1)) 54 | { 55 | yield return new(parentPath + "/" + dent.Name, dent.DataLba * 2048, dent.DataLength) { IsDirectory = dent.IsDirectory }; 56 | if (dent.IsDirectory) 57 | { 58 | var dentEndOfs = br.BaseStream.Position; 59 | foreach (var sf in TraverseIsoDirectoryEntry(br, dent, parentPath + "/" + dent.Name, recursiveCount + 1)) 60 | { 61 | yield return sf; 62 | } 63 | br.BaseStream.Position = dentEndOfs; 64 | } 65 | } 66 | } 67 | } 68 | 69 | public IEnumerable GetSubFiles() 70 | { 71 | yield return new("System Area", 0, 0x8000) { IconString = "🔶" }; 72 | 73 | using BinaryReader br = new(InputStream, Encoding.ASCII, true); 74 | 75 | int pathTableOfs = 0; 76 | int pathTableSize = 0; 77 | List rootDirs = []; 78 | 79 | for (int i = 0; i < 16; i++) 80 | { 81 | Logger.Debug($"Reading volume descriptor {i}…"); 82 | br.BaseStream.Position = 0x8000 + (2048 * i); 83 | var volDesOfs = br.BaseStream.Position; 84 | 85 | var volDesType = br.ReadByte(); 86 | var volDesId = Encoding.ASCII.GetString(br.ReadBytes(5)); 87 | var volDesVersion = br.ReadByte(); 88 | 89 | switch (volDesType) 90 | { 91 | case 1: 92 | case 2: 93 | bool usesJoliet = false; 94 | 95 | br.BaseStream.Position = volDesOfs + 8; 96 | var pvdSystemId = Encoding.ASCII.GetString(br.ReadBytes(32)); 97 | var pvdVolId = Encoding.ASCII.GetString(br.ReadBytes(32)); 98 | br.BaseStream.Position += 8; 99 | var pvdVolNumLogicalBlocks = br.ReadInt32(); br.BaseStream.Position += 4; 100 | var pvdEscapeSeqs = br.ReadBytes(32); 101 | var pvdVolSeqSize = br.ReadInt16(); br.BaseStream.Position += 2; 102 | var pvdVolSeqIdx = br.ReadInt16(); br.BaseStream.Position += 2; 103 | br.BaseStream.Position = volDesOfs + 128; 104 | var pvdLogicalBlockSize = br.ReadInt16(); br.BaseStream.Position += 2; 105 | var pvdPathTableSize = br.ReadInt32(); br.BaseStream.Position += 4; 106 | var pvdPathTableLeLba = br.ReadInt32(); 107 | var pvdPathTableOptLeLba = br.ReadInt32(); 108 | 109 | // FIXME: Only takes into account UCS-2 Level 3 escape sequence. 110 | if (pvdEscapeSeqs[0] == 0x25 && pvdEscapeSeqs[1] == 0x2f && pvdEscapeSeqs[2] == 0x45) 111 | { 112 | usesJoliet = true; 113 | } 114 | 115 | br.BaseStream.Position = volDesOfs + 156; 116 | rootDirs.Add(ParseIsoDirectoryEntry(br, usesJoliet ? Encoding.BigEndianUnicode : Encoding.ASCII)); 117 | 118 | pathTableOfs = pvdPathTableLeLba * 2048; 119 | pathTableSize = pvdPathTableSize; 120 | break; 121 | } 122 | 123 | string volDesDisplayString = volDesType switch 124 | { 125 | 0 => "Boot Record", 126 | 1 => "Primary Volume Descriptor", 127 | 2 => "Secondary Volume Descriptor", 128 | _ => $"Volume Descriptor, Type {volDesType:X2}" 129 | }; 130 | 131 | yield return new(volDesDisplayString, volDesOfs, 2048) { IconString = "🔶" }; 132 | 133 | // volume type 0xff signals end of volume descriptor table. 134 | if (volDesType == 0xff) break; 135 | } 136 | 137 | yield return new("Path Table", pathTableOfs, pathTableSize) { IconString = "🔶" }; 138 | 139 | var pathTableEntries = new List(); 140 | 141 | br.BaseStream.Position = pathTableOfs; 142 | 143 | while (br.BaseStream.Position < pathTableOfs + pathTableSize) 144 | { 145 | var pteDirNameLen = br.ReadByte(); 146 | var pteExtAttrRecLen = br.ReadByte(); 147 | var pteLba = br.ReadInt32(); 148 | var pteParentRecIdx = br.ReadInt16(); 149 | var pteDirName = Encoding.ASCII.GetString(br.ReadBytes(pteDirNameLen)); 150 | if (pteDirNameLen % 2 == 1) br.BaseStream.Position++; 151 | 152 | pathTableEntries.Add(new() { Name = pteDirName, Lba = pteLba, ParentIndex = pteParentRecIdx }); 153 | } 154 | 155 | foreach (var rootDir in rootDirs) 156 | { 157 | yield return new("/", rootDir.DataLba * 2048, rootDir.DataLength) { IsDirectory = true }; 158 | } 159 | 160 | // We're interested in long file names, which are stored in the SVD's root directory as per the Joliet specification. 161 | // For a complete parsing of the ISO file, we need both the legacy (ISO 9660) and Joliet directory entries for output. 162 | // File entries collide between hierarchies and only the Joliet ones will be yielded. 163 | 164 | for (int rootDirIdx = 0; rootDirIdx < rootDirs.Count; rootDirIdx++) 165 | { 166 | var rootDir = rootDirs[rootDirIdx]; 167 | 168 | br.BaseStream.Position = rootDir.DataLba * 2048; 169 | 170 | foreach (var dir in TraverseIsoDirectoryEntry(br, rootDir)) 171 | { 172 | if (dir.IsDirectory || rootDirIdx == rootDirs.Count - 1) 173 | { 174 | yield return dir; 175 | } 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Iso9660/IsoDirectoryEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Iso9660; 4 | 5 | class IsoDirectoryEntry 6 | { 7 | public string Name; 8 | public int DataLba; 9 | public int DataLength; 10 | public byte Flags; 11 | public Encoding TextEncoding = Encoding.ASCII; 12 | 13 | public bool IsDirectory => (Flags & 0b10) != 0; 14 | } 15 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Iso9660/IsoPathTableEntry.cs: -------------------------------------------------------------------------------- 1 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Iso9660; 2 | 3 | class IsoPathTableEntry 4 | { 5 | public string Name; 6 | public int Lba; 7 | public int ParentIndex; 8 | } 9 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/MiniDump/MiniDumpParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Unai.ExtendedBinaryWaterfall.Parsers.MiniDump; 7 | 8 | class ModuleEntry(string fileName, ulong baseAddress, ulong size, ulong endAddress, uint timestamp) 9 | { 10 | public string FileName = fileName; 11 | public ulong BaseAddress = baseAddress; 12 | public ulong Size = size; 13 | public ulong EndAddress = endAddress; 14 | public uint Timestamp = timestamp; 15 | } 16 | 17 | class MemoryEntry(ulong vaStart, ulong rva, ulong size) 18 | { 19 | public ulong VAStart = vaStart; 20 | public ulong RVA = rva; 21 | public ulong Size = size; 22 | } 23 | 24 | [Parser("minidump", "Windows Memory Dump (Minidump)", [ ".dmp" ])] 25 | public class MiniDumpParser : IParser 26 | { 27 | public Stream InputStream { get; set; } = null; 28 | public Stream AuxiliaryInputStream { get; set; } = null; 29 | 30 | public IEnumerable GetSubFiles() 31 | { 32 | var ret = DoGetSubFiles(); 33 | // Merge contiguous regions of memory with the same module file path. 34 | ret = [.. ret.MixLastOcurrences((sf, lsf) => sf.Path == lsf.Path, (lsf, sf) => { lsf.EndOffset = sf.EndOffset; })]; 35 | // Prepend the minidump header. 36 | return 37 | [ 38 | new("Header", 0, ret.OrderBy(sf => sf.StartOffset).FirstOrDefault().StartOffset) { IconString = "🔶" }, 39 | .. ret, 40 | ]; 41 | } 42 | 43 | private IEnumerable DoGetSubFiles() 44 | { 45 | // var auxFilePath = ((FileStream)InputStream).Name + ".txt"; // FIXME: this is horrible. 46 | // if (!File.Exists(auxFilePath)) 47 | // { 48 | // throw new FileNotFoundException($"File does not exist: '{auxFilePath}'.\nMake sure you generate a file with Python module 'minidump' (using the '--all' switch) that contains a list of executable modules, memory regions and other data at the specified path.\nExample: python -m minidump --all [input_file] > [output_file]"); 49 | // } 50 | // var auxFileStream = File.OpenRead(auxFilePath); 51 | 52 | List modules = []; 53 | List memoryRanges = []; 54 | char parseMode = ' '; 55 | bool doParse = false; 56 | 57 | using var sr = new StreamReader(AuxiliaryInputStream, Encoding.ASCII, leaveOpen: true); 58 | foreach (var line in sr.ReadToEnd().Split('\n')) 59 | { 60 | if (line.StartsWith("== ")) 61 | { 62 | doParse = false; 63 | if (line == "== ModuleList ==") 64 | { 65 | parseMode = 'o'; 66 | } 67 | else if (line == "== UnloadedModuleList ==") 68 | { 69 | parseMode = 'u'; 70 | } 71 | else if (line == "== MinidumpMemory64List ==") 72 | { 73 | parseMode = '6'; 74 | } 75 | else 76 | { 77 | parseMode = ' '; 78 | } 79 | continue; 80 | } 81 | 82 | if (!doParse && line.StartsWith("----")) 83 | { 84 | doParse = true; 85 | continue; 86 | } 87 | 88 | if (doParse && line.Contains(" | ")) 89 | { 90 | var fields = line.Split(" | ").Select(s => s.Trim()).ToArray(); 91 | switch (parseMode) 92 | { 93 | case 'o': 94 | modules.Add(new( 95 | fields[0], 96 | Utils.ParseHex(fields[1]), 97 | Utils.ParseHex(fields[2]), 98 | Utils.ParseHex(fields[3]), 99 | (uint)Utils.ParseHex(fields[4]) 100 | )); 101 | break; 102 | 103 | case 'u': 104 | modules.Add(new( 105 | fields[0], 106 | Utils.ParseHex(fields[1]), 107 | Utils.ParseHex(fields[2]), 108 | Utils.ParseHex(fields[3]), 109 | 0 110 | )); 111 | break; 112 | 113 | case '6': 114 | memoryRanges.Add(new( 115 | Utils.ParseHex(fields[0]), 116 | Utils.ParseHex(fields[1]), 117 | Utils.ParseHex(fields[2]) 118 | )); 119 | break; 120 | } 121 | } 122 | } 123 | 124 | foreach (var memoryRange in memoryRanges) 125 | { 126 | var ret = new SubFile("Unknown Memory", (long)memoryRange.RVA, (long)memoryRange.Size) 127 | { 128 | IconString = "❓", 129 | Description = $"Base Address: 0x{memoryRange.VAStart:X16}" 130 | }; 131 | 132 | var module = modules.FirstOrDefault(m => Utils.Intersects(m.BaseAddress, m.EndAddress, memoryRange.VAStart, memoryRange.VAStart + memoryRange.Size)); 133 | if (module != null) 134 | { 135 | ret.Path = module.FileName.Replace('\\', '/'); 136 | ret.IconString = null; 137 | } 138 | yield return ret; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/ParserAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall.Parsers; 4 | 5 | public class ParserAttribute(string id, string name = null, string[] extensions = null) : Attribute 6 | { 7 | public string Id { get; set; } = id; 8 | public string Name { get; set; } = name; 9 | public string[] FileExtensions { get; set; } = extensions; 10 | 11 | public ParserAttribute() : this(null) {} 12 | } 13 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/PortableExecutable/PortableExecutableParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.PortableExecutable; 6 | 7 | public enum PeDataDirectory 8 | { 9 | ExportTable, 10 | ImportTable, 11 | ResourceTable, 12 | ExceptionTable, 13 | SecurityTable, 14 | BaseRelocationTable, 15 | Debug, 16 | Description, 17 | GlobalPointer, 18 | TlsTable, 19 | LoadConfigurationTable, 20 | BoundImport, 21 | ImportAddressTable, 22 | DelayImportDescriptor, 23 | ClrRuntimeHeader, 24 | } 25 | 26 | [Parser("pe", "Portable Executable", [ ".exe", ".dll", ".mui", ".sys", ".scr", ".cpl", ".ocx", ".ax", ".fon", ".efi" ])] 27 | public class PortableExecutableParser : IParser 28 | { 29 | public Stream InputStream { get; set; } 30 | public Stream AuxiliaryInputStream { get; set; } 31 | 32 | public long CoffHeaderOffset { get; private set; } = 0; 33 | public long CoffOptionalSectionOffset { get; private set; } = 0; 34 | public long DataDirectoryTableOffset { get; private set; } = 0; 35 | 36 | public IEnumerable GetSubFiles() 37 | { 38 | using BinaryReader br = new(InputStream, Encoding.ASCII, true); 39 | 40 | yield return new("DOS Header", 0, 0x40); 41 | 42 | var peDosHdrMagic = br.ReadString(2); // "MZ" 43 | br.BaseStream.Position = 0x3c; 44 | CoffHeaderOffset = br.ReadUInt32(); 45 | br.BaseStream.Position = CoffHeaderOffset; 46 | 47 | yield return new("DOS Stub", 0x40, CoffHeaderOffset) { IconString = "🔶" }; 48 | yield return new("COFF Header", CoffHeaderOffset, 0x18) { IconString = "🔶" }; 49 | 50 | // PE COFF Header 51 | var peMagic = br.ReadString(4); // "PE\0\0" 52 | var peMachineId = br.ReadUInt16(); 53 | var peSectionCount = br.ReadUInt16(); 54 | var peTimestamp = br.ReadUInt32(); 55 | var peSymTabPtr = br.ReadUInt32(); // unused 56 | var peSymTabCount = br.ReadUInt32(); // unused 57 | var peOptionalHeaderSize = br.ReadUInt16(); 58 | var peFlags = br.ReadUInt16(); 59 | Logger.Debug($"PE COFF Header: machine {peMachineId:X4}, {peSectionCount} sections"); 60 | bool is64Bit = peMachineId == 0x8664; 61 | 62 | // PE Optional Header 63 | CoffOptionalSectionOffset = br.BaseStream.Position; 64 | var peOptHdrMagic = br.ReadUInt16(); 65 | bool isPe32Plus = peOptHdrMagic == 0x020b; 66 | var peLinkerVerMajor = br.ReadByte(); 67 | var peLinkerVerMinor = br.ReadByte(); 68 | var peSizeOfCode = br.ReadUInt32(); 69 | var peSizeOfInitData = br.ReadUInt32(); 70 | var peSizeOfUninitData = br.ReadUInt32(); 71 | var peEntryPointOfs = br.ReadUInt32(); 72 | var peBaseOfCode = br.ReadUInt32(); 73 | var peBaseOfData = isPe32Plus ? 0 : br.ReadUInt32(); 74 | // NT-specific 75 | var peNtImageBase = isPe32Plus ? br.ReadUInt64() : br.ReadUInt32(); 76 | var peNtSectionAlignment = br.ReadUInt32(); 77 | var peNtFileAlignment = br.ReadUInt32(); 78 | var peNtOsVerMajor = br.ReadUInt16(); 79 | var peNtOsVerMinor = br.ReadUInt16(); 80 | var peNtImageVerMajor = br.ReadUInt16(); 81 | var peNtImageVerMinor = br.ReadUInt16(); 82 | var peNtSubsysVerMajor = br.ReadUInt16(); 83 | var peNtSubsysVerMinor = br.ReadUInt16(); 84 | br.ReadUInt32(); // reserved 85 | var peNtSizeOfImage = br.ReadUInt32(); 86 | var peNtSizeOfHeaders = br.ReadUInt32(); 87 | var peNtChecksum = br.ReadUInt32(); 88 | var peNtSubsystem = br.ReadUInt16(); 89 | var peNtDllFlags = br.ReadUInt16(); 90 | var peNtSizeOfStackReserve = isPe32Plus ? br.ReadUInt64() : br.ReadUInt32(); 91 | var peNtSizeOfStackCommit = isPe32Plus ? br.ReadUInt64() : br.ReadUInt32(); 92 | var peNtSizeOfHeapReserve = isPe32Plus ? br.ReadUInt64() : br.ReadUInt32(); 93 | var peNtSizeOfHeapCommit = isPe32Plus ? br.ReadUInt64() : br.ReadUInt32(); 94 | var peNtLoaderFlags = br.ReadUInt32(); 95 | var peNtRvaSizePairCount = br.ReadUInt32(); 96 | Logger.Debug($"PE Opt. Header: magic {peOptHdrMagic:X4} code size {peSizeOfCode:X8} entrypoint {peEntryPointOfs:X8}"); 97 | Logger.Debug($"NT Header: image base {peNtImageBase:X8}, osver {peNtOsVerMajor}.{peNtOsVerMinor}, subsys {peNtSubsystem}, {peNtRvaSizePairCount} dirs"); 98 | yield return new("COFF Optional Header", CoffOptionalSectionOffset, br.BaseStream.Position - CoffOptionalSectionOffset) { IconString = "🔶" }; 99 | 100 | // Data Dirs. 101 | DataDirectoryTableOffset = br.BaseStream.Position; 102 | Logger.Debug("Reading PE data directories…"); 103 | for (int i = 0; i < peNtRvaSizePairCount; i++) 104 | { 105 | var dataDirRva = br.ReadUInt32(); 106 | var dataDirSize = br.ReadUInt32(); 107 | if (dataDirRva != 0) 108 | { 109 | Logger.Debug($"PE data dir {i,2}: RVA {dataDirRva:X16} Size {dataDirSize}"); 110 | } 111 | } 112 | yield return new("Data Directory Table", DataDirectoryTableOffset, br.BaseStream.Position - DataDirectoryTableOffset) { IconString = "🔶" }; 113 | 114 | // Sections 115 | Logger.Debug("Reading PE section headers…"); 116 | 117 | for (int i = 0; i < peSectionCount; i++) 118 | { 119 | var sectOfs = br.BaseStream.Position; 120 | 121 | var sectName = br.ReadString(8).TrimEnd('\0'); 122 | var sectSize = br.ReadUInt32(); 123 | var sectVirtualAddr = br.ReadUInt32(); 124 | var sectRawDataSize = br.ReadUInt32(); 125 | var sectRawDataPtr = br.ReadUInt32(); 126 | var sectRelocPtr = br.ReadUInt32(); 127 | var sectLineNumPtr = br.ReadUInt32(); 128 | var sectRelocCount = br.ReadUInt16(); 129 | var sectLineNumCount = br.ReadUInt16(); 130 | var sectFlags = br.ReadUInt32(); 131 | 132 | Logger.Debug($"PE Section: {sectName} size {sectSize:X8} vaddr {sectVirtualAddr:X8} data {sectRawDataPtr:X8}:{sectRawDataSize:X8}"); 133 | yield return new(sectName, (long)sectVirtualAddr, (long)sectSize); 134 | 135 | br.BaseStream.Position = sectOfs + 40; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Wad/WadParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Wad; 8 | 9 | [Parser("wad", "Doom Engine's Asset Archive (WAD)", [ ".wad" ])] 10 | public class WadParser : IParser 11 | { 12 | public static string[] MapLumps = 13 | [ 14 | "THINGS", 15 | "LINEDEFS", 16 | "SIDEDEFS", 17 | "VERTEXES", 18 | "SEGS", 19 | "SSECTORS", 20 | "NODES", 21 | "SECTORS", 22 | "REJECT", 23 | "BLOCKMAP", 24 | "BEHAVIOR", // Hexen 25 | ]; 26 | 27 | public Stream InputStream { get; set; } 28 | public Stream AuxiliaryInputStream { get; set; } 29 | 30 | public IEnumerable GetSubFiles() 31 | { 32 | using var br = new BinaryReader(InputStream, Encoding.ASCII, true); 33 | 34 | var wadMagic = br.ReadBytes(4); // IWAD / PWAD 35 | var wadLumpCount = br.ReadUInt32(); 36 | var wadDirectoryOff = br.ReadUInt32(); 37 | 38 | yield return new("WAD Header", 0, 8) { IconString = "🔶" }; 39 | yield return new("WAD Directory", wadDirectoryOff, br.BaseStream.Length - wadDirectoryOff) { IconString = "🔶" }; 40 | 41 | br.BaseStream.Position = wadDirectoryOff; 42 | 43 | string currentMap = null; 44 | bool isPixelData = false; // flat or sprite 45 | 46 | for (int i = 0; i < wadLumpCount; i++) 47 | { 48 | var lumpDataOff = br.ReadUInt32(); 49 | var lumpDataLen = br.ReadUInt32(); 50 | var lumpName = br.ReadString(8).TrimEnd('\0'); 51 | 52 | if (lumpDataLen == 0 && ((lumpName[0] == 'E' && lumpName[2] == 'M') || lumpName.StartsWith("MAP"))) 53 | { 54 | currentMap = lumpName; 55 | } 56 | else if (!MapLumps.Contains(lumpName)) 57 | { 58 | currentMap = null; 59 | } 60 | 61 | if (lumpName == "S_START" || lumpName == "F_START") 62 | { 63 | isPixelData = true; 64 | } 65 | else if (lumpName == "S_END" || lumpName == "F_END") 66 | { 67 | isPixelData = false; 68 | } 69 | 70 | if (lumpDataOff + lumpDataLen > 0) 71 | { 72 | var subfile = new SubFile(currentMap != null ? $"{currentMap}/{lumpName}" : lumpName, lumpDataOff, lumpDataLen); 73 | if (isPixelData) subfile.IconString = Utils.GetFileTypeEmojiFromExtension(".bmp"); 74 | yield return subfile; 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/WindowsIcon/WindowsIconParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace Unai.ExtendedBinaryWaterfall.Parsers.WindowsIcon; 7 | 8 | public class WindowsIconEntry 9 | { 10 | public int Width; 11 | public int Height; 12 | public int ColorPaletteColorCount; 13 | public ushort ColorPlanesOrHotspotX; 14 | public ushort BppOrHotspotY; 15 | public uint DataSize; 16 | public uint DataOffset; 17 | public byte[] RgbaBitmapData; 18 | 19 | public byte[] GetBitmap(byte[] iconFileData = null) 20 | { 21 | using MemoryStream ms = new(); 22 | using BinaryWriter bw = new(ms, Encoding.ASCII, true); 23 | 24 | // struct ICONIMAGE 25 | // var iconData = Data ?? iconFileData[(int)DataOffset..(int)(DataOffset + DataSize)]; 26 | 27 | bw.Write('B'); 28 | bw.Write('M'); 29 | bw.Write((uint)(RgbaBitmapData.Length + 14 + 40)); // file size 30 | bw.Write((uint)0); 31 | bw.Write((uint)(14 + 40)); 32 | 33 | // var infoHeaderOff = bw.BaseStream.Position; 34 | 35 | bw.Write((uint)40); 36 | bw.Write((uint)Width); 37 | bw.Write((uint)Height); 38 | bw.Write((ushort)1); // planes (16) 39 | bw.Write((ushort)BppOrHotspotY); 40 | bw.Write(0); // compression mode 41 | bw.Write(RgbaBitmapData.Length); // size of image 42 | bw.Write(0); // x px/m 43 | bw.Write(0); // y px/m 44 | bw.Write(0); // colors used 45 | bw.Write(0); // important colors 46 | 47 | bw.Write(RgbaBitmapData); 48 | 49 | return ms.ToArray(); 50 | } 51 | } 52 | 53 | [Parser("ico", "Windows Icon (ICO)", [ ".ico", ".cur" ])] 54 | public class WindowsIconParser : IParser 55 | { 56 | public Stream InputStream { get; set; } 57 | public Stream AuxiliaryInputStream { get; set; } 58 | 59 | public List Entries { get; } = []; 60 | 61 | public IEnumerable GetSubFiles() 62 | { 63 | foreach (var entry in Entries) 64 | { 65 | yield return new($"@{entry.DataOffset:x8}", entry.DataOffset, entry.DataSize); 66 | } 67 | } 68 | 69 | public void Load(byte[] input) 70 | { 71 | using MemoryStream ms = new(input, false); 72 | using BinaryReader br = new(ms); 73 | Load(br); 74 | } 75 | 76 | public void Load(BinaryReader br) 77 | { 78 | var reserved0 = br.ReadUInt16(); 79 | var imageType = br.ReadUInt16(); 80 | var imageCount = br.ReadUInt16(); 81 | 82 | for (int i = 0; i < imageCount; i++) 83 | { 84 | var imageEntry = new WindowsIconEntry(); 85 | 86 | imageEntry.Width = br.ReadByte(); 87 | imageEntry.Height = br.ReadByte(); 88 | imageEntry.ColorPaletteColorCount = br.ReadByte(); 89 | _ = br.ReadByte(); 90 | imageEntry.ColorPlanesOrHotspotX = br.ReadUInt16(); 91 | imageEntry.BppOrHotspotY = br.ReadUInt16(); 92 | imageEntry.DataSize = br.ReadUInt32(); 93 | imageEntry.DataOffset = br.ReadUInt32(); 94 | 95 | Logger.Debug($"Icon: {imageEntry.Width}×{imageEntry.Height} @{imageEntry.DataOffset:X8} {imageEntry.DataSize}"); 96 | 97 | Entries.Add(imageEntry); 98 | } 99 | 100 | foreach (var imageEntry in Entries) 101 | { 102 | br.BaseStream.Position = imageEntry.DataOffset; 103 | 104 | var bmInfoHeaderSize = br.ReadUInt32(); 105 | var bmWidth = br.ReadUInt32(); 106 | var bmHeight = br.ReadUInt32(); 107 | var bmPlanes = br.ReadUInt16(); 108 | var bmBitsPerPixel = br.ReadUInt16(); 109 | // var bmCompression = br.ReadUInt32(); 110 | // var bmSizeOfImage = br.ReadUInt32(); 111 | 112 | Logger.Debug($"Icon bitmap info: {bmWidth}×{bmHeight} {bmBitsPerPixel}bpp"); 113 | 114 | br.BaseStream.Position = imageEntry.DataOffset + bmInfoHeaderSize; 115 | 116 | if (bmBitsPerPixel == 32) // BGRA, then 1-bit alpha mask 117 | { 118 | byte[] bmXorMask = br.ReadBytes((int)((bmWidth * (bmHeight / 2)) * 4)); 119 | byte[] bmAndMask = br.ReadBytes((int)((bmWidth * (bmHeight / 2)) / 8)); 120 | 121 | imageEntry.RgbaBitmapData = [..bmXorMask]; 122 | 123 | // Apply AND (alpha) mask to BGRA data. 124 | // for (int off = 0; off < bmAndMask.Length; off++) 125 | // { 126 | // var maskValue8 = bmAndMask[off]; 127 | // if (off % (bmWidth / 8) == 0) Console.Error.WriteLine(); 128 | // Console.Error.Write($"{maskValue8:b8}"); 129 | // for (int byteOff = 0; byteOff < 8; byteOff++) 130 | // { 131 | // bool maskValue = ((maskValue8 >> (8 - byteOff)) & 1) == 1; 132 | // var rgbaOff = 3 + (off * 8 + byteOff) * 4; 133 | // imageEntry.RgbaBitmapData[rgbaOff] = (byte)(maskValue ? 0xff : 0x00); 134 | // // if (off < 16) Logger.Debug($"{off}[{byteOff}] = {maskValue8:b8} {(maskValue8 >> byteOff):b8} {maskValue} → {rgbaOff}"); 135 | 136 | // // Console.Error.Write($"{(maskValue?"#":".")}"); 137 | // } 138 | // } 139 | } 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/WindowsImage/WindowsImageParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.WindowsImage; 6 | 7 | [Parser("wim", "Windows Image (WIM)", [ ".wim" ])] 8 | public class WindowsImageParser : IParser 9 | { 10 | public Stream InputStream { get; set; } 11 | public Stream AuxiliaryInputStream { get; set; } 12 | 13 | public IEnumerable GetSubFiles() 14 | { 15 | var ret = ParseWimDir(); 16 | ret = [.. ret 17 | .Where(sf => sf.Length > 0) 18 | .GroupBy(sf => sf.StartOffset) 19 | .Select(sfg => sfg.FirstOrDefault()) 20 | .OrderBy(sf => sf.StartOffset)]; 21 | return 22 | [ 23 | .. ret, 24 | new("WIM File Table", ret.OrderBy(sf => sf.EndOffset).FirstOrDefault().EndOffset, InputStream.Length) { IconString = "🔶" }, 25 | ]; 26 | } 27 | 28 | private IEnumerable ParseWimDir() 29 | { 30 | using var sr = new StreamReader(AuxiliaryInputStream); 31 | string line; 32 | 33 | bool firstLine = true; 34 | string filePath = null; 35 | long fileSize = 0; 36 | long fileOffset = 0; 37 | int fileAttrFlags = 0; 38 | 39 | while ((line = sr.ReadLine()) != null) 40 | { 41 | if (line.StartsWith("--------")) 42 | { 43 | if (!firstLine) 44 | { 45 | yield return new(filePath, fileOffset, fileSize) { IsDirectory = (fileAttrFlags & 0x10) == 0x10 }; 46 | } 47 | firstLine = false; 48 | } 49 | string[] kvp = line.Split(" = ", 2); 50 | if (line.StartsWith("Full Path")) 51 | { 52 | filePath = kvp[1][1..^1]; 53 | } 54 | else if (line.StartsWith("Uncompressed size")) 55 | { 56 | fileSize = long.Parse(kvp[1].Split(' ')[0]); 57 | } 58 | else if (line.StartsWith("Offset in WIM")) 59 | { 60 | fileOffset = long.Parse(kvp[1].Split(' ')[0]); 61 | } 62 | else if (line.StartsWith("Attributes")) 63 | { 64 | fileAttrFlags = int.Parse(kvp[1][2..], System.Globalization.NumberStyles.HexNumber); 65 | } 66 | } 67 | 68 | yield return new(filePath, fileOffset, fileSize) { IsDirectory = (fileAttrFlags & 0x10) == 0x10 }; 69 | } 70 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Parsers/Zip/ZipParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Unai.ExtendedBinaryWaterfall.Parsers.Zip; 6 | 7 | [Parser("zip", "ZIP Archive", [ ".zip", ".apk", ".msix", ".epub", ".jar", ".war", ".docx", ".xlsx", ".pptx", ".odt", ".ods", ".odp", ".pk3", ".pk4" ])] 8 | public class ZipParser : IParser 9 | { 10 | public Stream InputStream { get; set; } 11 | public Stream AuxiliaryInputStream { get; set; } 12 | public bool SkipDirectories { get; set; } = false; 13 | 14 | public long EocdOffset { get; private set; } = 0; 15 | public long CentralDirectoryOffset { get; private set; } = 0; 16 | 17 | public IEnumerable GetSubFiles() 18 | { 19 | using var br = new BinaryReader(InputStream, Encoding.UTF8, true); 20 | 21 | // Find End of Central Directory (EOCD) Offset 22 | br.BaseStream.Position = br.BaseStream.Length - 22; 23 | while (br.BaseStream.Position > br.BaseStream.Length - 0xffff + 22) 24 | { 25 | var eocdSig = br.ReadUInt32(); 26 | if (eocdSig == 0x06054b50) 27 | { 28 | EocdOffset = br.BaseStream.Position - 4; 29 | break; 30 | } 31 | br.BaseStream.Position -= 8; 32 | } 33 | 34 | if (EocdOffset == 0) 35 | { 36 | Logger.Error("Cannot parse ZIP file: cannot find EOCD signature."); 37 | yield break; 38 | } 39 | 40 | var eocdDiskNumber = br.ReadUInt16(); 41 | var eocdDiskCdirStart = br.ReadUInt16(); 42 | var eocdCdirCount = br.ReadUInt16(); 43 | var eocdCdirTotalCount = br.ReadUInt16(); 44 | var eocdCdirSize = br.ReadUInt32(); 45 | var eocdCdirStart = br.ReadUInt32(); 46 | var eocdCommentSize = br.ReadUInt16(); 47 | var eocdComment = eocdCommentSize > 0 ? br.ReadString(eocdCommentSize) : null; 48 | 49 | CentralDirectoryOffset = eocdCdirStart; 50 | 51 | yield return new("End of Central Directory", EocdOffset, 22 + eocdCommentSize) { IconString = "🔶" }; 52 | yield return new("Central Directory", eocdCdirStart, eocdCdirSize) { IconString = "🔶" }; 53 | 54 | // Central Directory 55 | br.BaseStream.Position = CentralDirectoryOffset; 56 | 57 | while (br.BaseStream.Position < eocdCdirStart + eocdCdirSize) 58 | { 59 | var cdirOfs = br.BaseStream.Position; 60 | var cdirSig = br.ReadUInt32(); // 0x02014b50 / "PK\1\2" 61 | if (cdirSig != 0x02014b50) 62 | { 63 | Logger.Error($"Cannot read central directory record: invalid signature ({cdirSig:X8})"); 64 | break; 65 | } 66 | 67 | var cdirMadeByVer = br.ReadUInt16(); 68 | var cdirMinDecodeVer = br.ReadUInt16(); 69 | var cdirFlags = br.ReadUInt16(); 70 | var cdirCompressionAlgo = br.ReadUInt16(); 71 | var cdirLastWrite = br.ReadUInt32(); // MS-DOS format time, then date 72 | var cdirCrc32 = br.ReadUInt32(); 73 | var cdirCompressedSize = br.ReadUInt32(); 74 | var cdirUncompressedSize = br.ReadUInt32(); 75 | var cdirFileNameSize = br.ReadUInt16(); 76 | var cdirExtraFieldSize = br.ReadUInt16(); 77 | var cdirFileCommentSize = br.ReadUInt16(); 78 | var cdirFileDiskNumber = br.ReadUInt16(); 79 | var cdirFileAttrInt = br.ReadUInt16(); 80 | var cdirFileAttrExt = br.ReadUInt32(); 81 | var cdirLocalFileHdrOfs = br.ReadUInt32(); 82 | var cdirFileName = br.ReadString(cdirFileNameSize); 83 | var cdirExtraField = br.ReadBytes(cdirExtraFieldSize); 84 | var cdirFileComment = br.ReadString(cdirFileCommentSize); 85 | 86 | var cdirNextOfs = br.BaseStream.Position; 87 | 88 | Logger.Debug($"Dir. Rec at 0x{cdirOfs:X8}: '{cdirFileName}' {cdirCompressedSize} bytes (uncomp. {cdirUncompressedSize})"); 89 | 90 | // Local File Header 91 | br.BaseStream.Position = cdirLocalFileHdrOfs; 92 | var lfhSig = br.ReadUInt32(); // 0x04034b50 / "PK\3\4" 93 | br.BaseStream.Position += 22; 94 | var lfhFileNameSize = br.ReadUInt16(); 95 | var lfhExtraFieldSize = br.ReadUInt16(); 96 | 97 | var fileDataOfs = br.BaseStream.Position + lfhFileNameSize + lfhExtraFieldSize; 98 | var isDir = cdirFileName[^1] == '/'; 99 | 100 | if (!(isDir && SkipDirectories)) yield return new(cdirFileName.TrimEnd('/'), fileDataOfs, cdirCompressedSize) { IsDirectory = isDir }; 101 | 102 | br.BaseStream.Position = cdirNextOfs; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/SubFile.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | 3 | namespace Unai.ExtendedBinaryWaterfall; 4 | 5 | public class SubFile(string path, long startOffset, long length) 6 | { 7 | public string Path { get; set; } = path; 8 | public long StartOffset { get; set; } = startOffset; 9 | public long Length { get; set; } = length; 10 | public bool IsDirectory { get; set; } = false; 11 | public long EndOffset { get => StartOffset + Length; set => Length = value - StartOffset; } 12 | public string Description { get; set; } = null; 13 | public string IconString { get; set; } = null; 14 | public Image Icon { get; set; } = null; 15 | 16 | public string FileName => System.IO.Path.GetFileName(Path); 17 | public string FileDirectory => System.IO.Path.GetDirectoryName(Path); 18 | public string Extension => System.IO.Path.GetExtension(Path); 19 | 20 | public bool Intersects(long start, long end) => end > StartOffset && start <= EndOffset; 21 | } 22 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Unai.ExtendedBinaryWaterfall.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | True 6 | true 7 | 8 | v 9 | dev 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Unai.ExtendedBinaryWaterfall/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using SixLabors.ImageSharp; 8 | using SixLabors.ImageSharp.Processing; 9 | using Unai.ExtendedBinaryWaterfall.Parsers.WindowsIcon; 10 | 11 | namespace Unai.ExtendedBinaryWaterfall; 12 | 13 | public static class Utils 14 | { 15 | public static string GetFileTypeEmoji(SubFile subFile) 16 | { 17 | if (subFile.IconString != null) return subFile.IconString; 18 | if (subFile.IsDirectory) return "🗀"; 19 | return GetFileTypeEmojiFromExtension(subFile.Extension); 20 | } 21 | 22 | public static string GetFileTypeEmojiFromExtension(string extension) 23 | { 24 | if (extension == null) return null; 25 | 26 | return extension.ToLower() switch 27 | { 28 | ".png" or 29 | ".jpg" or ".jpeg" or ".jpe" or 30 | ".bmp" or ".dib" or 31 | ".ico" or 32 | ".gif" or 33 | ".heic" or 34 | ".webp" or 35 | ".dng" or 36 | ".tif" or ".tiff" 37 | => "🖼", 38 | ".sys" or 39 | ".dll" or 40 | ".cpl" or 41 | ".msc" or 42 | ".ax" 43 | => "⚙️", 44 | ".txt" or 45 | ".ini" or 46 | ".inf" or 47 | ".htm" or ".html" or 48 | ".xml" or 49 | ".sql" or 50 | ".log" 51 | => "🖹", 52 | ".wav" or 53 | ".mp3" or ".mp2" or ".mp1" or 54 | ".wma" or 55 | ".mid" or ".midi" 56 | => "🎵", 57 | ".avi" or 58 | ".wmv" or 59 | ".mp4" or 60 | ".3gp" or ".3gpp" or 61 | ".mov" or 62 | ".mkv" or 63 | ".mpg" or ".mpeg" or ".vob" 64 | => "🎞️", 65 | ".ufont" or 66 | ".ttf" or 67 | ".ttc" or 68 | ".fon" 69 | => "🗛", 70 | ".chm" or 71 | ".epub" 72 | => "🕮", 73 | ".bat" or 74 | ".exe" or 75 | ".com" or 76 | ".scr" 77 | => "🗔", 78 | ".cab" or 79 | ".7z" or 80 | ".rar" or 81 | ".tar" or 82 | ".tar.gz" or 83 | ".zip" 84 | => "📦️", 85 | ".cur" or 86 | ".ani" 87 | => "🖰", 88 | _ => "🗋", 89 | }; 90 | } 91 | 92 | public static string ToByteSizeString(long value) 93 | { 94 | if (value < 1024) 95 | { 96 | return $"{value:N0} B"; 97 | } 98 | 99 | float fvalue = value / 1024f; 100 | 101 | string[] suffixes = [ "KiB", "MiB", "GiB" ]; 102 | 103 | for (int i = 0; i < suffixes.Length; i++) 104 | { 105 | if (fvalue < 1024) 106 | { 107 | return $"{fvalue:N1} {suffixes[i]}"; 108 | } 109 | fvalue /= 1024f; 110 | } 111 | 112 | return $"Infinity"; 113 | } 114 | 115 | public static string TruncateString(string input, int maxLength) 116 | { 117 | if (input == null) return null; 118 | return input.Length > maxLength ? input[..(maxLength - 1)] + "…" : input; 119 | } 120 | 121 | public static ulong ParseHex(string input) 122 | { 123 | return ulong.Parse(input[2..], System.Globalization.NumberStyles.HexNumber); 124 | } 125 | 126 | public static bool Intersects(ulong aStart, ulong aEnd, ulong bStart, ulong bEnd) 127 | { 128 | return Math.Max(aStart, bStart) <= Math.Min(aEnd, bEnd); 129 | } 130 | 131 | public static IEnumerable SkipLastRepetition(this IEnumerable input, Func criteria) where T : class 132 | { 133 | T last = null; 134 | foreach (var item in input) 135 | { 136 | if (last == null) 137 | { 138 | yield return item; 139 | last = item; 140 | } 141 | else if (criteria(item, last)) 142 | { 143 | yield return item; 144 | last = item; 145 | } 146 | } 147 | } 148 | 149 | public static IEnumerable MixLastOcurrences(this IEnumerable input, Func criteria, Action mixFunc) where T : class 150 | { 151 | bool first = true; 152 | T output = null; 153 | foreach (var item in input) 154 | { 155 | if (first) 156 | { 157 | output = item; 158 | first = false; 159 | } 160 | else if (criteria(item, output)) 161 | { 162 | mixFunc(output, item); 163 | } 164 | else 165 | { 166 | yield return output; 167 | output = item; 168 | } 169 | } 170 | yield return output; 171 | } 172 | 173 | internal static SubFile ParseSubfile(Stream target, SubFile sf) 174 | { 175 | var ext = sf.Extension?.ToLower(); 176 | 177 | switch (ext) 178 | { 179 | case ".exe" or ".dll" or ".sys" or ".scr" or ".ocx" or ".ax" or ".cpl" or ".mui": 180 | Logger.Debug($"Parsing PE executable from subfile '{sf.Path}'…"); 181 | try 182 | { 183 | target.Position = sf.StartOffset; 184 | byte[] peFileBuf = new byte[sf.Length]; 185 | target.ReadExactly(peFileBuf); 186 | var peFile = new PeNet.PeFile(peFileBuf); 187 | 188 | if (peFile.Resources != null) 189 | { 190 | foreach (var stringEntry in peFile.Resources.VsVersionInfo.StringFileInfo.StringTable) 191 | { 192 | sf.Description = $"{stringEntry.OriginalFilename}\n{stringEntry.ProductName}\n{stringEntry.ProductVersion}\n{stringEntry.FileDescription}"; 193 | } 194 | if (peFile.Resources.GroupIconDirectories != null) 195 | { 196 | try 197 | { 198 | foreach (var giDir in peFile.Resources.GroupIconDirectories) 199 | { 200 | foreach (var bestIconGi in giDir.DirectoryEntries.OrderByDescending(gi => gi.WBitCount).OrderByDescending(gi => gi.BWidth)) 201 | { 202 | foreach (var bestIcon in bestIconGi.AssociatedIcons(peFile)) 203 | { 204 | byte[] iconData = bestIcon.AsIco(); // returns either headless BMP or PNG 205 | sf.Icon = GetImageFromWindowsIconData(iconData); 206 | 207 | if (sf.Icon != null) break; 208 | } 209 | 210 | if (sf.Icon != null) break; 211 | } 212 | 213 | if (sf.Icon != null) break; 214 | } 215 | } 216 | catch (Exception ex) 217 | { 218 | Logger.Error($"Cannot set subfile icon from an icon group: {ex.Message}"); 219 | } 220 | } 221 | } 222 | 223 | if (sf.Icon == null) 224 | { 225 | foreach (var icon in peFile.Icons()) 226 | { 227 | sf.Icon = GetImageFromWindowsIconData(icon); 228 | 229 | if (sf.Icon != null) break; 230 | } 231 | } 232 | } 233 | catch (Exception ex) 234 | { 235 | Logger.Error($"Cannot parse PE executable: {ex.Message}"); 236 | } 237 | break; 238 | 239 | case ".bmp" or ".jpg" or ".jpeg" or ".png" or ".tif" or ".tiff" or ".png" or ".webp" or ".tga": 240 | try 241 | { 242 | target.Position = sf.StartOffset; 243 | byte[] imageBuf = new byte[sf.Length]; 244 | target.ReadExactly(imageBuf); 245 | sf.Icon = Image.Load(imageBuf); 246 | } 247 | catch (Exception ex) 248 | { 249 | Logger.Error($"Cannot read image subfile: {ex.Message}"); 250 | } 251 | break; 252 | } 253 | 254 | sf.Icon?.Mutate(ctx => ctx.Resize(0, 128)); 255 | 256 | return sf; 257 | } 258 | 259 | private static Image GetImageFromWindowsIconData(byte[] iconData) 260 | { 261 | ArgumentNullException.ThrowIfNull(iconData); 262 | 263 | if (iconData[0] == 0x89 && iconData[1] == 0x50) // PNG 264 | { 265 | return Image.Load(iconData); 266 | } 267 | 268 | var iconParser = new WindowsIconParser(); 269 | iconParser.Load(iconData); 270 | 271 | return Image.Load(iconParser.Entries.First().GetBitmap()); 272 | } 273 | 274 | internal static IEnumerable NearestNeighborResample(this IList input, int newSampleCount = 48000) 275 | { 276 | for (int i = 0; i < newSampleCount; i++) 277 | { 278 | double ratio = i / (double)newSampleCount; 279 | int srcIndex = (int)(ratio * input.Count); 280 | yield return input[srcIndex]; 281 | } 282 | } 283 | 284 | public static IEnumerable LinearResample(this IList input, int newSampleCount = 48000) 285 | { 286 | for (int i = 0; i < newSampleCount; i++) 287 | { 288 | double ratio = i / (double)newSampleCount; 289 | float srcIndex = (float)(ratio * input.Count); // e.g.: 4.75 290 | int srcIndexInt = (int)srcIndex; // 4 291 | float srcIndexDec = srcIndex - srcIndexInt; // 0.75 292 | yield return ((1 - srcIndexDec) * input[srcIndexInt]) + (srcIndexDec * input[(srcIndexInt + 1) % input.Count]); 293 | } 294 | } 295 | 296 | public static IEnumerable[] ToPlanar(this IEnumerable input, int channelCount = 2) 297 | { 298 | var ret = new IEnumerable[channelCount]; 299 | for (int i = 0; i < channelCount; i++) 300 | { 301 | int currentChannel = i; 302 | ret[i] = input.Where((_, sampleIndex) => 303 | { 304 | return sampleIndex % channelCount == currentChannel; 305 | }); 306 | } 307 | return ret; 308 | } 309 | 310 | public static IEnumerable ToPacked(this IList[] input) 311 | { 312 | var channelCount = input.Length; 313 | for (int i = 0; i < input[0].Count; i++) 314 | { 315 | for (int ch = 0; ch < channelCount; ch++) yield return input[ch][i]; 316 | } 317 | } 318 | 319 | public static int Align(this int input, int boundary) 320 | { 321 | return (input / boundary) * boundary; 322 | } 323 | 324 | public static long Align(this long input, int boundary) 325 | { 326 | return (input / boundary) * boundary; 327 | } 328 | 329 | public static float Align(this float input, int boundary) 330 | { 331 | return (int)(input / boundary) * boundary; 332 | } 333 | 334 | public static string GetBufferHexString(BinaryReader br, int count = 4) 335 | { 336 | var ofs = br.BaseStream.Position; 337 | 338 | var buf = br.ReadBytes(count); 339 | var bufHex = string.Join(' ', buf.Select(x => x.ToString("X2"))); 340 | var bufAscii = string.Join("", buf.Select(x => char.IsBetween((char)x, ' ', '\x7f') ? (char)x : '.')); 341 | string ret = $"[{br.BaseStream.Position:X12}] {bufHex} {bufAscii}"; 342 | 343 | br.BaseStream.Position = ofs; 344 | 345 | return ret; 346 | } 347 | 348 | public static IDictionary GetTypesWithAttribute(Assembly asm = null) where T : Attribute, new() 349 | { 350 | asm ??= Assembly.GetExecutingAssembly(); 351 | return asm.GetExportedTypes() 352 | .Where(t => t.GetCustomAttribute() != null) 353 | .ToDictionary(t => t.GetCustomAttribute()); 354 | } 355 | 356 | public static IEnumerable GetPropertiesWithAttribute(Assembly asm = null) where T : Attribute, new() 357 | { 358 | asm ??= Assembly.GetExecutingAssembly(); 359 | foreach (var type in asm.GetExportedTypes()) 360 | { 361 | foreach (var prop in type.GetProperties().Where(p => p.GetCustomAttribute() != null)) 362 | { 363 | yield return prop; 364 | } 365 | } 366 | } 367 | 368 | public static IEnumerable GetPropertiesWithAttribute(Type t) where T : Attribute, new() 369 | { 370 | return t.GetProperties().Where(p => p.GetCustomAttribute() != null); 371 | } 372 | 373 | public static PropertyInfo GetPropertyFromCliArgument(string argName) 374 | { 375 | return GetPropertiesWithAttribute() 376 | .Where(p => argName.Length == 2 ? p.GetCustomAttribute().ShortParameterName == argName[1] : p.GetCustomAttribute().LongParameterName == argName[2..]).FirstOrDefault(); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Extended Binary Waterfall 2 | ![GitHub Tag](https://img.shields.io/github/v/tag/unai-d/extended-binary-waterfall?style=flat-square&label=latest%20tag) 3 | 4 | This program **reads arbitrary computer files** as **raw audio and video streams**, resulting in what's sometimes known as a **binary waterfall**. The “extended” part of it is the inclusion of a **detailed walktrough** of the **fragments, chunks or subfiles** that the target file may have. 5 | 6 | > [!WARNING] 7 | > This program is still in development. 8 | > Some code is still untested, and errors are expected to happen when running this software. 9 | 10 | ## Dependencies 11 | 12 | - Required 13 | - .NET 9 SDK 14 | - It hasn't been tested with older versions but it is **probably compatible** with them. You can try lower the version manually in the `csproj` file. 15 | - Optional 16 | - FFmpeg libraries (for the FFmpeg exporter) 17 | - In Windows 10/11, use the following command to install FFmpeg: 18 | ```powershell 19 | winget install "FFmpeg (Shared)" 20 | ``` 21 | Once installed, **restart the command line** and make sure the `PATH` environment variable is updated with the FFmpeg libraries path. 22 | - Alternatively, you can manually download the libraries at [CODEX FFMPEG](https://www.gyan.dev/ffmpeg/builds/) (make sure to download the “shared” variant). 23 | Once downloaded, move the DLLs to a known path (e.g. `C:\ffmpeg`). 24 | - [Unifont](https://unifoundry.com/unifont/index.html) 25 | - Some Linux distros have the option to install this font via their respective package manager, but in Windows a manual download is required. 26 | - Make sure to install both the default font and the “upper” variant for emoticons. 27 | - [`wimlib`](https://wimlib.net/) for WIM file listings. 28 | - `minidump` Python module for Windows Minidump memory region parsing. 29 | 30 | > [!IMPORTANT] 31 | > When using Unifont, make sure to install the **TTF format** instead of OTF. 32 | > It seems that Unifont contains OTF CFF2 tables that makes the text rendering library throw an exception. 33 | > 34 | > You can **download** the TTF version from an [**unofficial repository**](https://github.com/multitheftauto/unifont) since it doesn't get officially released by Unifoundry as a TTF file anymore. 35 | > 36 | > See the relevant SixLabors Fonts [issue](https://github.com/SixLabors/Fonts/issues/331) and [pull request](https://github.com/SixLabors/Fonts/pull/342) for this specific problem. 37 | 38 | ## Build and Run 39 | 40 | Use `run.sh` to quickly (build if necessary, then) run the program. 41 | 42 | Alternatively, standard `dotnet build`/`dotnet run` commands apply: 43 | 44 | Use `dotnet build` from the repository's path, then execute `dotnet run --project Unai.ExtendedBinaryWaterfall.Cli` to run the program. 45 | 46 | ## Usage 47 | 48 | ### Quick Start 49 | 50 | The following commands will assume your command line working directory is located at the resulting binaries from the build process. 51 | If you're on Windows, the executable will be suffixed with `.exe`. 52 | 53 | Execute this command to get information about the arguments that can be used: 54 | 55 | ```sh 56 | Unai.ExtendedBinaryWaterfall.Cli --help 57 | ``` 58 | 59 | When using `run.sh`, the command can be simplified to: 60 | 61 | ```sh 62 | ./run.sh --help 63 | ``` 64 | 65 | ### Examples 66 | 67 | #### Example 1 68 | 69 | Read an ISO file and save the result to a video file called `result.mkv` (requires FFmpeg): 70 | 71 | ```sh 72 | Unai.ExtendedBinaryWaterfall.Cli /path/to/file.iso --exporter=ffmpeg --output=result.mkv 73 | ``` 74 | 75 | #### Example 2 76 | 77 | Read a GameMaker archive file and preview the result in an SDL window: 78 | 79 | ```sh 80 | Unai.ExtendedBinaryWaterfall.Cli /path/to/data.win 81 | ``` 82 | 83 | SDL is the default exporter if none is specified. 84 | 85 | #### Example 3 86 | 87 | Read a `.dll` file and preview the FFmpeg encoding result with standard output redirection: 88 | 89 | ```sh 90 | Unai.ExtendedBinaryWaterfall.Cli "C:\Windows\system32\shell32.dll" --exporter=ffmpeg | ffplay -f matroska - 91 | ``` 92 | 93 | When no `-o`/`--output` argument is specified, EBW will default to the standard output. 94 | 95 | > [!WARNING] 96 | > Some command line interfaces like PowerShell will require a proper standard I/O encoding suitable for binary streams. 97 | > Otherwise you will end up with “corrupted” files. 98 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/env bash 2 | dotnet run --project Unai.ExtendedBinaryWaterfall.Cli -- $* 3 | --------------------------------------------------------------------------------