Repository: maxim-zhao/SidWizPlus Branch: master Commit: d16d2affad02 Files: 63 Total size: 487.0 KB Directory structure: gitextract_65uui726/ ├── .editorconfig ├── .gitignore ├── .gitmodules ├── Benchmark.xlsx ├── Directory.Build.props ├── LICENSE ├── LibSidWiz/ │ ├── Channel.cs │ ├── Extensions.cs │ ├── LibSidWiz.csproj │ ├── Mixer.cs │ ├── MultiDumperWrapper.cs │ ├── MyColorConverter.cs │ ├── MyColorEditor.cs │ ├── Outputs/ │ │ ├── FfmpegOutput.cs │ │ ├── IGraphicsOutput.cs │ │ ├── PreviewOutput.cs │ │ ├── PreviewOutputForm.Designer.cs │ │ ├── PreviewOutputForm.cs │ │ └── PreviewOutputForm.resx │ ├── ProcessWrapper.cs │ ├── SampleBuffer.cs │ ├── Triggers/ │ │ ├── AutoCorrelationTrigger.cs │ │ ├── BiggestPositiveWaveAreaTrigger.cs │ │ ├── BiggestWaveAreaTrigger.cs │ │ ├── ITriggerAlgorithm.cs │ │ ├── MiddleWidest.cs │ │ ├── NullTrigger.cs │ │ ├── PeakSpeedTrigger.cs │ │ ├── RisingEdgeTrigger.cs │ │ └── WidestWaveTrigger.cs │ └── WaveformRenderer.cs ├── LibVgm/ │ ├── Gd3Tag.cs │ ├── LibVgm.csproj │ ├── OptionalGzipStream.cs │ └── VgmFile.cs ├── README.md ├── SidWiz/ │ ├── ColorButton.Designer.cs │ ├── ColorButton.cs │ ├── HighDpiHelper.cs │ ├── MultiDumperForm.Designer.cs │ ├── MultiDumperForm.cs │ ├── MultiDumperForm.resx │ ├── Program.cs │ ├── Properties/ │ │ ├── Resources.Designer.cs │ │ ├── Resources.resx │ │ └── app.manifest │ ├── SidPlayForm.Designer.cs │ ├── SidPlayForm.cs │ ├── SidPlayForm.resx │ ├── SidWizPlusGUI.Designer.cs │ ├── SidWizPlusGUI.cs │ ├── SidWizPlusGUI.csproj │ ├── SidWizPlusGUI.resx │ └── app.config ├── SidWiz.sln ├── SidWiz.sln.DotSettings ├── SidWizPlus/ │ ├── App.config │ ├── BackgroundRenderer.cs │ ├── ImageInfo.cs │ ├── Program.cs │ ├── SidWizPlus.csproj │ └── TextInfo.cs └── appveyor.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*.cs] # IDE0290: Use primary constructor dotnet_diagnostic.IDE0290.severity = none ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Typescript v1 declaration files typings/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ /SizWizPlus/Resources/ClientSecret.json ================================================ FILE: .gitmodules ================================================ [submodule "wiki"] path = wiki url = https://github.com/maxim-zhao/SidWizPlus.wiki.git branch = master ================================================ FILE: Directory.Build.props ================================================ latest true ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Maxim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LibSidWiz/Channel.cs ================================================ using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Design; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Forms.Design; using LibSidWiz.Triggers; using NAudio.Wave; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace LibSidWiz { /// /// Wraps a single "voice", and also deals with loading the data into memory /// [SuppressMessage("ReSharper", "UnusedMember.Global")] public class Channel: IDisposable { private readonly bool _autoReloadOnSettingChanged; private SampleBuffer _samples; private SampleBuffer _samplesForTrigger; private string _filename; private string _externalTriggerFilename; private ITriggerAlgorithm _algorithm; private int _triggerLookaheadFrames; // Default to current frame only private int _triggerLookaheadOnFailureFrames = 2; // Default to 2 frames ahead private Color _lineColor = Color.White; private string _label = ""; private float _lineWidth = 3; private float _scale = 1.0f; private int _viewWidthInSamples = 1500; private Color _fillColor = Color.Transparent; private float _zeroLineWidth; private Color _zeroLineColor = Color.Transparent; private Font _labelFont; private Color _labelColor = Color.Transparent; private Color _borderColor = Color.Transparent; private float _borderWidth; private ContentAlignment _labelAlignment = ContentAlignment.TopLeft; private Padding _labelMargins = new(0, 0, 0, 0); private bool _invertedTrigger; private bool _borderEdges = true; private Color _backgroundColor = Color.Transparent; private bool _clip; private Sides _side = Sides.Mix; private bool _smoothLines = true; private bool _filter; private bool _renderIfSilent; private double _fillBase; public Channel(bool autoReloadOnSettingChanged) { _autoReloadOnSettingChanged = autoReloadOnSettingChanged; } public enum Sides { Left, Right, Mix } public event Action Changed; public Task LoadDataAsync(CancellationToken token = new()) { return Task.Factory.StartNew(() => { try { ErrorMessage = ""; if (string.IsNullOrEmpty(Filename)) { _samples = null; SampleCount = 0; Max = 0; Length = TimeSpan.Zero; Loading = false; IsEmpty = true; return false; } IsEmpty = false; Loading = true; Console.WriteLine($"- Reading {Filename}"); _samples = new SampleBuffer(Filename, Side, HighPassFilter); SampleRate = _samples.SampleRate; Length = _samples.Length; token.ThrowIfCancellationRequested(); _samples.Analyze(); SampleCount = _samples.Count; token.ThrowIfCancellationRequested(); Max = Math.Max(Math.Abs(_samples.Max), Math.Abs(_samples.Min)); // This is a bit arbitrary. Nuked OPLL emits a DC offset of about 0.0008..0.0018 for an unused channel, // and a fade out (or low pass filter) will pull that down to 0. Console.WriteLine($"- Sample range for {Filename} is {_samples.Min}..{_samples.Max} = range {Math.Abs(_samples.Max - _samples.Min)}"); IsSilent = Math.Abs(_samples.Max - _samples.Min) < SilenceThreshold; // Point at the same SampleBuffer _samplesForTrigger = string.IsNullOrEmpty(ExternalTriggerFilename) ? _samples : new SampleBuffer(ExternalTriggerFilename, Side, HighPassFilter); Loading = false; return true; } catch (TaskCanceledException) { // Blank out if cancelled Max = 0; SampleRate = 0; Length = TimeSpan.Zero; if (_samplesForTrigger != _samples) { _samplesForTrigger?.Dispose(); } _samplesForTrigger = null; _samples?.Dispose(); _samples = null; Loading = false; return false; } catch (Exception ex) { ErrorMessage = ex.ToString(); Max = 0; SampleRate = 0; Length = TimeSpan.Zero; if (_samplesForTrigger != _samples) { _samplesForTrigger?.Dispose(); } _samplesForTrigger = null; _samples?.Dispose(); _samples = null; Loading = false; return false; } finally { Changed?.Invoke(this, false); } }, token); } [Category("Data")] [Description("The full text of any error message when loading the file")] [JsonIgnore] public string ErrorMessage { get; private set; } [Category("Data")] [Editor(typeof(FileNameEditor), typeof(UITypeEditor))] [Description("The filename to be rendered")] public string Filename { get => _filename; set { bool needReload = value != _filename; _filename = value; Changed?.Invoke(this, needReload); if (_filename != "" && string.IsNullOrEmpty(_label)) { Label = GuessNameFromMultidumperFilename(_filename); } } } [Category("Triggering")] [Editor(typeof(FileNameEditor), typeof(UITypeEditor))] [Description("The filename to use for oscilloscope triggering. Leave blank to use the channel's sound data.")] public string ExternalTriggerFilename { get => _externalTriggerFilename; set { bool needReload = value != _externalTriggerFilename; _externalTriggerFilename = value; // Change algorithm to RisingEdgeTrigger when using an external trigger _algorithm = new RisingEdgeTrigger(); Changed?.Invoke(this, needReload); } } [Category("Data")] [Description("The channel to use from the file (if stereo)")] public Sides Side { get => _side; set { bool needReload = value != _side; _side = value; Changed?.Invoke(this, needReload); if (_autoReloadOnSettingChanged) { LoadDataAsync(); } } } [Category("Data")] [Description("If enabled, high pass filtering will be used to remove DC offsets")] public bool HighPassFilter { get => _filter; set { bool needReload = value != _filter; _filter = value; Changed?.Invoke(this, needReload); if (_autoReloadOnSettingChanged) { LoadDataAsync(); } } } [Category("Data")] [Description("The amplitude range at which a channel is considered silent")] public float SilenceThreshold { get; set; } = 0.01f; [Category("Triggering")] [Description("The algorithm to use for rendering")] [TypeConverter(typeof(TriggerAlgorithmTypeConverter))] [JsonConverter(typeof(TriggerAlgorithmJsonConverter))] public ITriggerAlgorithm Algorithm { get => _algorithm; set { _algorithm = value; Changed?.Invoke(this, false); } } [Category("Triggering")] [Description("How many frames to allow the triggering algorithm to look ahead. Zero means only look within the current frame. Set to larger numbers to support sync to low frequencies, but too large numbers can cause erroneous matches.")] public int TriggerLookaheadFrames { get => _triggerLookaheadFrames; set { _triggerLookaheadFrames = value; Changed?.Invoke(this, false); } } [Category("Triggering")] [Description("How many frames to allow the triggering algorithm to look ahead, when nothing is found with the default lookahead.")] public int TriggerLookaheadOnFailureFrames { get => _triggerLookaheadOnFailureFrames; set { _triggerLookaheadOnFailureFrames = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The line colour")] [Editor(typeof(MyColorEditor), typeof(UITypeEditor))] [TypeConverter(typeof(MyColorConverter))] public Color LineColor { get => _lineColor; set { _lineColor = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The line width, in pixels. Fractional values are supported.")] public float LineWidth { get => _lineWidth; set { _lineWidth = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The fill colour. Set to transparent to have no fill.")] [Editor(typeof(MyColorEditor), typeof(UITypeEditor))] [TypeConverter(typeof(MyColorConverter))] public Color FillColor { get => _fillColor; set { _fillColor = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The base of the fill. Set to 0 for the centre line, -1 to fill from the bottom and 1 for the top. Other values also work.")] public double FillBase { get => _fillBase; set { _fillBase = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("Whether to draw lines pixelated (false) or anti-aliased (true)")] public bool SmoothLines { get => _smoothLines; set { _smoothLines = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The width of the zero line")] public float ZeroLineWidth { get => _zeroLineWidth; set { _zeroLineWidth = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The color of the zero line")] [Editor(typeof(MyColorEditor), typeof(UITypeEditor))] [TypeConverter(typeof(MyColorConverter))] public Color ZeroLineColor { get => _zeroLineColor; set { _zeroLineColor = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The color of the border")] [Editor(typeof(MyColorEditor), typeof(UITypeEditor))] [TypeConverter(typeof(MyColorConverter))] public Color BorderColor { get => _borderColor; set { _borderColor = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The width of the border")] public float BorderWidth { get => _borderWidth; set { _borderWidth = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("Whether to draw the outer edges of any border boxes")] public bool BorderEdges { get => _borderEdges; set { _borderEdges = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("A background colour for the channel. This is layered above any background image, and can be transparent.")] [Editor(typeof(MyColorEditor), typeof(UITypeEditor))] [TypeConverter(typeof(MyColorConverter))] public Color BackgroundColor { get => _backgroundColor; set { _backgroundColor = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The label for the channel")] [Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))] public string Label { get => _label; set { _label = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The font for the channel label")] public Font LabelFont { get => _labelFont; set { _labelFont = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The color for the channel label")] [Editor(typeof(MyColorEditor), typeof(UITypeEditor))] [TypeConverter(typeof(MyColorConverter))] public Color LabelColor { get => _labelColor; set { _labelColor = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The alignment for the channel label")] public ContentAlignment LabelAlignment { get => _labelAlignment; set { _labelAlignment = value; Changed?.Invoke(this, false); } } [Category("Appearance")] [Description("The margins for the chanel label")] public Padding LabelMargins { get => _labelMargins; set { _labelMargins = value; Changed?.Invoke(this, false); } } [Category("Adjustment")] [Description("Vertical scaling. This may be set by the auto-scaler.")] public float Scale { get => _scale; set { _scale = value; Changed?.Invoke(this, false); } } [Category("Adjustment")] [Description("Whether to constrain the waveform to its screen area when scaled past 100%")] public bool Clip { get => _clip; set { _clip = value; Changed?.Invoke(this, false); } } [Category("Adjustment")] [Description("View width, in ms")] [JsonIgnore] public float ViewWidthInMilliseconds { get => SampleRate == 0 ? 0 : (float)_viewWidthInSamples * 1000 / SampleRate; set { _viewWidthInSamples = (int) (value / 1000 * SampleRate); Changed?.Invoke(this, false); } } [Category("Adjustment")] [Description("View width, in samples")] public int ViewWidthInSamples { get => _viewWidthInSamples; set { _viewWidthInSamples = value; Changed?.Invoke(this, false); } } [Category("Triggering")] [Description("Set to true to trigger in the opposite direction")] // ReSharper disable once MemberCanBePrivate.Global public bool InvertedTrigger { get => _invertedTrigger; set { _invertedTrigger = value; Changed?.Invoke(this, false); } } [Category("Data")] [Description("Peak amplitude for the channel")] [JsonIgnore] public float Max { get; private set; } [Browsable(false)] [JsonIgnore] public long SampleCount { get; private set; } [Category("Data")] [Description("Duration of the channel")] [JsonIgnore] public TimeSpan Length { get; private set; } [Category("Data")] [Description("Sampling rate of the channel")] [JsonIgnore] public int SampleRate { get; private set; } [Category("Appearance")] [Description("Whether to render silent channels normally. If false, a warning message is shown instead.")] public bool RenderIfSilent { get => _renderIfSilent; set { _renderIfSilent = value; Changed?.Invoke(this, false); } } // ReSharper disable once CompareOfFloatsByEqualityOperator [Browsable(false)] [JsonIgnore] public bool IsSilent { get; private set; } [Browsable(false)] [JsonIgnore] public bool Loading { get; private set; } = true; [Browsable(false)] [JsonIgnore] public bool IsEmpty { get; private set; } [Browsable(false)] [JsonIgnore] internal Rectangle Bounds { get; set; } internal float GetSample(int sampleIndex, bool forTrigger = true) { var source = forTrigger ? _samplesForTrigger : _samples; return sampleIndex < 0 || sampleIndex >= source.Count ? 0 : source[sampleIndex] * Scale * (forTrigger && InvertedTrigger ? -1 : 1); } internal int GetTriggerPoint(int frameIndexSamples, int frameSamples, int previousTriggerPoint) { // Try at default settings var result = Algorithm.GetTriggerPoint(this, frameIndexSamples, frameIndexSamples + frameSamples * (TriggerLookaheadFrames + 1), previousTriggerPoint); if (result < frameIndexSamples) { // Try again result = Algorithm.GetTriggerPoint(this, frameIndexSamples, frameIndexSamples + frameSamples * (TriggerLookaheadOnFailureFrames + 1), previousTriggerPoint); } if (result < frameIndexSamples) { // Default on failure result = frameIndexSamples; } return result; } public static string GuessNameFromMultidumperFilename(string filename) { var namePart = Path.GetFileNameWithoutExtension(filename); try { if (namePart == null) { return filename; } var index = namePart.IndexOf(" - YM2413 #", StringComparison.Ordinal); if (index > -1) { index = int.Parse(namePart.Substring(index + 11)); if (index < 9) { return $"YM2413 Tone {index + 1}"; } switch (index) { case 9: return "YM2413 Bass Drum"; case 10: return "YM2413 Snare Drum"; case 11: return "YM2413 Tom-Tom"; case 12: return "YM2413 Cymbal"; case 13: return "YM2413 Hi-Hat"; } } index = namePart.IndexOf(" - SEGA PSG #", StringComparison.Ordinal); if (index > -1) { if (int.TryParse(namePart.Substring(index + 13), out index)) { switch (index) { case 0: case 1: case 2: return $"Sega PSG Square {index + 1}"; case 3: return "Sega PSG Noise"; } } } index = namePart.IndexOf(" - SN76489 #", StringComparison.Ordinal); if (index > -1) { if (int.TryParse(namePart.Substring(index + 12), out index)) { switch (index) { case 0: case 1: case 2: return $"SN76489 Square {index + 1}"; case 3: return "SN76489 Noise"; } } } // Guess it's the bit after the last " - " index = namePart.LastIndexOf(" - ", StringComparison.Ordinal); if (index > -1) { return namePart.Substring(index + 3); } } catch (Exception ex) { Console.Error.WriteLine($"Error guessing channel name for {filename}: {ex}"); } // Default to just the filename return namePart; } /// /// This allows us to use a property grid to select a trigger algorithm /// // ReSharper disable once MemberCanBePrivate.Global public class TriggerAlgorithmTypeConverter: StringConverter { public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) { return true; } public override bool GetStandardValuesSupported(ITypeDescriptorContext context) { return true; } public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) { return new StandardValuesCollection( Assembly.GetExecutingAssembly() .GetTypes() .Where(t => typeof(ITriggerAlgorithm).IsAssignableFrom(t) && t != typeof(ITriggerAlgorithm)) .Select(t => t.Name) .ToList()); } public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); } public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { var type = Assembly.GetExecutingAssembly() .GetTypes() .FirstOrDefault(t => typeof(ITriggerAlgorithm).IsAssignableFrom(t) && t.Name.ToLowerInvariant().Equals(value.ToString().ToLowerInvariant())); if (type != null) { return Activator.CreateInstance(type) as ITriggerAlgorithm; } } return base.ConvertFrom(context, culture, value); } } // ReSharper disable once MemberCanBePrivate.Global public class TriggerAlgorithmJsonConverter: JsonConverter { public override void WriteJson(JsonWriter writer, ITriggerAlgorithm value, JsonSerializer serializer) { writer.WriteValue(value.GetType().Name); } public override ITriggerAlgorithm ReadJson(JsonReader reader, Type objectType, ITriggerAlgorithm existingValue, bool hasExistingValue, JsonSerializer serializer) { var type = Assembly.GetExecutingAssembly() .GetTypes() .FirstOrDefault(t => typeof(ITriggerAlgorithm).IsAssignableFrom(t) && t.Name.ToLowerInvariant().Equals(reader.Value?.ToString().ToLowerInvariant())); if (type != null) { return Activator.CreateInstance(type) as ITriggerAlgorithm; } return existingValue; } } public void Dispose() { _samples?.Dispose(); if (_samplesForTrigger != _samples) { _samplesForTrigger.Dispose(); } _labelFont?.Dispose(); } public string ToJson() { return JsonConvert.SerializeObject(this, new JsonSerializerSettings { Formatting = Formatting.Indented, }); } public void FromJson(string json, bool preserveSource) { if (preserveSource) { JsonConvert.PopulateObject(json, this, new JsonSerializerSettings { ContractResolver = new PreservingContractResolver() }); } else { JsonConvert.PopulateObject(json, this); } } private class PreservingContractResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); if (property.PropertyName == nameof(Filename) || property.PropertyName == nameof(Label) || property.PropertyName == nameof(ExternalTriggerFilename)) { property.Ignored = true; } return property; } } public bool IsMono() { if (Side == Sides.Left || Side == Sides.Right) { return true; } using var reader = new WaveFileReader(_filename); var sp = reader.ToSampleProvider().ToStereo(); if (sp.WaveFormat.Channels == 1) { return true; } int bufferSize = sp.WaveFormat.SampleRate * 10; var buffer = new float[bufferSize]; sp.Read(buffer, 0, bufferSize); for (int i = 0; i < bufferSize; i += 2) { // ReSharper disable once CompareOfFloatsByEqualityOperator if (buffer[i] != buffer[i + 1]) { return false; } } return true; } } } ================================================ FILE: LibSidWiz/Extensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace LibSidWiz { public static class Extensions { public static IOrderedEnumerable OrderByAlphaNumeric(this IEnumerable source, Func selector) { // Materialise the collection if necessary var list = source.ToList(); // Find the longest sequence of digits var max = list .SelectMany(i => Regex .Matches(selector(i), @"\d+") .Cast() .Select(m => (int?)m.Value.Length)) .Max() ?? 0; // Pad all number sequences to that length, then order by this padded string return list.OrderBy(i => Regex.Replace(selector(i), @"\d+", m => m.Value.PadLeft(max, '0'))); } } } ================================================ FILE: LibSidWiz/LibSidWiz.csproj ================================================  net48 true true latest true ..\NReplayGain.dll ================================================ FILE: LibSidWiz/Mixer.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using NAudio.Wave; using NAudio.Wave.SampleProviders; using NReplayGain; namespace LibSidWiz { /// /// Deals with mixing audio to a "master file" /// public static class Mixer { public static void MixToFile(IList channels, string filename, bool applyReplayGain) { Console.WriteLine("Mixing per-channel data..."); // We make new readers... var readers = new List(); try { readers.AddRange(channels .Where(c => !string.IsNullOrEmpty(c.Filename)) .Select(c => new WaveFileReader(c.Filename))); if (applyReplayGain) { Console.WriteLine("Computing ReplayGain..."); // We read it in a second at a time, to calculate Replay Gains var mixer = new MixingSampleProvider(readers.Select(x => x.ToSampleProvider().ToStereo())); const int sampleRate = 44100; var resampler = new WdlResamplingSampleProvider(mixer, sampleRate); // We use a 1s buffer... var buffer = new float[sampleRate * 2]; // *2 for stereo var replayGain = new TrackGain(sampleRate); for (;;) { int numRead = resampler.Read(buffer, 0, buffer.Length); if (numRead == 0) { break; } // And analyze replayGain.AnalyzeSamples(buffer, numRead); } // The +3 is to make it at "YouTube loudness", which is a lot louder than ReplayGain defaults to. // TODO make this configurable? var gain = replayGain.GetGain() + 3; Console.WriteLine($"Applying ReplayGain ({gain:N} dB) and saving to {filename}"); // Reset the readers foreach (var reader in readers) { reader.Position = 0; } // We make a new mixer just in case resetting the previous one is problematic... mixer = new MixingSampleProvider(readers.Select(x => x.ToSampleProvider().ToStereo())); var amplifier = new VolumeSampleProvider(mixer) {Volume = (float) Math.Pow(10, gain / 20)}; WaveFileWriter.CreateWaveFile(filename, amplifier.ToWaveProvider()); } else { var mixer = new MixingSampleProvider(readers.Select(x => x.ToSampleProvider().ToStereo())); WaveFileWriter.CreateWaveFile(filename, mixer.ToWaveProvider()); } } finally { foreach (var reader in readers) { reader.Dispose(); } } } } } ================================================ FILE: LibSidWiz/MultiDumperWrapper.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace LibSidWiz { public class MultiDumperWrapper: IDisposable { private readonly string _multiDumperPath; private readonly int _samplingRate; private readonly int _loopCount; private readonly int _fadeMs; private readonly int _gapMs; private readonly string _extraOptions; private ProcessWrapper _processWrapper; private readonly HashSet _allowedParameters; public MultiDumperWrapper(string multiDumperPath, int samplingRate, int loopCount, int fadeMs, int gapMs, string extraOptions) { _multiDumperPath = multiDumperPath; _samplingRate = samplingRate; _loopCount = loopCount; _fadeMs = fadeMs; _gapMs = gapMs; _extraOptions = extraOptions; // We parse the usage info first to check for allowed parameters, in the form --name or --name= var helpText = GetOutputText("", true); _allowedParameters = [ ..Regex.Matches(helpText, "--[^= ]+") .Cast() .Select(x => x.Value) ]; } public class Song { // ReSharper disable UnusedAutoPropertyAccessor.Global public string Name { get; set; } public string Author { get; set; } public string Comment { get; set; } public string Copyright { get; set; } public string Dumper { get; set; } public string Game { get; set; } public string System { get; set; } public List Channels { get; set; } public int Index { get; set; } public string Filename { get; set; } public TimeSpan IntroLength { get; set; } public TimeSpan LoopLength { get; set; } // ReSharper restore UnusedAutoPropertyAccessor.Global public int LoopCount { get; set; } public TimeSpan ForceLength { get; set; } = TimeSpan.Zero; public override string ToString() { var length = GetLength(); var sb = new StringBuilder(); sb.Append($"#{Index}: ") .Append(string.IsNullOrWhiteSpace(Game) ? "Unknown game" : Game) .Append(" - ") .Append(string.IsNullOrWhiteSpace(Name) ? "Unknown title" : Name) .Append(" - ") .Append(string.IsNullOrWhiteSpace(Author) ? "Unknown author" : Author) .Append(string.IsNullOrWhiteSpace(Comment) ? "" : $" ({Comment})") .Append(length <= TimeSpan.Zero ? " (Unknown length)" : $" ({length})"); return sb.ToString(); } public TimeSpan GetLength() { if (ForceLength > TimeSpan.Zero) { return ForceLength; } var length = TimeSpan.Zero; if (IntroLength > TimeSpan.Zero) { length += IntroLength; } if (LoopLength > TimeSpan.Zero) { length += TimeSpan.FromTicks(LoopLength.Ticks * LoopCount); } return length; } } public IEnumerable GetSongs(string filename) { filename = Path.GetFullPath(filename); if (!File.Exists(filename)) { throw new FileNotFoundException("Cannot find VGM file", filename); } // Don't check the --json parameter as it may not have mentioned it for an old build var json = GetOutputText($"\"{filename}\" --json", false); if (string.IsNullOrEmpty(json)) { throw new Exception("Failed to get song data from MultiDumper"); } // Extract metadata // Example result: // { // "channels":[ // "SEGA PSG #0","SEGA PSG #1","SEGA PSG #2","SEGA PSG #3", // "YM2413 #0","YM2413 #1","YM2413 #2","YM2413 #3","YM2413 #4","YM2413 #5","YM2413 #6", // "YM2413 #7","YM2413 #8","YM2413 #9","YM2413 #10","YM2413 #11","YM2413 #12","YM2413 #13"], // "containerinfo": // { // "copyright":"1988/08/14", // "dumper":"sherpa", // "game":"Golvellius - Valley of Doom", // "system":"Sega Master System" // }, // "songs":[ // { // "author":"Masatomo Miyamoto, Takeshi Santo, Shin-kun, Pazu", // "comment":"", // "name":"Title Screen", // "length":"12345" // }], // "subsongCount":1 // } // or: // { "error": "some error message" } dynamic metadata = JsonConvert.DeserializeObject(json) ?? throw new Exception("Failed to parse song metadata"); if (metadata["error"] != null) { throw new Exception($"Failed to parse song metadata: {metadata.error}"); } var channels = metadata.channels.ToObject>(); var songs = (JArray)metadata.songs; var i = 0; return songs.Cast().Select(s => new Song { Filename = filename, Index = i++, Name = Clean(s.name), Author = Clean(s.author), Channels = channels, Comment = Clean(s.comment), Copyright = Clean(metadata.containerinfo.copyright), Dumper = Clean(metadata.containerinfo.dumper), Game = Clean(metadata.containerinfo.game), System = Clean(metadata.containerinfo.system), IntroLength = TimeSpan.FromMilliseconds((int)(s.intro_length ?? 0)), LoopLength = TimeSpan.FromMilliseconds((int)(s.loop_length ?? 0)), LoopCount = _loopCount }); // This helps us reject any junk strings MultiDumper gives us for empty tags static string Clean(string s) => string.IsNullOrEmpty(s) || s.Any(char.IsControl) ? string.Empty : s; } private string GetOutputText(string args, bool includeStdErr) { using var p = new ProcessWrapper( _multiDumperPath, args, includeStdErr); string text = string.Join("", p.Lines()); // Try to decode any UTF-8 in there try { text = Encoding.UTF8.GetString(Encoding.Default.GetBytes(text)); } catch (Exception) { // Ignore it, use unfixed string } return text; } public IEnumerable Dump(Song song, Action onProgress) { var args = new StringBuilder($"\"{song.Filename}\" {song.Index}"); AddArgIfSupported(args, "sampling_rate", _samplingRate); AddArgIfSupported(args, "fade_length", _fadeMs); AddArgIfSupported(args, "loop_count", _loopCount); AddArgIfSupported(args, "gap_length", _gapMs); if (song.ForceLength > TimeSpan.Zero) { AddArgIfSupported(args, "play_length", (long)song.ForceLength.TotalMilliseconds); } if (!string.IsNullOrEmpty(_extraOptions)) { args.Append($" {_extraOptions}"); } _processWrapper = new ProcessWrapper( _multiDumperPath, args.ToString(), showConsole:true); var progressParts = Enumerable.Repeat(0.0, song.Channels.Count).ToList(); var r = new Regex(@"(?\d+)\|(?\d+)\|(?\d+)"); var stopwatch = Stopwatch.StartNew(); foreach (var match in _processWrapper.Lines().Select(l => r.Match(l)).Where(m => m.Success)) { var channel = Convert.ToInt32(match.Groups["channel"].Value); if (channel < 0 || channel > song.Channels.Count) { continue; } var position = Convert.ToDouble(match.Groups["position"].Value); var total = Convert.ToDouble(match.Groups["total"].Value); progressParts[channel] = position / total; if (stopwatch.Elapsed.TotalMilliseconds > 100) { // Update the progress every 100ms onProgress?.Invoke(progressParts.Average()); stopwatch.Restart(); } } _processWrapper.Dispose(); _processWrapper = null; onProgress?.Invoke(1.0); var baseName = Path.Combine( Path.GetDirectoryName(song.Filename) ?? "", Path.GetFileNameWithoutExtension(song.Filename)); return song.Channels.Select(channel => $"{baseName} - {channel}.wav"); } private void AddArgIfSupported(StringBuilder args, string name, object value) { if (_allowedParameters.Contains($"--{name}")) { args.Append($" --{name}={value}"); } else { Console.Error.WriteLine($"Arg not supported: {name}"); } } public void Dispose() { _processWrapper?.Dispose(); } } } ================================================ FILE: LibSidWiz/MyColorConverter.cs ================================================ using System.ComponentModel; using System.Drawing; namespace LibSidWiz; /// /// This overrides ColorConverter which interferes with the colour editor. /// public class MyColorConverter : ColorConverter { public override bool GetStandardValuesSupported(ITypeDescriptorContext context) { return false; } } ================================================ FILE: LibSidWiz/MyColorEditor.cs ================================================ using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Design; using System.Windows.Forms; using System.Windows.Forms.Design; namespace LibSidWiz; /// /// This allows us to implement a custom colour picker /// public class MyColorEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) { return UITypeEditorEditStyle.Modal; } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { if (value is not Color c || provider == null) { return value; } var svc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); if (svc == null) { return c; } using var form = new Cyotek.Windows.Forms.ColorPickerDialog(); form.Color = c; form.ShowAlphaChannel = true; return svc.ShowDialog(form) == DialogResult.OK ? form.Color : c; } public override bool GetPaintValueSupported(ITypeDescriptorContext context) { return true; } public override void PaintValue(PaintValueEventArgs e) { // This is just for the little rectangle on the left // TODO: indicate transparency? var color = (Color)e.Value; if (color.A < 255) { // Draw a checkerboard of 4x4 using the system colours e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds); for (var x = 0; x < e.Bounds.Width; x += 4) for (var y = 0; y < e.Bounds.Height; y += 4) if (((x/4 ^ y/4) & 1) == 0) { e.Graphics.FillRectangle(SystemBrushes.WindowText, x, y, 4, 4); } } using (var brush = new SolidBrush(color)) { e.Graphics.FillRectangle(brush, e.Bounds); } e.Graphics.DrawRectangle(SystemPens.WindowText, e.Bounds); } } ================================================ FILE: LibSidWiz/Outputs/FfmpegOutput.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Text; using SkiaSharp; namespace LibSidWiz.Outputs { public class FfmpegOutput : IGraphicsOutput { private readonly bool _throwStandardError; private readonly Process _process; private readonly BinaryWriter _writer; public FfmpegOutput(string pathToExe, string filename, int width, int height, int fps, string extraArgs, string masterAudioFilename, string videoCodec, string audioCodec, bool throwStandardError) { _throwStandardError = throwStandardError; // Build the FFMPEG commandline var arguments = "-y -hide_banner"; // Overwrite, don't show banner at startup // Audio input if (File.Exists(masterAudioFilename)) { arguments += $" -i \"{masterAudioFilename}\""; } // Video input arguments += $" -f rawvideo -pixel_format bgr0 -video_size {width}x{height} -framerate {fps} -i pipe:"; // Audio output arguments += $" -codec:a {audioCodec}"; // Video output arguments += $" -codec:v {videoCodec} -movflags +faststart"; // Extra args arguments += $" {extraArgs} \"{filename}\""; Console.WriteLine($"Starting FFMPEG: {pathToExe} {arguments}"); // We don't want a BOM to be injected if the system code page is set to UTF-8. // This fails sometimes, so we swallow the error... try { Console.InputEncoding = Encoding.ASCII; } catch (Exception e) { Console.WriteLine($"Failed to change console encoding to ASCII. You may get video corruption. Exception said: {e.Message}"); } // Start it up _process = Process.Start( new ProcessStartInfo { FileName = pathToExe, Arguments = arguments, UseShellExecute = false, RedirectStandardInput = true, RedirectStandardError = throwStandardError, RedirectStandardOutput = false, CreateNoWindow = throwStandardError // makes it inline in console mode } ); if (_process == null) { throw new Exception($"Couldn't start FFMPEG with commandline {pathToExe} {arguments}"); } _writer = new BinaryWriter(_process.StandardInput.BaseStream); } public void Write(SKImage _, byte[] data, double __, TimeSpan ___) { try { _writer.Write(data); } catch (Exception e) { if (_throwStandardError) { if (_process.HasExited) { throw new Exception( $""" FFMPEG has exited unexpectedly. Exit code was {_process.ExitCode}. Standard error was: {_process.StandardError.ReadToEnd()} """, e); } throw new Exception( $""" Cannot write to FFMPEG. Standard error was: {_process.StandardError.ReadToEnd()} """, e); } throw; } } public void Dispose() { // This triggers a shutdown _process?.StandardInput.BaseStream.Close(); if (_throwStandardError) { _process?.StandardError.Close(); } // And we wait for it to finish... _process?.WaitForExit(); _process?.Dispose(); _writer?.Dispose(); } } } ================================================ FILE: LibSidWiz/Outputs/IGraphicsOutput.cs ================================================ using System; using SkiaSharp; namespace LibSidWiz.Outputs { public interface IGraphicsOutput: IDisposable { void Write(SKImage image, byte[] data, double fractionComplete, TimeSpan length); } } ================================================ FILE: LibSidWiz/Outputs/PreviewOutput.cs ================================================ using System; using System.Diagnostics; using System.Windows.Forms; using Microsoft.WindowsAPICodePack.Taskbar; using SkiaSharp; using SkiaSharp.Views.Desktop; namespace LibSidWiz.Outputs { public class PreviewOutput : IGraphicsOutput { private readonly int _frameSkip; private readonly bool _pumpMessageQueue; private readonly PreviewOutputForm _form; private int _frameIndex; private readonly Stopwatch _stopwatch; private TimeSpan _lastFpsUpdateTime = TimeSpan.Zero; public PreviewOutput(int frameSkip, bool pumpMessageQueue = false) { _frameSkip = frameSkip; _pumpMessageQueue = pumpMessageQueue; _form = new PreviewOutputForm(); _form.Show(); _form.SetDesktopLocation(0, 0); _stopwatch = Stopwatch.StartNew(); } public void Write(SKImage image, byte[] data, double fractionComplete, TimeSpan length) { if (!_form.Visible) { throw new Exception("Preview window closed"); } // Post-increment so we take frame 0 var showFrame = _frameIndex++ % _frameSkip == 0; var showFps = showFrame || (_stopwatch.Elapsed - _lastFpsUpdateTime).TotalMilliseconds > 100; if (showFps) { var elapsedSeconds = _stopwatch.Elapsed.TotalSeconds; var fps = _frameIndex / elapsedSeconds; var eta = TimeSpan.FromSeconds(elapsedSeconds / fractionComplete - elapsedSeconds); _form.BeginInvoke(new Action(() => { if (_form.IsDisposed || !_form.Visible) { return; } _form.toolStripStatusLabel2.Text = $"{fractionComplete:P} of {length} @ {fps:F}fps, ETA {eta:g}"; TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Normal, _form.Handle); TaskbarManager.Instance.SetProgressValue((int)(fractionComplete * 100), 100, _form.Handle); })); _lastFpsUpdateTime = _stopwatch.Elapsed; } if (showFrame) { // Copy the bitmap for use on the GUI thread var copy = image.ToBitmap(); _form.BeginInvoke(new Action(() => { if (_form.IsDisposed || !_form.Visible) { return; } _form.pictureBox1.Image = copy; })); } if (_pumpMessageQueue) { Application.DoEvents(); } } public void Dispose() { _stopwatch.Stop(); try { _form?.BeginInvoke(() => { _form?.Close(); _form?.Dispose(); } ); } catch (Exception) { // We might get this if exiting the program } } } } ================================================ FILE: LibSidWiz/Outputs/PreviewOutputForm.Designer.cs ================================================ namespace LibSidWiz.Outputs { partial class PreviewOutputForm { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(PreviewOutputForm)); this.pictureBox1 = new System.Windows.Forms.PictureBox(); this.statusStrip1 = new System.Windows.Forms.StatusStrip(); this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel(); this.toolStripStatusLabel2 = new System.Windows.Forms.ToolStripStatusLabel(); ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); this.statusStrip1.SuspendLayout(); this.SuspendLayout(); // // pictureBox1 // this.pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill; this.pictureBox1.Location = new System.Drawing.Point(0, 0); this.pictureBox1.Name = "pictureBox1"; this.pictureBox1.Size = new System.Drawing.Size(368, 103); this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize; this.pictureBox1.TabIndex = 3; this.pictureBox1.TabStop = false; // // statusStrip1 // this.statusStrip1.ImageScalingSize = new System.Drawing.Size(32, 32); this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.toolStripStatusLabel1, this.toolStripStatusLabel2}); this.statusStrip1.Location = new System.Drawing.Point(0, 103); this.statusStrip1.Name = "statusStrip1"; this.statusStrip1.Size = new System.Drawing.Size(368, 22); this.statusStrip1.TabIndex = 4; this.statusStrip1.Text = "Ready!"; // // toolStripStatusLabel1 // this.toolStripStatusLabel1.Name = "toolStripStatusLabel1"; this.toolStripStatusLabel1.Size = new System.Drawing.Size(0, 17); // // toolStripStatusLabel2 // this.toolStripStatusLabel2.Name = "toolStripStatusLabel2"; this.toolStripStatusLabel2.Size = new System.Drawing.Size(39, 17); this.toolStripStatusLabel2.Text = "Ready"; // // PreviewOutputForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoSize = true; this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.ClientSize = new System.Drawing.Size(368, 125); this.Controls.Add(this.pictureBox1); this.Controls.Add(this.statusStrip1); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.Name = "PreviewOutputForm"; ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); this.statusStrip1.ResumeLayout(false); this.statusStrip1.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); } #endregion public System.Windows.Forms.PictureBox pictureBox1; private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1; public System.Windows.Forms.StatusStrip statusStrip1; public System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel2; } } ================================================ FILE: LibSidWiz/Outputs/PreviewOutputForm.cs ================================================ using System.Windows.Forms; namespace LibSidWiz.Outputs { public partial class PreviewOutputForm : Form { public PreviewOutputForm() { InitializeComponent(); } } } ================================================ FILE: LibSidWiz/Outputs/PreviewOutputForm.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 17, 17 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 //+trf//ra3//62t//+trf//UlL//wAAAP8Ara3/AK2t/wAAAP//AAAA/wAAAP8AAAAA////AK2t/wD/ //8A////AP///wD///8Ara3/AP///wD///8A////AP///wCtrf8A////AP///wD///8A////AK2t/wD/ //8A////AP///wD///8AAAD/AP///wD///8A////AP///wCtrf8A////AP///wD///8AAAD/UlL//62t //+trf//ra3//62t//+trf//ra3//62t//9SUv//AAAA/wCtrf8A////AK2t/wAAAP//AAAA/wAAAP8A AAAA////AK2t/wD///8A////AP///wD///8Ara3/AP///wD///8A////AP///wCtrf8A////AP///wD/ //8A////AK2t/wD///8A////AP///wD///8Ara3/AAAA/wD///8A////AP///wCtrf8A////AP///wD/ //8AAAD/ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//UlL//wAAAP8Ara3/AAAA//8A AAD/AAAA/wAAAP8AAAAA////AK2t/wD///8A////AP///wD///8Ara3/AP///wD///8A////AP///wCt rf8A////AP///wD///8A////AK2t/wD///8A////AP///wD///8Ara3/AP///wAAAP8A////AP///wCt rf8A////AP///wAAAP9SUv//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//1JS //8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAAA////AK2t/wD///8A////AP///wD///8Ara3/AP///wD/ //8A////AP///wCtrf8A////AP///wD///8A////AK2t/wD///8A////AP///wD///8Ara3/AP///wAA AP8AAAD/AP///wCtrf8A////AP///wAAAP+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAAAra3/AK2t/wCtrf8Ara3/AK2t/wCt rf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCtrf8Ara3/AK2t/wAAAP8Ara3/AK2t/wCt rf8Ara3/AAAA/1JS//9SUv//AAAA/wCtrf8Ara3/AAAA/1JS//+trf//ra3//wAAAP9SUv//ra3//62t //+trf//ra3//62t//+trf//AAAA/62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAAA////AK2t/wD/ //8A////AP///wD///8Ara3/AP///wD///8A////AP///wCtrf8A////AAAA/wD///8Ara3/AAAA/wAA AP8A////AP///wD///8AAAD/UlL//62t//+trf//UlL//wAAAP8A////AAAA/62t//+trf//AAAA/1JS //+trf//AAAA/62t//+trf//ra3//62t//+trf//AAAA/wAAAP+trf//ra3//wAAAP//AAAA/wAAAP8A AAAA////AK2t/wD///8A////AP///wD///8Ara3/AP///wAAAP8AAAD/AP///wCtrf8Ara3/AAAA/wAA AP8AAAD//wAAAAAAAP8A////AP///wAAAP8AAAD/ra3//62t//+trf//ra3//wAAAP8AAAD/UlL//62t //8AAAD//wAAAAAAAP8AAAD/UlL//62t//8AAAD/UlL//62t//8AAAD//wAAAP8AAAAAAAD/AAAA//8A AAD/AAAA/wAAAP8AAAAA////AK2t/wD///8A////AP///wD///8AAAD/AAAA//8AAAD/AAAAAAAA/wAA AP8AAAD//wAAAP8AAAD/AAAA/wAAAAAAAP8A////AAAA//8AAAAAAAD/ra3//62t//+trf//ra3//wAA AP8AAAD/ra3//62t//8AAAD//wAAAP8AAAD/AAAAAAAA/wAAAP8AAAD/UlL//62t//8AAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAA////AK2t/wD///8AAAD/AAAA/wAAAP//AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD//wAAAP8AAAAAAAD/UlL//1JS //9SUv//UlL//wAAAP9SUv//ra3//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP9SUv//ra3//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/AAAA/wAAAP//AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP8AAAD/AAAA/wAA AP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP+trf//ra3//wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAAAA AP+trf//ra3//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/AAAA/1JS //9SUv//UlL//1JS//9SUv//UlL//1JS//9SUv//UlL//1JS//8AAAD/AAAA/1JS//9SUv//AAAA/wAA AP8AAAD/AAAA/1JS//+trf//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/wAA AP9SUv//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//UlL//1JS//9SUv//AAAA/62t//+trf//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAAAAAD/UlL//62t//+trf//ra3//62t//+trf//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA AP8AAAD/ra3//62t//+trf//ra3//62t//+trf//UlL//wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAAAAAP9SUv//ra3//62t//+trf//ra3//wAAAP8AAAD/AAAA//////////////////// /////////////wAAAP8AAK3/AAAA/wAAAP8AAAD/ra3//62t//+trf//ra3//1JS//8AAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/1JS//+trf//ra3//62t//8AAAD/AAAA/wAArf8AAP//AAAA//// /////////////////////////////wAAAP8AAP//AAD//wAA//8AAK3/AAAA/wAAAP+trf//ra3//62t //9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//wAAAP8AAK3/AAD//wAA //8AAP//AAAA/////////////////////////////////wAAAP8AAP//AAD//wAA//8AAP//AAD//wAA rf8AAAD/ra3//62t//9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//ra3//wAA AP8AAP//AAD//wAA//8AAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAP//AAD//wAA //8AAP//AAD//wAA//8AAAD/ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t //+trf//AAAA/wAArf8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAK3/AAAA/62t//+trf//UlL//wAAAP//AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAAAAAD/ra3//62t//+trf//AAAA/wAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAP//AAD//wAA //8AAP//AAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAP//AAAA/62t//+trf//UlL//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//AAAA/wAA//8AAAD//////////////////////wAA AP8AAP//AAD//wAA//8AAP//AAD//wAAAP///////////////////////////wAAAP8AAP//AAAA/62t //+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//ra3//wAAAP8AAAD///////// /////////////wAAAP8AAK3/AACt/wAArf8AAK3/AACt/wAAAP///////////////////////////wAA AP8AAAD/ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//ra3//62t //8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA AP8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//9SUv//AAAA//8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAAAAA/1JS//+trf//ra3//62t//8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBS rf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/62t//+trf//ra3//62t//9SUv//AAAA//8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAAAAAD/AAAA/wAAAP//AAAA/wAAAAAAAP9SUv//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//1JS //8AAAD//wAAAP8AAAAAAAD/AAAA/wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAAAAAP+trf//ra3//1JS//8AAAD//wAAAP8AAAAAAAD/ra3//62t//8AUq3/AFKt/wBS rf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/62t //+trf//UlL//wAAAP//AAAA/wAAAAAAAP+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP+trf//AAAA/1JS//8AAAD/AAAA/wAAAP8AAAD/ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//UlL//wAAAP8AAAD/AAAA/wAAAP+trf//AAAA/1JS//8AAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP+trf//AAAA/1JS//8AAAD/ra3//1JS //8AAAD/ra3//62t//+trf//ra3//wAAAP8AAAD/AAAA/62t//+trf//ra3//62t//+trf//ra3//62t //8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//UlL//wAAAP9SUv//ra3//wAAAP+trf//AAAA/1JS //8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP+trf//AAAA/1JS //8AAAD/ra3//1JS//8AAAD/ra3//62t//+trf//AAAA/wAAAP8AAAD/AAAA/wAAAP+trf//ra3//62t //+trf//ra3//wAAAP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//UlL//wAAAP9SUv//ra3//wAA AP+trf//AAAA/1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAA AP+trf//AAAA/1JS//8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//AAAA/wAAAP8AAAD/AAAA/wAA AP+trf//ra3//62t//+trf//ra3//wAAAP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//UlL//wAA AP8AAAD/AAAA/wAAAP+trf//AAAA/1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAAAAAP+trf//ra3//1JS//8AAAD//wAAAP8AAAAAAAD/ra3//62t//+trf//ra3//62t //8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//ra3//62t//+trf//AAAA/wAAAP8AAAD/ra3//62t //+trf//UlL//wAAAP//AAAA/wAAAAAAAP+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/AAAA/wAAAP//AAAA/wAAAP8AAAAAAAD/ra3//62t //+trf//ra3//62t//8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//ra3//62t//+trf//AAAA/wAA AP8AAAD/ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAAAAAD/AAAA/wAAAP//AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAAAAAD/ra3//62t//+trf//AAAA/wAAAP8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//ra3//wAA AP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//AAAA/wAAAP8AAAD/AAAA/wAAAP+trf//ra3//62t //+trf//ra3//wAAAP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//ra3//wAAAP8AAAD/AAAA/62t //+trf//ra3//62t//+trf//ra3//62t//8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//UlL//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/UlL//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//9SUv//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAAAAA/1JS//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//1JS//9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP8AAAD/UlL//1JS//9SUv//AAAA/1JS//9SUv//UlL//wAA AP9SUv//UlL//1JS//8AAAD/UlL//1JS//8AAAD/UlL//wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABSUv//AAAA//8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/1JS//9SUv//AAAA/wAAAP8AAAD/AAAA/wAA AP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA//8AAAAAAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAACtrf//UlL//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/UlL//62t//+trf//AAAA//8A AAD/AAAAAAAA//8AAAD/AAAA/wAAAAAAAP//AAAA/wAAAP8AAAAAAAD//wAAAP8AAAAAAAD//wAAAP8A AAD/AAAAAAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AACtrf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP9SUv//ra3//62t //8AAAD//wAAAP8AAAD/AAAAAAAA//8AAAD/AAAA/wAAAAAAAP//AAAA/wAAAP8AAAAAAAD//wAAAP8A AAAAAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAACtrf//ra3//62t//9SUv//AAAA/wAAAP//AAAA/wAAAP8AAAD/AAAAAAAA/1JS //+trf//ra3//wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/AAAA//8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAACtrf//ra3//62t//+trf//ra3//1JS//8AAAD//wAAAP8A AAAAAAD/UlL//62t//+trf//AAAA/1JS//9SUv//AAAA/wAAAP//AAAA/wAAAAAAAP9SUv//UlL//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//+trf//ra3//62t //9SUv//AAAA/wAAAP9SUv//ra3//62t//+trf//ra3//62t//8AAAD/ra3//1JS//8AAAD/AAAA/1JS //+trf//ra3//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t //+trf//ra3//62t//+trf//UlL//1JS//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//wAA AP8AAAD/UlL//62t//+trf//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAAAAAP+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAAAAAAAAAcAAAAAAAAABwAAAAAAAAAPAAAAAAAAAB8AAAAAAAAADwAAAAAAAAAH AAAAAAAAAAcAAAAAgAEAzwAAAMeIAcD/AAAD/9gD4f8AAB//gAHh/wAA//4AAAP/AAD/+AAAA/8AAP/w AAAH/wAA/+AAAAf/AAD/wAAAA/8AAP/AAAAD/wAA/4AAAAH/AAD/gAAAAf8AAP+AAAAB/wAA/4AAAAH/ AAD/gAAAAf8AAP+AAAAB/wAA/8AAAAP/AAD/wAAAA/8AAPxgAAAGPwAA+DAAAAwfAAD4AAAAAB8AAPgA AAAAHwAA+AAAAAAfAAD4AAAAAB8AAPgwAAAMHwAA/HAAAA4/AAD/8AAAD/8AAP/wAAAP/wAA//AAAA// AAD/8AAAD/8AAP/wAAAP/wAA//gAAB//AAB//AAAP/8AAD/4AAC//wAAH/Bu7d//AAAP4O7t//8AAAPA +f///wAAAYAw////AAAAAAD///8AAIAAAf///wAAwAAD////AAAoAAAAMAAAAGAAAAABACAAAAAAAAAk AAASCwAAEgsAAAAAAAAAAAAAAK2t/wCtrf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCt rf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCtrf8AAAD/AK2t/wCtrf8Ara3/AK2t/wCt rf8Ara3/AK2t/wCtrf8Ara3/AK2t/wAAAP9SUv//ra3//62t//+trf//ra3//62t//+trf//ra3//1JS //8AAAD/AK2t/wCtrf8AAAD//wAAAP8AAAD/AAAAAP///wCtrf8A////AP///wD///8A////AK2t/wD/ //8A////AP///wD///8Ara3/AP///wD///8A////AP///wCtrf8A////AP///wD///8A////AAAA/wD/ //8A////AP///wD///8Ara3/AP///wD///8A////AAAA/1JS//+trf//ra3//62t//+trf//ra3//62t //+trf//UlL//wAAAP8Ara3/AP///wCtrf8AAAD//wAAAP8AAAD/AAAAAP///wCtrf8A////AP///wD/ //8A////AK2t/wD///8A////AP///wD///8Ara3/AP///wD///8A////AP///wCtrf8A////AP///wD/ //8A////AK2t/wAAAP8A////AP///wD///8Ara3/AP///wD///8A////AAAA/62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//1JS//8AAAD/AK2t/wAAAP//AAAA/wAAAP8AAAD/AAAAAP///wCt rf8A////AP///wD///8A////AK2t/wD///8A////AP///wD///8Ara3/AP///wD///8A////AP///wCt rf8A////AP///wD///8A////AK2t/wD///8AAAD/AP///wD///8Ara3/AP///wD///8AAAD/UlL//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//9SUv//AAAA//8AAAD/AAAA/wAAAP8A AAD/AAAAAP///wCtrf8A////AP///wD///8A////AK2t/wD///8A////AP///wD///8Ara3/AP///wD/ //8A////AP///wCtrf8A////AP///wD///8A////AK2t/wD///8AAAD/AAAA/wD///8Ara3/AP///wD/ //8AAAD/ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//UlL//wAA AP//AAAA/wAAAP8AAAD/AAAAAK2t/wCtrf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCt rf8Ara3/AK2t/wCtrf8Ara3/AK2t/wCtrf8AAAD/AK2t/wCtrf8Ara3/AK2t/wAAAP9SUv//UlL//wAA AP8Ara3/AK2t/wAAAP9SUv//ra3//62t//8AAAD/UlL//62t//+trf//ra3//62t//+trf//ra3//wAA AP+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAAAP///wCtrf8A////AP///wD///8A////AK2t/wD/ //8A////AP///wD///8Ara3/AP///wAAAP8A////AK2t/wAAAP8AAAD/AP///wD///8A////AAAA/1JS //+trf//ra3//1JS//8AAAD/AP///wAAAP+trf//ra3//wAAAP9SUv//ra3//wAAAP+trf//ra3//62t //+trf//ra3//wAAAP8AAAD/ra3//62t//8AAAD//wAAAP8AAAD/AAAAAP///wCtrf8A////AP///wD/ //8A////AK2t/wD///8AAAD/AAAA/wD///8Ara3/AK2t/wAAAP8AAAD/AAAA//8AAAAAAAD/AP///wD/ //8AAAD/AAAA/62t//+trf//ra3//62t//8AAAD/AAAA/1JS//+trf//AAAA//8AAAAAAAD/AAAA/1JS //+trf//AAAA/1JS//+trf//AAAA//8AAAD/AAAAAAAA/wAAAP//AAAA/wAAAP8AAAD/AAAAAP///wCt rf8A////AP///wD///8A////AAAA/wAAAP//AAAA/wAAAAAAAP8AAAD/AAAA//8AAAD/AAAA/wAAAP8A AAAAAAD/AP///wAAAP//AAAAAAAA/62t//+trf//ra3//62t//8AAAD/AAAA/62t//+trf//AAAA//8A AAD/AAAA/wAAAAAAAP8AAAD/AAAA/1JS//+trf//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAAP///wCtrf8A////AAAA/wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA//8AAAD/AAAAAAAA/1JS//9SUv//UlL//1JS//8AAAD/UlL//62t //8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/UlL//62t//8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA AP8AAAD/ra3//62t//8AAAD/AAAA//8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t//8AAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/wAAAP9SUv//UlL//1JS//9SUv//UlL//1JS //9SUv//UlL//1JS//9SUv//AAAA/wAAAP9SUv//UlL//wAAAP8AAAD/AAAA/wAAAP9SUv//ra3//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP8AAAD/UlL//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//1JS//9SUv//UlL//wAA AP+trf//ra3//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/1JS//+trf//ra3//62t //+trf//ra3//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/62t//+trf//ra3//62t //+trf//ra3//1JS//8AAAD/AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/UlL//62t //+trf//ra3//62t//8AAAD/AAAA/wAAAP////////////////////////////////8AAAD/AACt/wAA AP8AAAD/AAAA/62t//+trf//ra3//62t//9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAA AP9SUv//ra3//62t//+trf//AAAA/wAAAP8AAK3/AAD//wAAAP////////////////////////////// //8AAAD/AAD//wAA//8AAP//AACt/wAAAP8AAAD/ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAAAAAP+trf//ra3//62t//8AAAD/AACt/wAA//8AAP//AAD//wAAAP////////////// //////////////////8AAAD/AAD//wAA//8AAP//AAD//wAA//8AAK3/AAAA/62t//+trf//UlL//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//62t//8AAAD/AAD//wAA//8AAP//AAD//wAA AP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAD//wAA//8AAP//AAD//wAA//8AAP//AAAA/62t //+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//wAAAP8AAK3/AAD//wAA //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA //8AAP//AACt/wAAAP+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//wAA AP8AAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAD//wAA//8AAP//AAD//wAA//8AAAD/AAAA/wAA AP8AAAD/AAAA/wAAAP8AAAD/AAD//wAAAP+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t //+trf//ra3//wAAAP8AAP//AAAA//////////////////////8AAAD/AAD//wAA//8AAP//AAD//wAA //8AAAD///////////////////////////8AAAD/AAD//wAAAP+trf//ra3//1JS//8AAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAAAAA/62t//+trf//ra3//62t//8AAAD/AAAA//////////////////////8AAAD/AACt/wAA rf8AAK3/AACt/wAArf8AAAD///////////////////////////8AAAD/AAAA/62t//+trf//ra3//1JS //8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//62t//+trf//AAAA/wAAAP8AAAD/AAAA/wAA AP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/ra3//62t //+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP9SUv//ra3//62t //+trf//AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBS rf8AUq3/AFKt/wBSrf+trf//ra3//62t//+trf//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/wAAAP8AAAD//wAAAP8A AAAAAAD/UlL//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//9SUv//AAAA//8AAAD/AAAAAAAA/wAA AP8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t //9SUv//AAAA//8AAAD/AAAAAAAA/62t//+trf//AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBS rf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf8AUq3/AFKt/wBSrf+trf//ra3//1JS//8AAAD//wAAAP8A AAAAAAD/ra3//62t//9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAAAAAD/ra3//wAAAP9SUv//AAAA/wAAAP8AAAD/AAAA/62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//1JS //8AAAD/AAAA/wAAAP8AAAD/ra3//wAAAP9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAAAAAD/ra3//wAAAP9SUv//AAAA/62t//9SUv//AAAA/62t//+trf//ra3//62t //8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//ra3//62t//+trf//AAAA/wAAAP8AAAD/ra3//62t //+trf//ra3//1JS//8AAAD/UlL//62t//8AAAD/ra3//wAAAP9SUv//AAAA//8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//wAAAP9SUv//AAAA/62t//9SUv//AAAA/62t //+trf//ra3//wAAAP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//ra3//62t//8AAAD/AAAA/wAA AP8AAAD/AAAA/62t//+trf//ra3//1JS//8AAAD/UlL//62t//8AAAD/ra3//wAAAP9SUv//AAAA//8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//wAAAP9SUv//AAAA/wAA AP8AAAD/AAAA/62t//+trf//ra3//wAAAP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//ra3//62t //8AAAD/AAAA/wAAAP8AAAD/AAAA/62t//+trf//ra3//1JS//8AAAD/AAAA/wAAAP8AAAD/ra3//wAA AP9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t //9SUv//AAAA//8AAAD/AAAAAAAA/62t//+trf//ra3//62t//+trf//AAAA/wAAAP8AAAD/ra3//62t //+trf//ra3//62t//+trf//ra3//wAAAP8AAAD/AAAA/62t//+trf//ra3//1JS//8AAAD//wAAAP8A AAAAAAD/ra3//62t//9SUv//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAAAAA/wAAAP8AAAD//wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//62t//+trf//AAAA/wAA AP8AAAD/ra3//62t//+trf//ra3//62t//+trf//ra3//wAAAP8AAAD/AAAA/62t//+trf//ra3//1JS //8AAAD//wAAAP8AAAD/AAAAAAAA/wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//wAA AP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//ra3//62t//8AAAD/AAAA/wAAAP8AAAD/AAAA/62t //+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t //+trf//ra3//wAAAP8AAAD/AAAA/wAAAP8AAAD/ra3//62t//+trf//ra3//62t//8AAAD/AAAA/wAA AP8AAAD/AAAA/62t//+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAAAAA/62t//+trf//ra3//62t//8AAAD/AAAA/wAAAP+trf//ra3//62t//+trf//ra3//62t //+trf//AAAA/wAAAP8AAAD/ra3//62t//+trf//ra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//1JS//8AAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAA/1JS//+trf//ra3//62t//+trf//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//UlL//1JS //8AAAD//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP9SUv//ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //9SUv//UlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAAAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAAAAAD/AAAA/1JS//9SUv//UlL//wAAAP9SUv//UlL//1JS//8AAAD/UlL//1JS//9SUv//AAAA/1JS //9SUv//AAAA/1JS//8AAAD/AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAUlL//wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAAAAAP9SUv//UlL//wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA AP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP//AAAAAAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAra3//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/1JS//+trf//ra3//wAAAP//AAAA/wAAAAAAAP//AAAA/wAAAP8A AAAAAAD//wAAAP8AAAD/AAAAAAAA//8AAAD/AAAAAAAA//8AAAD/AAAA/wAAAAAAAP//AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAra3//62t//9SUv//AAAA//8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/UlL//62t//+trf//AAAA//8AAAD/AAAA/wAAAAAA AP//AAAA/wAAAP8AAAAAAAD//wAAAP8AAAD/AAAAAAAA//8AAAD/AAAAAAAA//8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAra3//62t //+trf//UlL//wAAAP8AAAD//wAAAP8AAAD/AAAA/wAAAAAAAP9SUv//ra3//62t//8AAAD/AAAA//8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/wAAAP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAAra3//62t//+trf//ra3//62t//9SUv//AAAA//8AAAD/AAAAAAAA/1JS//+trf//ra3//wAA AP9SUv//UlL//wAAAP8AAAD//wAAAP8AAAAAAAD/UlL//1JS//8AAAD//wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAAAAAA/62t//+trf//ra3//62t//+trf//UlL//wAAAP8AAAD/UlL//62t //+trf//ra3//62t//+trf//AAAA/62t//9SUv//AAAA/wAAAP9SUv//ra3//62t//8AAAD//wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAP+trf//ra3//62t//+trf//ra3//1JS //9SUv//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//8AAAD/AAAA/1JS//+trf//ra3//wAA AP//AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAD/ra3//62t //+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t//+trf//ra3//62t //+trf//AAAA//8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8A AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAAAAAAAAH AAAAAAAAAAcAAAAAAAAADwAAAAAAAAAfAAAAAAAAAA8AAAAAAAAABwAAAAAAAAAHAAAAAIABAM8AAADH iAHA/wAAA//YA+H/AAAf/4AB4f8AAP/+AAAD/wAA//gAAAP/AAD/8AAAB/8AAP/gAAAH/wAA/8AAAAP/ AAD/wAAAA/8AAP+AAAAB/wAA/4AAAAH/AAD/gAAAAf8AAP+AAAAB/wAA/4AAAAH/AAD/gAAAAf8AAP/A AAAD/wAA/8AAAAP/AAD8YAAABj8AAPgwAAAMHwAA+AAAAAAfAAD4AAAAAB8AAPgAAAAAHwAA+AAAAAAf AAD4MAAADB8AAPxwAAAOPwAA//AAAA//AAD/8AAAD/8AAP/wAAAP/wAA//AAAA//AAD/8AAAD/8AAP/4 AAAf/wAAf/wAAD//AAA/+AAAv/8AAB/wbu3f/wAAD+Du7f//AAADwPn///8AAAGAMP///wAAAAAA//// AACAAAH///8AAMAAA////wAA ================================================ FILE: LibSidWiz/ProcessWrapper.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading; namespace LibSidWiz { public class ProcessWrapper: IDisposable { private readonly Process _process; private readonly BlockingCollection _lines = new(new ConcurrentQueue()); private readonly CancellationTokenSource _cancellationTokenSource; private int _streamCount; public ProcessWrapper(string filename, string arguments, bool captureStdErr = false, bool captureStdOut = true, bool showConsole = false) { _cancellationTokenSource = new CancellationTokenSource(); _process = new Process { StartInfo = new ProcessStartInfo { FileName = filename, Arguments = arguments, RedirectStandardOutput = captureStdOut, RedirectStandardError = captureStdErr, UseShellExecute = false, CreateNoWindow = !showConsole }, EnableRaisingEvents = true }; if (_process == null) { throw new Exception($"Error running {filename} {arguments}"); } if (captureStdOut) { _process.OutputDataReceived += OnText; } if (captureStdErr) { _process.ErrorDataReceived += OnText; } _process.Start(); if (captureStdOut) { _process.BeginOutputReadLine(); ++_streamCount; } if (captureStdErr) { _process.BeginErrorReadLine(); ++_streamCount; } } private void OnText(object sender, DataReceivedEventArgs e) { if (_cancellationTokenSource.IsCancellationRequested) { return; } try { _lines.TryAdd(e.Data, -1, _cancellationTokenSource.Token); } catch (OperationCanceledException) { // Discard it } } /// /// Blocks while waiting for the next line... /// public IEnumerable Lines() { while (!_cancellationTokenSource.IsCancellationRequested) { string line; try { // Blocking take line = _lines.Take(_cancellationTokenSource.Token); } catch (OperationCanceledException) { yield break; } if (line == null) { if (--_streamCount == 0) { // We see a null to indicate the end of each stream. We break on the last one. yield break; } // Else drop it continue; } yield return line; } } public void WaitForExit() => _process.WaitForExit(); public void Dispose() { _cancellationTokenSource.Cancel(); _lines.CompleteAdding(); if (_process != null) { // Try to kill the process if it is alive if (!_process.HasExited) { try { _process.EnableRaisingEvents = false; _process.Kill(); } catch (Exception) { // May throw if the process terminates first } } _process.Dispose(); } _cancellationTokenSource.Dispose(); _lines.Dispose(); } } } ================================================ FILE: LibSidWiz/SampleBuffer.cs ================================================ using System; using NAudio.Dsp; using NAudio.Wave; namespace LibSidWiz { internal class SampleBuffer: IDisposable { private readonly WaveStream _reader; private readonly ISampleProvider _sampleProvider; private class Chunk { public long Offset; public long End; public float[] Buffer; public bool TryGet(long index, out float value) { if (index >= Offset && index < End) { value = Buffer[index - Offset]; return true; } value = 0; return false; } } private readonly Chunk _chunk1; private readonly Chunk _chunk2; // 4 bytes per sample so this is 1MB // If we are rendering ~16 frames at once, we need (typically) 1323 samples per frame, // which is a window of 84KB. This is far from causing us trouble here. private const int ChunkSize = 256 * 1024 * 1; public long Count { get; } public int SampleRate { get; } public TimeSpan Length { get; } public float Max { get; private set; } public float Min { get; private set; } public SampleBuffer(string filename, Channel.Sides side, bool filter) { _reader = new AudioFileReader(filename); Count = _reader.Length * 8 / _reader.WaveFormat.BitsPerSample / _reader.WaveFormat.Channels; SampleRate = _reader.WaveFormat.SampleRate; Length = _reader.TotalTime; _sampleProvider = side switch { Channel.Sides.Left => _reader.ToSampleProvider().ToMono(1.0f, 0.0f), Channel.Sides.Right => _reader.ToSampleProvider().ToMono(0.0f, 1.0f), Channel.Sides.Mix => _reader.ToSampleProvider().ToMono(), _ => throw new ArgumentOutOfRangeException(nameof(side), side, null) }; if (filter) { _sampleProvider = new HighPassSampleProvider(_sampleProvider); } _chunk1 = new Chunk { Buffer = new float[ChunkSize], Offset = -1, End = -1 }; _chunk2 = new Chunk { Buffer = new float[ChunkSize], Offset = -1, End = -1 }; } public void Dispose() { _reader.Dispose(); } public float this[long index] { get { // We may be accessed from multiple threads; we therefore need to lock access to avoid concurrent access. lock (this) { if (_chunk1.TryGet(index, out var value) || _chunk2.TryGet(index, out value)) { return value; } // If we are asked for sample 0, reset the buffers if (index == 0) { _chunk1.Offset = _chunk1.End = _chunk2.Offset = _chunk2.End = -1; } // Else pick the lower index chunk to read into var chunk = _chunk1.Offset < _chunk2.Offset ? _chunk1 : _chunk2; // Pick the rounded offset chunk.Offset = (index / ChunkSize) * ChunkSize; chunk.End = chunk.Offset + ChunkSize; _reader.Position = chunk.Offset * _reader.WaveFormat.BitsPerSample / 8 * _reader.WaveFormat.Channels; _sampleProvider.Read(chunk.Buffer, 0, ChunkSize); return chunk.Buffer[index - chunk.Offset]; } } } public void Analyze() { Min = float.MaxValue; Max = float.MinValue; for (int i = 0; i < Count; ++i) { var sample = this[i]; Min = Math.Min(Min, sample); Max = Math.Max(Max, sample); } } } internal class HighPassSampleProvider(ISampleProvider sampleProvider) : ISampleProvider { private readonly BiQuadFilter _filter = BiQuadFilter.HighPassFilter(sampleProvider.WaveFormat.SampleRate, 20, 1); public int Read(float[] buffer, int offset, int count) { int result = sampleProvider.Read(buffer, offset, count); // Apply the filter for (int i = 0; i < result; ++i) { buffer[i] = _filter.Transform(buffer[i]); } return result; } public WaveFormat WaveFormat => sampleProvider.WaveFormat; } } ================================================ FILE: LibSidWiz/Triggers/AutoCorrelationTrigger.cs ================================================ using System; #if DEBUG using System.Diagnostics; using System.Text; #endif namespace LibSidWiz.Triggers { // ReSharper disable once UnusedType.Global internal class AutoCorrelationTrigger: ITriggerAlgorithm { private float[] _normalDistribution; public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { var width = endIndex - startIndex; if (_normalDistribution == null || _normalDistribution.Length != width) { _normalDistribution = new float[width]; // Generate distribution // We fit 2 standard deviations to the width double variance = width * width / 16.0; double mu = width / 2.0; var scale = Math.Sqrt(2 * Math.PI * variance); for (int i = 0; i < width; ++i) { _normalDistribution[i] = (float) (Math.Exp(-(i - mu) * (i - mu) / (2 * variance)) / scale); } } var maxCorrelation = double.MinValue; var bestOffset = startIndex; var previousStart = previousIndex - width / 2; // We compute the correlation between the previous window and each possible offset in the new one, // weighted by a normal distribution so we prefer ones near the middle. // The correlation is defined as // sum((x - mean(x)) * (y - mean(y)) / sqrt(sum(pow(x - mean(x), 2)) * sum(pow(y - mean(y), 2))) // where x and y are the samples in each series. Since the reference is fixed, we can compute it once. float sumY = 0; float sumX = 0; for (int i = 0; i < width; ++i) { sumY += channel.GetSample(previousStart + i); sumX += channel.GetSample(startIndex + i); } var meanY = sumY / width; float sumSquaredYDiff = 0; for (int i = 0; i < width; ++i) { var y = channel.GetSample(previousStart + i); var yDiff = y - meanY; sumSquaredYDiff += yDiff * yDiff; } // ReSharper disable once CompareOfFloatsByEqualityOperator if (sumSquaredYDiff == 0.0f) { // No point continuing - we return the middle of the data return startIndex + width / 2; } var correlations = new double[width]; for (int trialOffset = 0; trialOffset < width; ++trialOffset) { // We compute mean(x) as we go var meanX = sumX / width; // ...by amending the moving sum. This will accumulate floating point error but it's not significant. sumX -= channel.GetSample(startIndex + trialOffset); sumX += channel.GetSample(startIndex + trialOffset + width); // sum((x - mean(x)) * (y - mean(y)) / sqrt(sum(pow(x - mean(x), 2)) * sum(pow(y - mean(y), 2))) float sumTop = 0; float sumSquaredXDiff = 0; for (int i = 0; i < width; ++i) { var x = channel.GetSample(startIndex + trialOffset + i); var y = channel.GetSample(previousStart + i); sumTop += (x - meanX) * (y - meanY); sumSquaredXDiff += (x - meanX) * (x - meanX); } var correlation = sumTop / Math.Sqrt(sumSquaredXDiff * sumSquaredYDiff); // We then weight by our normal distribution so we will prefer points near the middle. correlation *= _normalDistribution[trialOffset]; // debug correlations[trialOffset] = correlation; if (correlation > maxCorrelation) { maxCorrelation = correlation; bestOffset = trialOffset; } } #if DEBUG Debug.WriteLine($"Autocorrelation: between {startIndex} and {endIndex}, max = {maxCorrelation}, offset = {bestOffset} ({(float)(bestOffset - startIndex)/(endIndex - startIndex):P})"); var sb = new StringBuilder(); for (int i = 0; i < width; ++i) { sb.AppendLine($"{channel.GetSample(previousStart + i)}\t{channel.GetSample(startIndex + i)}\t{correlations[i]}"); } #endif return startIndex + bestOffset; } } } ================================================ FILE: LibSidWiz/Triggers/BiggestPositiveWaveAreaTrigger.cs ================================================ using System; namespace LibSidWiz.Triggers { /// /// Finds the wave with the biggest positive area (= sum of positive samples) /// // ReSharper disable once UnusedType.Global internal class BiggestPositiveWaveAreaTrigger : ITriggerAlgorithm { public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { int bestOffset = -1; int lastCrossingPoint = endIndex; float previousSample = channel.GetSample(startIndex); float bestArea = 0; float currentArea = float.MinValue; // For each sample... for (int i = startIndex + 1; i < endIndex; ++i) { // Add on the area var sample = channel.GetSample(i); currentArea += Math.Abs(sample); if (sample > 0 && previousSample <= 0) { // Positive edge - reset lastCrossingPoint = i; currentArea = sample; } else if (sample <= 0 && previousSample > 0) { // Negative edge - check if it's a new biggest if (currentArea > bestArea) { bestArea = currentArea; bestOffset = lastCrossingPoint; } } previousSample = sample; } return bestOffset; } } } ================================================ FILE: LibSidWiz/Triggers/BiggestWaveAreaTrigger.cs ================================================ using System; namespace LibSidWiz.Triggers { /// /// Finds the positive+negative wave with the biggest area (= sum of absolute samples) /// // ReSharper disable once UnusedType.Global internal class BiggestWaveAreaTrigger : ITriggerAlgorithm { public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { int bestOffset = -1; int lastCrossingPoint = endIndex; float previousSample = channel.GetSample(startIndex); float bestArea = 0; float currentArea = float.MinValue; // For each sample... for (int i = startIndex + 1; i < endIndex; ++i) { // Add on the area var sample = channel.GetSample(i); currentArea += Math.Abs(sample); if (sample > 0 && previousSample <= 0) { // Positive edge - check if it's a new biggest if (currentArea > bestArea) { bestArea = currentArea; bestOffset = lastCrossingPoint; } // And reset lastCrossingPoint = i; currentArea = sample; } previousSample = sample; } return bestOffset; } } } ================================================ FILE: LibSidWiz/Triggers/ITriggerAlgorithm.cs ================================================ namespace LibSidWiz.Triggers { public interface ITriggerAlgorithm { /// /// Finds a "trigger point" within a channel's samples /// /// Channel object holding samples /// Index of start of frame for analysis /// Index of end of frame for analysis /// Index of previously found trigger /// Index of the trigger point, should be between startIndex and endIndex. Return -1 for failure. int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex); } } ================================================ FILE: LibSidWiz/Triggers/MiddleWidest.cs ================================================ using System.Collections.Generic; namespace LibSidWiz.Triggers { /// /// This corresponds to SidWiz's "alternate" algorithm. /// We measure the width of each full wave, and then select the widest ones. /// We then select the start point of the "middle" one, if more than one was found. /// // ReSharper disable once UnusedType.Global class MiddleWidest: ITriggerAlgorithm { public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { var candidates = new List(); int lastCrossingPoint = endIndex; float previousSample = channel.GetSample(startIndex); int bestLength = 0; // For each sample... for (int i = startIndex + 1; i < endIndex; ++i) { var sample = channel.GetSample(i); if (sample > 0 && previousSample <= 0) { // Positive edge int length = i - lastCrossingPoint; if (length > bestLength) { candidates.Clear(); bestLength = length; } if (length == bestLength) { candidates.Add(lastCrossingPoint); } lastCrossingPoint = i; } previousSample = sample; } if (candidates.Count == 0) { return -1; } // We select the "middle" one, preferring the one on the right if even return candidates[candidates.Count / 2]; } } } ================================================ FILE: LibSidWiz/Triggers/NullTrigger.cs ================================================ namespace LibSidWiz.Triggers { /// /// Null algorithm just returns the first sample it's given /// // ReSharper disable once UnusedType.Global internal class NullTrigger: ITriggerAlgorithm { public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { return startIndex; } } } ================================================ FILE: LibSidWiz/Triggers/PeakSpeedTrigger.cs ================================================ namespace LibSidWiz.Triggers { /// /// Finds the positive edge which most quickly reaches the peak value in the sample range. /// This is implemented in a slightly complicated way to make it do it with a single pass over the samples, /// you could implement it as: /// 1. Find first zero crossing /// 2. Find max sample value after that /// 4. Select the zero crossing closest to a following max value /// This algorithm is based code from オップナー2608. /// This algorithm can show good stability for waves which cross the zero point more than once. /// public class PeakSpeedTrigger : ITriggerAlgorithm { public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { float peakValue = float.MinValue; int shortestDistance = int.MaxValue; int result = -1; int i = startIndex; while (i < endIndex) { // First find a positive edge crossing zero while (channel.GetSample(i) > 0 && i < endIndex) ++i; while (channel.GetSample(i) <= 0 && i < endIndex) ++i; // Remember this point int lastCrossing = i; // Now move forward looking for a peak for (var sample = channel.GetSample(i); sample > 0 && i < endIndex; ++i) { if (sample > peakValue) { // It's a new high peakValue = sample; result = lastCrossing; shortestDistance = i - lastCrossing; } // ReSharper disable once CompareOfFloatsByEqualityOperator else if (sample == peakValue && (i - lastCrossing) < shortestDistance) { // It's equal to the best peak but closer to the crossing point result = lastCrossing; shortestDistance = i - lastCrossing; } sample = channel.GetSample(i); } } return result; } } } ================================================ FILE: LibSidWiz/Triggers/RisingEdgeTrigger.cs ================================================ namespace LibSidWiz.Triggers { /// /// Trigger that finds the rising edge of the wave. /// This is normally fine for simple waveforms but it can fall down when it sees /// waves which cross the centre point more than once. /// It also only finds the first rising edge in the sample range, rather than the /// one nearest the centre. /// internal class RisingEdgeTrigger : ITriggerAlgorithm { public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { // We step through the sample and select the first negative -> positive transition int result = startIndex; while (channel.GetSample(result) > 0 && result < endIndex) ++result; while (channel.GetSample(result) <= 0 && result < endIndex) ++result; if (result == endIndex) { // Failed to find anything result = -1; } return result; } } } ================================================ FILE: LibSidWiz/Triggers/WidestWaveTrigger.cs ================================================ namespace LibSidWiz.Triggers { /// /// Finds the widest positive+negative wave in the range /// This can get confused by volume changes on SN76489 noise, which it will perceive as a wide wave /// // ReSharper disable once UnusedType.Global internal class WidestWaveTrigger : ITriggerAlgorithm { public int GetTriggerPoint(Channel channel, int startIndex, int endIndex, int previousIndex) { int bestOffset = -1; int lastCrossingPoint = endIndex; float previousSample = channel.GetSample(startIndex); int bestLength = 0; // For each sample... for (int i = startIndex + 1; i < endIndex; ++i) { var sample = channel.GetSample(i); if (sample > 0 && previousSample <= 0) { // Positive edge int length = i - lastCrossingPoint; if (length > bestLength) { bestLength = length; bestOffset = lastCrossingPoint; } lastCrossingPoint = i; } previousSample = sample; } return bestOffset; } } } ================================================ FILE: LibSidWiz/WaveformRenderer.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using LibSidWiz.Outputs; using SkiaSharp; using SkiaSharp.Views.Desktop; namespace LibSidWiz { /// /// Class responsible for rendering /// public class WaveformRenderer { private readonly List _channels = []; public int Width { get; set; } public int Height { get; set; } public int Columns { get; set; } public int SamplingRate { get; set; } public int FramesPerSecond { get; set; } public Color BackgroundColor { get; set; } = Color.Black; public Image BackgroundImage { get; set; } public Rectangle RenderingBounds { get; set; } public void AddChannel(Channel channel) { _channels.Add(channel); } public void Render(IList outputs, int numThreads, bool verboseLogging) { int numFrames = (int)(_channels.Max(c => c.SampleCount) * FramesPerSecond / SamplingRate); var length = TimeSpan.FromSeconds((double)numFrames / FramesPerSecond); int frameIndex = 0; Render(0, numFrames, (bm, rawData) => { double fractionComplete = (double) ++frameIndex / numFrames; foreach (var output in outputs) { output.Write(bm, rawData, fractionComplete, length); } }, numThreads >= 1 ? numThreads : Environment.ProcessorCount); } /// /// Renders a range of frames into the given destination image, calling back the handler for each one /// private void Render(int startFrame, int endFrame, Action onFrame, int numThreads) { // Default rendering bounds if not set var renderingBounds = RenderingBounds; if (renderingBounds.Width == 0 || renderingBounds.Height == 0) { renderingBounds = new Rectangle(0, 0, Width, Height); } // Compute channel bounds var numRows = _channels.Count / Columns + (_channels.Count % Columns == 0 ? 0 : 1); for (int i = 0; i < _channels.Count; ++i) { int ChannelX(int column1) => column1 * renderingBounds.Width / Columns + renderingBounds.Left; int ChannelY(int row1) => row1 * renderingBounds.Height / numRows + renderingBounds.Top; var channel = _channels[i]; var column = i % Columns; var row = i / Columns; // Compute sizes as difference to next one to avoid off-by-one errors var x = ChannelX(column); var y = ChannelY(row); channel.Bounds = new Rectangle(x, y, ChannelX(column + 1) - x, ChannelY(row + 1) - y); } // We generate our "base image" var templateData = new byte[Width * Height * 4]; GCHandle pinnedArray = GCHandle.Alloc(templateData, GCHandleType.Pinned); try { using var templateImage = new Bitmap(Width, Height, Width * 4, PixelFormat.Format32bppPArgb, pinnedArray.AddrOfPinnedObject()); GenerateTemplate(templateImage); // We want to do a few things in parallel: // 1. For each frame, for each channel, get the previous frame trigger point // 2. Find the next trigger point // 3. When a frame has all the channels' trigger points, draw it // 4. Call the callback in strict frame order // #3 is the slowest part, but we'd like to somewhat parallelize parts 1-2 as well. // So we do that in a parallel for, in a task, emitting into a queue... var queue = new BlockingCollection(numThreads); // Initialise the "previous trigger points" var frameSamples = SamplingRate / FramesPerSecond; var triggerPoints = new int[_channels.Count]; for (int channelIndex = 0; channelIndex < _channels.Count; ++channelIndex) { triggerPoints[channelIndex] = (int)((long)startFrame * SamplingRate / FramesPerSecond) - frameSamples; } Task.Factory.StartNew(() => { for (var frameIndex = startFrame; frameIndex <= endFrame; ++frameIndex) { var info = GetFrameInfo(frameIndex, frameSamples, triggerPoints); // Add to the queue (may block) queue.Add(info); } // Signal that this phase is done queue.CompleteAdding(); }); // Then we want to make a bunch of objects to consume from the queue, as it is filled up, in parallel. // These are tricky because they will want to emit bitmaps, which need to be passed to the renderers in order. // To do that, a simple FIFO is not enough; we want it to also be ordered by frame index and only consume in order. var renderedFrames = new ConcurrentDictionary(); var frameReadySignal = new AutoResetEvent(false); // Then we kick off some parallel threads to do the rendering, consuming from queue and emitting to renderedFrames indexed by frame index foreach (var _ in Enumerable.Range(0, numThreads)) { Task.Factory.StartNew(() => { // We make a thread-local image to render into // This is the raw data buffer we use to store the generated image. // We need it in this form, so we can pass it to FFMPEG. var rawData = new byte[Width * Height * 4]; // We also need to "pin" it so the bitmap can be based on it. var innerPinnedArray = GCHandle.Alloc(rawData, GCHandleType.Pinned); // Prepare the pens and brushes we will use var linePaints = _channels.Select(c => c.LineColor == Color.Transparent || c.LineWidth <= 0 ? null : new SKPaint { Color = new SKColor(c.LineColor.R, c.LineColor.G, c.LineColor.B, c.LineColor.A), StrokeWidth = c.LineWidth, IsAntialias = c.SmoothLines, Style = SKPaintStyle.Stroke, StrokeMiter = c.LineWidth, StrokeJoin = SKStrokeJoin.Bevel }).ToList(); var fillPaints = _channels.Select(c => c.FillColor == Color.Transparent ? null : new SKPaint { Color = new SKColor(c.FillColor.R, c.FillColor.G, c.FillColor.B, c.FillColor.A), IsAntialias = c.SmoothLines, Style = SKPaintStyle.Fill }).ToList(); try { using var pixmap = new SKPixmap(new SKImageInfo(Width, Height, SKColorType.Bgra8888, SKAlphaType.Opaque), innerPinnedArray.AddrOfPinnedObject()); using var image = SKImage.FromPixels(pixmap); using var surface = SKSurface.Create(pixmap.Info, innerPinnedArray.AddrOfPinnedObject()); // Prepare buffers to hold the line coordinates var path = new SKPath(); var fillPath = new SKPath(); while (!queue.IsCompleted) { // Get a frame to work on. This may block. var frame = queue.Take(); var g = surface.Canvas; // Copy from the template Buffer.BlockCopy(templateData, 0, rawData, 0, templateData.Length); // For each channel... for (var channelIndex = 0; channelIndex < _channels.Count; ++channelIndex) { var channel = _channels[channelIndex]; if (channel.IsEmpty) continue; if (!string.IsNullOrEmpty(channel.ErrorMessage)) g.DrawText( channel.ErrorMessage, new SKPoint(channel.Bounds.Left + channel.Bounds.Width / 2.0f, channel.Bounds.Top + channel.Bounds.Height / 2.0f), new SKPaint { Typeface = SKTypeface.Default, Color = SKColors.Red, TextAlign = SKTextAlign.Center, }); else if (channel.Loading) g.DrawText( "Loading data...", new SKPoint(channel.Bounds.Left + channel.Bounds.Width / 2.0f, channel.Bounds.Top + channel.Bounds.Height / 2.0f), new SKPaint { Typeface = SKTypeface.Default, Color = SKColors.Green, TextAlign = SKTextAlign.Center, }); else if (channel.IsSilent && !channel.RenderIfSilent) g.DrawText( "This channel is silent", new SKPoint(channel.Bounds.Left + channel.Bounds.Width / 2.0f, channel.Bounds.Top + channel.Bounds.Height / 2.0f), new SKPaint { Typeface = SKTypeface.Default, Color = SKColors.Yellow, TextAlign = SKTextAlign.Center, }); else // ReSharper disable once AccessToDisposedClosure RenderWave(g, channel, frame.ChannelTriggerPoints[channelIndex], linePaints[channelIndex], fillPaints[channelIndex], path, fillPath, channel.FillBase); } // We "lend" the data to the frame info temporarily frame.Bitmap = image; frame.RawData = rawData; renderedFrames.TryAdd(frame.FrameIndex, frame); // Signal the consuming thread frameReadySignal.Set(); // Wait for it to finish consuming frame.BitmapConsumed.WaitOne(); } } finally { innerPinnedArray.Free(); foreach (var paint in linePaints.Concat(fillPaints)) { paint?.Dispose(); } } }, TaskCreationOptions.LongRunning); } // Finally, we consume the queue for (var frameIndex = startFrame; frameIndex < endFrame; frameIndex++) { FrameInfo frame; while (!renderedFrames.TryGetValue(frameIndex, out frame)) { // Wait for the dictionary to have this index frameReadySignal.WaitOne(); } // Emit the frame onFrame(frame.Bitmap, frame.RawData); frame.BitmapConsumed.Set(); renderedFrames.TryRemove(frameIndex, out _); } } finally { pinnedArray.Free(); } } private FrameInfo GetFrameInfo(int frameIndex, int frameSamples, int[] triggerPoints) { // Compute the start of the sample window int frameIndexSamples = (int)((long)frameIndex * SamplingRate / FramesPerSecond); var info = new FrameInfo { FrameIndex = frameIndex, ChannelTriggerPoints = Enumerable.Repeat(frameIndexSamples, _channels.Count).ToList() }; // For each channel... for (int channelIndex = 0; channelIndex < _channels.Count; ++channelIndex) { var channel = _channels[channelIndex]; if (channel.IsEmpty || !string.IsNullOrEmpty(channel.ErrorMessage) || channel.Loading || channel.IsSilent) { // No trigger to find continue; } // Compute the "trigger point". This will be the centre of our rendering. var triggerPoint = channel.GetTriggerPoint(frameIndexSamples, frameSamples, triggerPoints[channelIndex]); info.ChannelTriggerPoints[channelIndex] = triggerPoint; triggerPoints[channelIndex] = triggerPoint; } return info; } private class FrameInfo { public int FrameIndex { get; set; } public List ChannelTriggerPoints { get; set; } public SKImage Bitmap { get; set; } public byte[] RawData { get; set; } public AutoResetEvent BitmapConsumed { get; } = new(false); } private void GenerateTemplate(Image template) { // Draw it using var g = Graphics.FromImage(template); g.SmoothingMode = SmoothingMode.HighQuality; g.InterpolationMode = InterpolationMode.HighQualityBicubic; if (BackgroundImage != null) { // Fill with the background image using var attribute = new ImageAttributes(); attribute.SetWrapMode(WrapMode.TileFlipXY); g.DrawImage( BackgroundImage, new Rectangle(0, 0, Width, Height), 0, 0, BackgroundImage.Width, BackgroundImage.Height, GraphicsUnit.Pixel, attribute); } else { // Fill background using var brush = new SolidBrush(BackgroundColor); g.FillRectangle(brush, -1, -1, Width + 1, Height + 1); } foreach (var channel in _channels) { if (channel.BackgroundColor != Color.Transparent) { using var b = new SolidBrush(channel.BackgroundColor); g.FillRectangle(b, channel.Bounds); } if (channel.ZeroLineColor != Color.Transparent && channel.ZeroLineWidth > 0) { using var pen = new Pen(channel.ZeroLineColor, channel.ZeroLineWidth); // Draw the zero line g.DrawLine( pen, channel.Bounds.Left, channel.Bounds.Top + channel.Bounds.Height / 2, channel.Bounds.Right, channel.Bounds.Top + channel.Bounds.Height / 2); } if (channel.BorderWidth > 0 && channel.BorderColor != Color.Transparent) { using var pen = new Pen(channel.BorderColor, channel.BorderWidth); if (channel.BorderEdges) { // We want all edges to show equally. // To achieve this, we need to artificially pull the edges in 1px on the right and bottom. g.DrawRectangle( pen, channel.Bounds.Left, channel.Bounds.Top, channel.Bounds.Width - (channel.Bounds.Right == RenderingBounds.Right ? 1 : 0), channel.Bounds.Height - (channel.Bounds.Bottom == RenderingBounds.Bottom ? 1 : 0)); } else { // We want to draw all lines which are not on the rendering bounds if (channel.Bounds.Left != RenderingBounds.Left) { g.DrawLine(pen, channel.Bounds.Left, channel.Bounds.Top, channel.Bounds.Left, channel.Bounds.Bottom); } if (channel.Bounds.Top != RenderingBounds.Top) { g.DrawLine(pen, channel.Bounds.Left, channel.Bounds.Top, channel.Bounds.Right, channel.Bounds.Top); } if (channel.Bounds.Right != RenderingBounds.Right) { g.DrawLine(pen, channel.Bounds.Right, channel.Bounds.Top, channel.Bounds.Right, channel.Bounds.Bottom); } if (channel.Bounds.Bottom != RenderingBounds.Bottom) { g.DrawLine(pen, channel.Bounds.Left, channel.Bounds.Bottom, channel.Bounds.Right, channel.Bounds.Bottom); } } } if (channel.LabelFont != null && channel.LabelColor != Color.Transparent) { g.TextRenderingHint = TextRenderingHint.AntiAlias; using var brush = new SolidBrush(channel.LabelColor); var stringFormat = new StringFormat(); var layoutRectangle = new RectangleF( channel.Bounds.Left + channel.LabelMargins.Left, channel.Bounds.Top + channel.LabelMargins.Top, channel.Bounds.Width - channel.LabelMargins.Left - channel.LabelMargins.Right, channel.Bounds.Height - channel.LabelMargins.Top - channel.LabelMargins.Bottom); switch (channel.LabelAlignment) { case ContentAlignment.TopLeft: stringFormat.Alignment = StringAlignment.Near; stringFormat.LineAlignment = StringAlignment.Near; break; case ContentAlignment.TopCenter: stringFormat.Alignment = StringAlignment.Center; stringFormat.LineAlignment = StringAlignment.Near; break; case ContentAlignment.TopRight: stringFormat.Alignment = StringAlignment.Far; stringFormat.LineAlignment = StringAlignment.Near; break; case ContentAlignment.MiddleLeft: stringFormat.Alignment = StringAlignment.Near; stringFormat.LineAlignment = StringAlignment.Center; break; case ContentAlignment.MiddleCenter: stringFormat.Alignment = StringAlignment.Center; stringFormat.LineAlignment = StringAlignment.Center; break; case ContentAlignment.MiddleRight: stringFormat.Alignment = StringAlignment.Far; stringFormat.LineAlignment = StringAlignment.Center; break; case ContentAlignment.BottomLeft: stringFormat.Alignment = StringAlignment.Near; stringFormat.LineAlignment = StringAlignment.Far; break; case ContentAlignment.BottomCenter: stringFormat.Alignment = StringAlignment.Center; stringFormat.LineAlignment = StringAlignment.Far; break; case ContentAlignment.BottomRight: stringFormat.Alignment = StringAlignment.Far; stringFormat.LineAlignment = StringAlignment.Far; break; default: throw new ArgumentOutOfRangeException(); } g.DrawString(channel.Label, channel.LabelFont, brush, layoutRectangle, stringFormat); } } } private static void RenderWave(SKCanvas g, Channel channel, int triggerPoint, SKPaint linePaint, SKPaint fillPaint, SKPath path, SKPath fillPath, double fillBase) { // And the initial sample index var leftmostSampleIndex = triggerPoint - channel.ViewWidthInSamples / 2; float xOffset = channel.Bounds.Left; float xScale = (float) channel.Bounds.Width / channel.ViewWidthInSamples; float yOffset = channel.Bounds.Top + channel.Bounds.Height * 0.5f; float yScale = -channel.Bounds.Height * 0.5f; path.Rewind(); for (var i = 0; i < channel.ViewWidthInSamples; ++i) { var sampleValue = channel.GetSample(leftmostSampleIndex + i, false); var x = xOffset + i * xScale; var y = yOffset + sampleValue * yScale; if (channel.Clip) { y = Math.Min(Math.Max(y, channel.Bounds.Top), channel.Bounds.Bottom); } if (i == 0) { path.MoveTo(x, y); } else { path.LineTo(x, y); } } // Then draw them all in one go... // Draw the fill "under" the line if (fillPaint != null) { // We need to add points to complete the path // We compute the Y position of this line. -0.5 scales -1..1 to bottom..top. var baseY = (float)(yOffset + channel.Bounds.Height * -0.5 * fillBase); fillPath.Rewind(); fillPath.AddPath(path); fillPath.LineTo(channel.Bounds.Right, baseY); fillPath.LineTo(channel.Bounds.Left, baseY); g.DrawPath(fillPath, fillPaint); } if (linePaint != null) { g.DrawPath(path, linePaint); } } /// /// Version for rendering a single frame for previewing /// public Bitmap RenderFrame(float position = 0) { var frameIndex = _channels.Count > 0 ? (int) (position * _channels.Max(c => c.SampleCount) * FramesPerSecond / SamplingRate) : 0; Bitmap bitmap = null; Render(frameIndex, frameIndex + 1, (bm, _) => { bitmap = bm.ToBitmap(); }, 1); return bitmap; } } } ================================================ FILE: LibVgm/Gd3Tag.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Text; namespace LibVgm { public class Gd3Tag { public struct MultiLanguageTag { public string English { get; set; } public string Japanese { get; set; } public readonly override string ToString() { return English.Length > 0 ? Japanese.Length > 0 ? $"{English} ({Japanese})" : $"{English}" : ""; } } // ReSharper disable MemberCanBePrivate.Global public string Ident { get; private set; } public decimal Version { get; private set; } public MultiLanguageTag Title { get; set; } public MultiLanguageTag Game { get; set; } public MultiLanguageTag System { get; set; } public MultiLanguageTag Composer { get; set; } public string Date { get; set; } public string Ripper { get; set; } public string Notes { get; set; } // ReSharper restore MemberCanBePrivate.Global public static Gd3Tag LoadFromVgm(string filename) { // Open the stream using var s = new OptionalGzipStream(filename); using var r = new BinaryReader(s, Encoding.ASCII); r.ReadBytes(0x14); var offset = r.ReadUInt32() + 0x14; if (offset == 0) { // No tag return null; } if (offset > s.Length - 8 - 11*2) { throw new InvalidDataException("Not enough room in file for GD3 offset"); } var result = new Gd3Tag(); result.Parse(s, offset); return result; } public void Parse(Stream s, uint offset) { var tags = new List(); using (var r = new BinaryReader(s, Encoding.Unicode, true)) { s.Seek(offset, SeekOrigin.Begin); Ident = Encoding.ASCII.GetString(r.ReadBytes(4)); if (Ident != "Gd3 ") { throw new InvalidDataException("GD3 header not found"); } var version = r.ReadUInt32(); // BCD to integer int scaled = 0; int factor = 1; for (int i = 0; i < 8; ++i) { var digit = (int) version & 0xf; scaled += digit * factor; version >>= 4; factor *= 10; } Version = (decimal) scaled / 100; if (Version >= 2.00m) { throw new Exception($"GD3 version {Version} not supported"); } var length = r.ReadUInt32(); if (s.Length - s.Position > length) { throw new Exception("File not big enough for GD3 data"); } // We read out 11 UCS-2 strings var str = ""; for (int i = 0; i < 11; ++i) { for (;;) { var c = r.ReadChar(); if (c == '\0') { tags.Add(str); str = ""; break; } str += c; } } } // We then put them into our properties Title = new MultiLanguageTag {English = tags[0], Japanese = tags[1]}; Game = new MultiLanguageTag {English = tags[2], Japanese = tags[3]}; System = new MultiLanguageTag {English = tags[4], Japanese = tags[5]}; Composer = new MultiLanguageTag {English = tags[6], Japanese = tags[7]}; Date = tags[8]; Ripper = tags[9]; Notes = tags[10]; } public override string ToString() { var title = Title.ToString(); var game = Game.ToString(); var system = System.English; var composer = Composer.ToString(); var sb = new StringBuilder(); // Track Title - Game - System (Date) // Composer // Ripped by Ripper if (title.Length > 0) sb.Append(title); if (game.Length > 0) sb.Append($" – {game}"); if (system.Length > 0) sb.Append($" – {system}"); if (Date.Length > 0) sb.Append($" ({Date})"); if (composer.Length > 0) { sb.AppendLine(); sb.Append(composer); } if (Ripper.Length > 0) { sb.AppendLine(); sb.Append($"Ripped by {Ripper}"); } return sb.ToString(); } } } ================================================ FILE: LibVgm/LibVgm.csproj ================================================ netstandard2.0 latest true ================================================ FILE: LibVgm/OptionalGzipStream.cs ================================================ using System.IO; using System.IO.Compression; namespace LibVgm { /// /// Stream class which transparently supports GZipped or uncompressed files /// internal class OptionalGzipStream : Stream { private readonly Stream _stream; public OptionalGzipStream(string filename) { var fileStream = new FileStream(filename, FileMode.Open); // Check if it's GZipped bool needGzip = fileStream.ReadByte() == 0x1f && fileStream.ReadByte() == 0x8b; fileStream.Seek(0, SeekOrigin.Begin); if (needGzip) { using var gZipStream = new GZipStream(fileStream, CompressionMode.Decompress); _stream = new MemoryStream(); gZipStream.CopyTo(_stream); _stream.Seek(0, SeekOrigin.Begin); } else { _stream = fileStream; } } public override void Flush() => _stream.Flush(); public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); public override void SetLength(long value) => _stream.SetLength(value); public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count); public override void Write(byte[] buffer, int offset, int count) => _stream.Write(buffer, offset, count); public override bool CanRead => _stream.CanRead; public override bool CanSeek => _stream.CanSeek; public override bool CanWrite => _stream.CanWrite; public override long Length => _stream.Length; public override long Position { get => _stream.Position; set => _stream.Position = value; } protected override void Dispose(bool disposing) { _stream?.Dispose(); base.Dispose(disposing); } } } ================================================ FILE: LibVgm/VgmFile.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Text; namespace LibVgm { public class VgmFile: IDisposable { // It's painful to seek in GZipped streams, so we don't bother... private readonly MemoryStream _stream; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable MemberCanBeProtected.Global public VgmHeader Header { get; } public Gd3Tag Gd3Tag { get; } public class VgmHeader { public string Ident { get; set; } public uint EndOfFileOffset { get; set; } public decimal Version { get; set; } public uint Sn76489Clock { get; set; } public uint Ym2413Clock { get; set; } public uint Gd3Offset { get; set; } public uint TotalSamples { get; set; } public uint LoopOffset { get; set; } public uint LoopSamples { get; set; } public uint Rate { get; set; } public uint SnFeedback { get; set; } public uint SnWidth { get; set; } public uint SnFlag { get; set; } public uint Ym2612Clock { get; set; } public uint Ym2151Clock { get; set; } public uint DataOffset { get; set; } = 0x40; public uint SegaPcmClock { get; set; } public uint SpcmInterface { get; set; } public uint Rf5C68Clock { get; set; } public uint Ym2203Clock { get; set; } public uint Ym2608Clock { get; set; } public uint Ym2610BClock { get; set; } public uint Ym3812Clock { get; set; } public uint Ym3526Clock { get; set; } public uint Y8950Clock { get; set; } public uint Ymf262Clock { get; set; } public uint Ymf278BClock { get; set; } public uint Ymf271Clock { get; set; } public uint Ymz280BClock { get; set; } public uint Rf5C164Clock { get; set; } public uint PwmClock { get; set; } public uint Ay8910Clock { get; set; } public uint AyType { get; set; } public uint AyFlags { get; set; } public uint VolumeModifier { get; set; } public uint LoopBase { get; set; } public uint LoopModifier { get; set; } public uint GbDmgClock { get; set; } public uint NesApuClock { get; set; } public uint MultiPcmClock { get; set; } public uint Upd7759Clock { get; set; } public uint Okim6258Clock { get; set; } public uint Okim6258Flags { get; set; } public uint K054539Flags { get; set; } public uint C140ChipType { get; set; } public uint Okim6295Clock { get; set; } public uint K051649Clock { get; set; } public uint K054539Clock { get; set; } public uint HuC6280Clock { get; set; } public uint C140Clock { get; set; } public uint K053260Clock { get; set; } public uint PokeyClock { get; set; } public uint QSoundClock { get; set; } public uint ScspClock { get; set; } public uint ExtraHeaderOffset { get; set; } public uint WonderSwanClock { get; set; } public uint VsuClock { get; set; } public uint Saa1099Clock { get; set; } public uint Es5503Clock { get; set; } public uint Es5506Clock { get; set; } public uint Es5503Channels { get; set; } public uint Es5506Channels { get; set; } public uint X1010Clock { get; set; } public uint C352Clock { get; set; } public uint Ga20Clock { get; set; } internal VgmHeader() { Ident = "Vgm "; Version = 1.10m; } internal void Parse(Stream s) { using var r = new BinaryReader(s, Encoding.ASCII, true); // VGM 1.00 Ident = string.Concat(r.ReadChars(4)); EndOfFileOffset = r.ReadUInt32(); if (EndOfFileOffset == 0) { EndOfFileOffset = (uint) s.Length; } else { EndOfFileOffset += 4; // Make absolute } var version = r.ReadUInt32(); // BCD to integer int scaled = 0; int factor = 1; for (int i = 0; i < 8; ++i) { var digit = (int) version & 0xf; scaled += digit * factor; version >>= 4; factor *= 10; } Version = (decimal) scaled / 100; Sn76489Clock = r.ReadUInt32(); Ym2413Clock = r.ReadUInt32(); Gd3Offset = r.ReadUInt32(); if (Gd3Offset != 0) { Gd3Offset += 0x14; // Make absolute } TotalSamples = r.ReadUInt32(); LoopOffset = r.ReadUInt32(); if (LoopOffset > 0) { LoopOffset += 0x1c; // Make absolute } LoopSamples = r.ReadUInt32(); if (Version > 1.01m) { Rate = r.ReadUInt32(); } // VGM 1.10 if (Version > 1.10m) { SnFeedback = r.ReadUInt16(); SnWidth = r.ReadByte(); if (Version > 1.51m) { SnFlag = r.ReadByte(); } else { r.ReadByte(); } Ym2612Clock = r.ReadUInt32(); Ym2151Clock = r.ReadUInt32(); if (Version > 1.50m) { DataOffset = r.ReadUInt32(); if (DataOffset == 0) { DataOffset = 0x40; // Assume default } else { DataOffset += 0x34; // Make absolute } if (Version > 1.51m) { SegaPcmClock = r.ReadUInt32(); SpcmInterface = r.ReadUInt32(); Rf5C68Clock = r.ReadUInt32(); Ym2203Clock = r.ReadUInt32(); Ym2608Clock = r.ReadUInt32(); Ym2610BClock = r.ReadUInt32(); Ym3812Clock = r.ReadUInt32(); Ym3526Clock = r.ReadUInt32(); Y8950Clock = r.ReadUInt32(); Ymf262Clock = r.ReadUInt32(); Ymf278BClock = r.ReadUInt32(); Ymf271Clock = r.ReadUInt32(); Ymz280BClock = r.ReadUInt32(); Rf5C164Clock = r.ReadUInt32(); PwmClock = r.ReadUInt32(); Ay8910Clock = r.ReadUInt32(); var n = r.ReadUInt32(); AyType = n >> 24; AyFlags = n & 0xffffff; if (Version > 1.60m) { VolumeModifier = r.ReadByte(); r.ReadByte(); LoopBase = r.ReadByte(); } else { r.ReadBytes(3); } LoopModifier = r.ReadByte(); if (Version > 1.61m) { GbDmgClock = r.ReadUInt32(); NesApuClock = r.ReadUInt32(); MultiPcmClock = r.ReadUInt32(); Upd7759Clock = r.ReadUInt32(); Okim6258Clock = r.ReadUInt32(); Okim6258Flags = r.ReadByte(); K054539Flags = r.ReadByte(); C140ChipType = r.ReadByte(); r.ReadByte(); Okim6295Clock = r.ReadUInt32(); K051649Clock = r.ReadUInt32(); K054539Clock = r.ReadUInt32(); HuC6280Clock = r.ReadUInt32(); C140Clock = r.ReadUInt32(); K053260Clock = r.ReadUInt32(); PokeyClock = r.ReadUInt32(); QSoundClock = r.ReadUInt32(); if (Version > 1.70m) { if (Version > 1.71m) { ScspClock = r.ReadUInt32(); } else { r.ReadUInt32(); } ExtraHeaderOffset = (uint) r.BaseStream.Position + r.ReadUInt32(); if (Version > 1.71m) { WonderSwanClock = r.ReadUInt32(); VsuClock = r.ReadUInt32(); Saa1099Clock = r.ReadUInt32(); Es5503Clock = r.ReadUInt32(); Es5506Clock = r.ReadUInt32(); Es5503Channels = r.ReadUInt16(); Es5506Channels = r.ReadByte(); r.ReadByte(); X1010Clock = r.ReadUInt32(); C352Clock = r.ReadUInt32(); Ga20Clock = r.ReadUInt32(); } } } } } } } } public interface ICommand { } public class GenericCommand: ICommand { public byte[] Values { get; set; } public GenericCommand(BinaryReader reader, int count) { Values = reader.ReadBytes(count); } public override string ToString() => $"Generic command {Values.Length} bytes"; } public class AddressDataCommand: ICommand { public byte Type { get; } public byte Address { get; set; } public byte Data { get; set; } public AddressDataCommand(BinaryReader reader, byte type) { Type = type; Address = reader.ReadByte(); Data = reader.ReadByte(); } public override string ToString() => $"Type {Type:X} Address {Address:X} Data {Data:X}"; } public class WaitCommand : ICommand { public int Ticks { get; set; } public WaitCommand(BinaryReader reader, byte type) { switch (type) { case 0x61: Ticks = reader.ReadUInt16(); break; case 0x62: Ticks = 735; break; case 0x63: Ticks = 882; break; case 0x70: case 0x71: case 0x72: case 0x73: case 0x74: case 0x75: case 0x76: case 0x77: case 0x78: case 0x79: case 0x7a: case 0x7b: case 0x7c: case 0x7d: case 0x7e: case 0x7f: Ticks = (type & 0xf) + 1; break; } } public override string ToString() => $"Wait {Ticks} samples"; } public class SampleWaitCommand : WaitCommand { public SampleWaitCommand(BinaryReader reader, byte type) : base(reader, type) { // Sample waits are one less --Ticks; } public override string ToString() => $"Wait {Ticks} samples and play sample"; } public class StopCommand : ICommand { public override string ToString() => "Stop"; } public class DataBlock : ICommand { public byte BlockType { get; set; } public byte[] Data { get; set; } public DataBlock(BinaryReader reader) { reader.ReadByte(); // Skip the stop command BlockType = reader.ReadByte(); var count = reader.ReadInt32(); Data = reader.ReadBytes(count); } public override string ToString() => $"Data block: type {BlockType:X} size {Data}"; } public class PcmRamWrite : ICommand { public byte ChipType { get; set; } public int ReadOffset { get; set; } public int Count { get; set; } public int WriteOffset { get; set; } public PcmRamWrite(BinaryReader reader) { reader.ReadByte(); // Skip the stop command ChipType = reader.ReadByte(); ReadOffset = reader.ReadUInt16() + reader.ReadByte() << 16; // 24-bit read WriteOffset = reader.ReadUInt16() + reader.ReadByte() << 16; // 24-bit read Count = reader.ReadUInt16() + reader.ReadByte() << 16; // 24-bit read if (Count == 0) { Count = 1 << 24; } } } public abstract class DacStreamCommand : ICommand { public byte StreamId { get; set; } protected DacStreamCommand(BinaryReader reader) { StreamId = reader.ReadByte(); } } public class DacStreamSetupCommand : DacStreamCommand { public DacStreamSetupCommand(BinaryReader reader) : base(reader) { ChipType = reader.ReadByte(); Port = reader.ReadByte(); Command = reader.ReadByte(); } public byte Command { get; set; } public byte Port { get; set; } public byte ChipType { get; set; } } public class DacStreamDataCommand: DacStreamCommand { public DacStreamDataCommand(BinaryReader reader) : base(reader) { BankId = reader.ReadByte(); StepSize = reader.ReadByte(); StepBase = reader.ReadByte(); } public byte StepBase { get; set; } public byte StepSize { get; set; } public byte BankId { get; set; } } public class DacStreamFrequencyCommand: DacStreamCommand { public DacStreamFrequencyCommand(BinaryReader reader) : base(reader) { Frequency = reader.ReadUInt32(); } public uint Frequency { get; set; } } public class DacStreamStartCommand: DacStreamCommand { public DacStreamStartCommand(BinaryReader reader) : base(reader) { Offset = reader.ReadUInt32(); LengthMode = reader.ReadByte(); Count = reader.ReadUInt32(); } public uint Count { get; set; } public byte LengthMode { get; set; } public uint Offset { get; set; } } public class DacStreamStopCommand : DacStreamCommand { public DacStreamStopCommand(BinaryReader reader) : base(reader) {} } public class DacStreamFastStartCommand : DacStreamCommand { public DacStreamFastStartCommand(BinaryReader reader) : base(reader) { BlockId = reader.ReadUInt16(); Flags = reader.ReadByte(); } public byte Flags { get; set; } public ushort BlockId { get; set; } } // ReSharper restore MemberCanBeProtected.Global // ReSharper restore UnusedAutoPropertyAccessor.Global // ReSharper restore MemberCanBePrivate.Global // ReSharper restore AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable once UnusedMember.Global public VgmFile() { // Empty file _stream = new MemoryStream(); Header = new VgmHeader(); Gd3Tag = new Gd3Tag(); } public VgmFile(string filename) { _stream = new MemoryStream(); Header = new VgmHeader(); Gd3Tag = new Gd3Tag(); LoadFromFile(filename); } private void LoadFromFile(string filename) { // We copy all the data into a memory stream to allow seeking using (var s = new OptionalGzipStream(filename)) { s.CopyTo(_stream); _stream.Seek(0, SeekOrigin.Begin); } // We parse the header Header.Parse(_stream); // And the GD3 tag, if present if (Header.Gd3Offset != 0) { Gd3Tag.Parse(_stream, Header.Gd3Offset); } } public IEnumerable Commands() { // Seek to the start _stream.Seek(Header.DataOffset, SeekOrigin.Begin); using var reader = new BinaryReader(_stream, Encoding.Default, true); while (_stream.Position < _stream.Length) { //var b = reader.ReadByte(); switch (reader.ReadByte()) { case <= 0x2f: // Unhandled continue; case <= 0x3f: yield return new GenericCommand(reader, 1); // Reserved range break; case <= 0x4e: yield return new GenericCommand(reader, 2); // Reserved range break; case <= 0x50: yield return new GenericCommand(reader, 1); // GG stereo or PSG break; case var b and <= 0x5f: yield return new AddressDataCommand(reader, b); // FM chips break; case 0x60: // Unhandled continue; case var b and <= 0x63: yield return new WaitCommand(reader, b); break; case <= 0x65: // Unhandled continue; case 0x66: yield return new StopCommand(); yield break; case 0x67: yield return new DataBlock(reader); break; case 0x68: yield return new PcmRamWrite(reader); break; case < 0x6f: // Unhandled continue; case var b and >= 0x70 and <= 0x7f: yield return new WaitCommand(reader, b); break; case var b and <= 0x8f: yield return new SampleWaitCommand(reader, b); break; case 0x90: yield return new DacStreamSetupCommand(reader); break; case 0x91: yield return new DacStreamDataCommand(reader); break; case 0x92: yield return new DacStreamFrequencyCommand(reader); break; case 0x93: yield return new DacStreamStartCommand(reader); break; case 0x94: yield return new DacStreamStopCommand(reader); break; case 0x95: yield return new DacStreamFastStartCommand(reader); break; case <= 0x9f: // Unhandled continue; case var b and 0xa0: yield return new AddressDataCommand(reader, b); break; case <= 0xaf: yield return new GenericCommand(reader, 2); // Reserved range break; case var b and <= 0xbf: yield return new AddressDataCommand(reader, b); break; case <= 0xdf: yield return new GenericCommand(reader, 3); // Reserved + some allocated break; default: yield return new GenericCommand(reader, 4); // Reserved + some allocated break; } } } public void Dispose() { _stream?.Dispose(); GC.SuppressFinalize(this); } } } ================================================ FILE: README.md ================================================ # SidWizPlus ![](https://github.com/maxim-zhao/SidWizPlus/wiki/logo.png) This is a program which generates "oscilloscope view" videos from multi-track audio files. It is often used for VGM/chiptune rendering for use on video sharing sites, but can also be used for other multitrack audio files. [![](http://img.youtube.com/vi/H-Ip9c0yjGk/0.jpg)](http://www.youtube.com/watch?v=H-Ip9c0yjGk "Sonic 3 - Ice Cap Zone - Brad Buxer") [![](http://img.youtube.com/vi/ITQFs6-1LSg/0.jpg)](http://www.youtube.com/watch?v=ITQFs6-1LSg "Bohemian Rhapsody - Queen") The primary goals of this project are: 1. Generating videos from VGM packs from [SMS Power!](http://www.smspower.org/Music) - [see them on YouTube](https://www.youtube.com/channel/UCCsvqzh7JjNNheYTplGvhCQ) 2. Producing a base for others to work on the features they want Get builds from here: https://github.com/maxim-zhao/SidWizPlus/releases/ [![Build status](https://ci.appveyor.com/api/projects/status/vpa5eav7sm1n7ik6?svg=true)](https://ci.appveyor.com/project/maxim-zhao/sidwizplus) You can get a compatible build of MultiDumper here: https://github.com/maxim-zhao/multidumper/releases/ ## Features added * Commandline mode * Replaced renderer with Skia, allowing simpler code and more advanced rendering: * Antialiasing * Background image * Line width control * Optional fill * Alpha blending * Multi-threaded rendering * Added rendering features from other variants * Grid * Channel labels * Integration with [MultiDumper](https://bitbucket.org/losnoco/multidumper) to generate tracks from a VGM * Including automatic removal of unused tracks * Waveform scaling * Including auto-scaling (e.g. scale peak to 100%) * Automatic master audio track generation, with optional ReplayGain * One-shot audio+video file creation via FFMPEG with optional preview * Or run in preview-only mode * Unlimited tracks and columns * Render to any size video * YouTube uploading (commandline only) * Including generation of titles and descriptions from tags in VGM files * New GUI using the same renderer to give previews as you change settings * Almost-live updates as you edit settings * Preview rendering and data loading on background threads so it's pretty fast * Select your preview location as you go * Most parameters editable per-channel * Save and load all settings * Settings files are JSON so you can edit them yourself to make them "partial" * Copy and paste channel settings * Also as JSON so you can save them as text ## Usage guide [Read the usage guide on the wiki](https://github.com/maxim-zhao/SidWizPlus/wiki) ================================================ FILE: SidWiz/ColorButton.Designer.cs ================================================ namespace SidWizPlusGUI { partial class ColorButton { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Component Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { components = new System.ComponentModel.Container(); } #endregion } } ================================================ FILE: SidWiz/ColorButton.cs ================================================ using System; using System.ComponentModel; using System.Drawing; using System.Windows.Forms; namespace SidWizPlusGUI { /// /// /// Button that lets you pick a color /// public partial class ColorButton : Button { public event EventHandler ColorChanged; private Color _color; public Color Color { get => _color; set { _color = value; BackColor = value; ForeColor = _color.GetBrightness() < 0.5 ? Color.White : Color.Black; Text = _color.Name; ColorChanged?.Invoke(this, EventArgs.Empty); } } public ColorButton() { InitializeComponent(); } public ColorButton(IContainer container) { container.Add(this); InitializeComponent(); Click += OnClick; } private void OnClick(object sender, EventArgs e) { using var colorDialog = new Cyotek.Windows.Forms.ColorPickerDialog(); colorDialog.Color = _color; colorDialog.ShowAlphaChannel = true; if (colorDialog.ShowDialog(this) != DialogResult.OK) { return; } Color = colorDialog.Color; } } } ================================================ FILE: SidWiz/HighDpiHelper.cs ================================================ using System; using System.Collections; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace SidWizPlusGUI { public static class HighDpiHelper { public static void AdjustControlImagesDpiScale(Control container) { var dpiScale = GetDpiScale(container).Value; if (CloseToOne(dpiScale)) { return; } AdjustControlImagesDpiScale(container.Controls, dpiScale); } private static void AdjustControlImagesDpiScale(IEnumerable controls, float dpiScale) { foreach (Control control in controls) { switch (control) { case ButtonBase { Image: not null } button: button.Image = ScaleImage(button.Image, dpiScale); break; case SplitContainer splitContainer: splitContainer.SplitterDistance = (int)(splitContainer.SplitterDistance * dpiScale); break; case TabControl { ImageList: not null } tabControl: { var imageList = new ImageList { ImageSize = ScaleSize(tabControl.ImageList.ImageSize, dpiScale), ColorDepth = ColorDepth.Depth32Bit }; for (var i = 0 ; i < tabControl.ImageList.Images.Count; ++i) { imageList.Images.Add( tabControl.ImageList.Images.Keys[i], ScaleImage(tabControl.ImageList.Images[i], dpiScale)); } tabControl.ImageList = imageList; break; } case ToolStrip toolStrip: ScaleToolStrip(dpiScale, toolStrip); toolStrip.AutoSize = true; break; } if (control.ContextMenuStrip != null) { ScaleToolStrip(dpiScale, control.ContextMenuStrip); } // Then recurse AdjustControlImagesDpiScale(control.Controls, dpiScale); } } private static void ScaleToolStrip(float dpiScale, ToolStrip toolStrip) { toolStrip.ImageScalingSize = ScaleSize(toolStrip.ImageScalingSize, dpiScale); foreach (var item in toolStrip.Items.Cast().Where(i => i.Image != null)) { item.Image = ScaleImage(item.Image, dpiScale); } } private static bool CloseToOne(float dpiScale) { return Math.Abs(dpiScale - 1) < 0.001; } private static Lazy GetDpiScale(Control control) { return new Lazy(() => { using var graphics = control.CreateGraphics(); return graphics.DpiX / 96.0f; }); } private static Image ScaleImage(Image image, float dpiScale) { var newSize = ScaleSize(image.Size, dpiScale); var newBitmap = new Bitmap(newSize.Width, newSize.Height); using (var g = Graphics.FromImage(newBitmap)) { g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.InterpolationMode = InterpolationMode.NearestNeighbor; g.DrawImage(image, new Rectangle(new Point(), newSize)); } image.Dispose(); return newBitmap; } private static Size ScaleSize(Size size, float scale) { return new Size((int) (size.Width * scale), (int) (size.Height * scale)); } } } ================================================ FILE: SidWiz/MultiDumperForm.Designer.cs ================================================ using System.ComponentModel; using System.Windows.Forms; namespace SidWizPlusGUI { partial class MultiDumperForm { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { this.Subsongs = new System.Windows.Forms.ListBox(); this.OKButton = new System.Windows.Forms.Button(); this.ProgressBar = new System.Windows.Forms.ProgressBar(); this.label1 = new System.Windows.Forms.Label(); this.lengthBox = new System.Windows.Forms.TextBox(); this.SuspendLayout(); // // Subsongs // this.Subsongs.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.Subsongs.FormattingEnabled = true; this.Subsongs.IntegralHeight = false; this.Subsongs.Items.AddRange(new object[] { " "}); this.Subsongs.Location = new System.Drawing.Point(12, 12); this.Subsongs.Name = "Subsongs"; this.Subsongs.Size = new System.Drawing.Size(663, 237); this.Subsongs.TabIndex = 0; this.Subsongs.SelectedIndexChanged += new System.EventHandler(this.Subsongs_SelectedIndexChanged); this.Subsongs.DoubleClick += new System.EventHandler(this.OkButtonClick); // // OKButton // this.OKButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.OKButton.Enabled = false; this.OKButton.Location = new System.Drawing.Point(600, 291); this.OKButton.Name = "OKButton"; this.OKButton.Size = new System.Drawing.Size(75, 23); this.OKButton.TabIndex = 4; this.OKButton.Text = "OK"; this.OKButton.UseVisualStyleBackColor = true; this.OKButton.Click += new System.EventHandler(this.OkButtonClick); // // ProgressBar // this.ProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.ProgressBar.Location = new System.Drawing.Point(12, 291); this.ProgressBar.Name = "ProgressBar"; this.ProgressBar.Size = new System.Drawing.Size(582, 23); this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Continuous; this.ProgressBar.TabIndex = 3; // // label1 // this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); this.label1.AutoSize = true; this.label1.Enabled = false; this.label1.Location = new System.Drawing.Point(9, 263); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(102, 13); this.label1.TabIndex = 1; this.label1.Text = "Song length (mm:ss)"; // // lengthBox // this.lengthBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); this.lengthBox.Enabled = false; this.lengthBox.Location = new System.Drawing.Point(117, 260); this.lengthBox.Name = "lengthBox"; this.lengthBox.Size = new System.Drawing.Size(100, 20); this.lengthBox.TabIndex = 2; // // MultiDumperForm // this.AcceptButton = this.OKButton; this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(687, 327); this.Controls.Add(this.lengthBox); this.Controls.Add(this.label1); this.Controls.Add(this.ProgressBar); this.Controls.Add(this.OKButton); this.Controls.Add(this.Subsongs); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "MultiDumperForm"; this.ShowIcon = false; this.ShowInTaskbar = false; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "Multidumper subsong selection"; this.Closing += new System.ComponentModel.CancelEventHandler(this.SubsongSelectionForm_Closing); this.Load += new System.EventHandler(this.SubSongSelectionForm_Load); this.ResumeLayout(false); this.PerformLayout(); } #endregion private System.Windows.Forms.ListBox Subsongs; private System.Windows.Forms.Button OKButton; private System.Windows.Forms.ProgressBar ProgressBar; private Label label1; private TextBox lengthBox; } } ================================================ FILE: SidWiz/MultiDumperForm.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; using LibSidWiz; namespace SidWizPlusGUI { public partial class MultiDumperForm : Form { private readonly string _filename; private readonly MultiDumperWrapper _wrapper; public IEnumerable Filenames { get; private set; } public MultiDumperForm(string filename, string multiDumperPath, int samplingRate, int loopCount, int fadeMs, int gapMs, string extraOptions) { _filename = filename; _wrapper = new MultiDumperWrapper(multiDumperPath, samplingRate, loopCount, fadeMs, gapMs, extraOptions); InitializeComponent(); } private void OkButtonClick(object sender, EventArgs e) { if (!(Subsongs.SelectedItem is MultiDumperWrapper.Song song)) { return; } if (song.GetLength() <= TimeSpan.Zero) { // Try to parse the text box if (!TimeSpan.TryParseExact(lengthBox.Text, "m\\:ss", null, out var length)) { return; } song.ForceLength = length; } OKButton.Enabled = false; // We start a task to wrap the load task Task.Factory.StartNew(() => { try { Filenames = _wrapper.Dump(song, progress => { ProgressBar.BeginInvoke( new Action(() => ProgressBar.Value = (int) (progress * 100))); }).ToList(); BeginInvoke(new Action(() => { DialogResult = DialogResult.OK; Close(); })); } catch (Exception) { BeginInvoke(new Action(() => { DialogResult = DialogResult.Cancel; Filenames = null; Close(); })); } }); } private void SubSongSelectionForm_Load(object sender, EventArgs e) { Subsongs.Items.Clear(); Subsongs.Items.Add($"Checking {_filename}..."); // Start a task to load the metadata Task.Factory.StartNew(() => { try { var songs = _wrapper.GetSongs(_filename).ToList(); Subsongs.BeginInvoke(new Action(() => { // Back on the GUI thread... Subsongs.Items.Clear(); Subsongs.Items.AddRange(songs.ToArray()); Subsongs.SelectedIndex = 0; OKButton.Enabled = true; if (songs.Count == 1 && songs[0].GetLength() > TimeSpan.Zero) { // If only one song, and it has a length, choose it OkButtonClick(this, EventArgs.Empty); } })); } catch (Exception ex) { Subsongs.BeginInvoke(new Action(() => { // Back on the GUI thread... Subsongs.Items.Add($"Failed to read {_filename}: {ex.Message}"); })); } }); } private void SubsongSelectionForm_Closing(object sender, EventArgs e) { _wrapper.Dispose(); } private void Subsongs_SelectedIndexChanged(object sender, EventArgs e) { // We enable the length controls if the track is missing info label1.Enabled = lengthBox.Enabled = Subsongs.SelectedItem is MultiDumperWrapper.Song s && s.GetLength() <= TimeSpan.Zero; } } } ================================================ FILE: SidWiz/MultiDumperForm.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ================================================ FILE: SidWiz/Program.cs ================================================ using System; using System.Windows.Forms; namespace SidWizPlusGUI { internal static class Program { /// /// The main entry point for the application. /// [STAThread] private static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new SidWizPlusGui()); } } } ================================================ FILE: SidWiz/Properties/Resources.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace SidWizPlusGUI.Properties { using System; /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SidWizPlusGUI.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// internal static System.Drawing.Bitmap lightning { get { object obj = ResourceManager.GetObject("lightning", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// internal static System.Drawing.Bitmap page { get { object obj = ResourceManager.GetObject("page", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// internal static System.Drawing.Bitmap page_refresh { get { object obj = ResourceManager.GetObject("page_refresh", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// internal static System.Drawing.Bitmap page_save { get { object obj = ResourceManager.GetObject("page_save", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } } } ================================================ FILE: SidWiz/Properties/Resources.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ..\Resources\page_refresh.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\page_save.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\lightning.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\page.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ================================================ FILE: SidWiz/Properties/app.manifest ================================================  true/PM ================================================ FILE: SidWiz/SidPlayForm.Designer.cs ================================================ using System.Windows.Forms; namespace SidWizPlusGUI { partial class SidPlayForm { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { this.Subsongs = new System.Windows.Forms.ListBox(); this.OKButton = new System.Windows.Forms.Button(); this.ProgressBar = new System.Windows.Forms.ProgressBar(); this.SuspendLayout(); // // Subsongs // this.Subsongs.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.Subsongs.FormattingEnabled = true; this.Subsongs.IntegralHeight = false; this.Subsongs.ItemHeight = 25; this.Subsongs.Items.AddRange(new object[] { " "}); this.Subsongs.Location = new System.Drawing.Point(15, 15); this.Subsongs.Margin = new System.Windows.Forms.Padding(6); this.Subsongs.Name = "Subsongs"; this.Subsongs.Size = new System.Drawing.Size(998, 364); this.Subsongs.TabIndex = 0; this.Subsongs.DoubleClick += new System.EventHandler(this.OkButtonClick); // // OKButton // this.OKButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.OKButton.Enabled = false; this.OKButton.Location = new System.Drawing.Point(863, 391); this.OKButton.Margin = new System.Windows.Forms.Padding(6); this.OKButton.Name = "OKButton"; this.OKButton.Size = new System.Drawing.Size(150, 44); this.OKButton.TabIndex = 2; this.OKButton.Text = "OK"; this.OKButton.UseVisualStyleBackColor = true; this.OKButton.Click += new System.EventHandler(this.OkButtonClick); // // ProgressBar // this.ProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.ProgressBar.Location = new System.Drawing.Point(15, 391); this.ProgressBar.Margin = new System.Windows.Forms.Padding(6); this.ProgressBar.Name = "ProgressBar"; this.ProgressBar.Size = new System.Drawing.Size(836, 44); this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Continuous; this.ProgressBar.TabIndex = 4; // // SidPlayForm // this.AcceptButton = this.OKButton; this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(1028, 450); this.Controls.Add(this.ProgressBar); this.Controls.Add(this.OKButton); this.Controls.Add(this.Subsongs); this.Margin = new System.Windows.Forms.Padding(6); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "SidPlayForm"; this.ShowIcon = false; this.ShowInTaskbar = false; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "SidPlay subsong selection"; this.Closing += new System.ComponentModel.CancelEventHandler(this.SubSongSelectionForm_Closing); this.Load += new System.EventHandler(this.SubSongSelectionForm_Load); this.ResumeLayout(false); } #endregion private System.Windows.Forms.ListBox Subsongs; private System.Windows.Forms.Button OKButton; private ProgressBar ProgressBar; } } ================================================ FILE: SidWiz/SidPlayForm.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using LibSidWiz; namespace SidWizPlusGUI { public partial class SidPlayForm : Form { public class SidFile { public string Filename { get; } public string Released { get; } public string Name { get; } public string Author { get; } public int SongCount { get; } public string Chip { get; } // ReSharper disable once MemberCanBePrivate.Global public int Version { get; } public SidFile(string filename) { Filename = filename; using var f = new FileStream(filename, FileMode.Open); using var r = new BinaryReader(f, Encoding.ASCII); // C64 is big-endian... f.Seek(0x04, SeekOrigin.Begin); Version = r.ReadByte() << 8 | r.ReadByte(); f.Seek(0x0e, SeekOrigin.Begin); SongCount = r.ReadByte() << 8 | r.ReadByte(); f.Seek(0x16, SeekOrigin.Begin); Name = new string(r.ReadChars(32)).TrimEnd('\0'); Author = new string(r.ReadChars(32)).TrimEnd('\0'); Released = new string(r.ReadChars(32)).TrimEnd('\0'); if (Version >= 2) { f.Seek(0x76, SeekOrigin.Begin); var flags = r.ReadByte() << 8 | r.ReadByte(); switch ((flags >> 4) & 0b11) { case 0b00: Chip = "SID"; break; case 0b01: Chip = "MOS6581"; break; case 0b10: Chip = "MOS8580"; break; case 0b11: Chip = "MOS6581 & MOS8580"; break; } } } } public class SidPlayWrapper : IDisposable { private readonly string _sidPlayPath; private IList _processWrappers; public SidPlayWrapper(string sidPlayPath) { _sidPlayPath = sidPlayPath; } public class Song { public SidFile File { get; set; } public int Index { get; set; } public override string ToString() { return $"#{Index}: {File.Name} - {File.Author} - {File.Released}"; } } public IEnumerable GetSongs(string filename) { filename = Path.GetFullPath(filename); if (!File.Exists(filename)) { throw new FileNotFoundException("Cannot find file", filename); } var file = new SidFile(filename); return Enumerable.Range(1, file.SongCount).Select(index => new Song {File = file, Index = index}); } public IEnumerable Dump(Song song) { var filenames = new List(); _processWrappers = Enumerable.Range(1, 4).Select(channel => { var muting = string.Join(" ", Enumerable.Range(1, 4).Where(n => n != channel).Select(n => $"-u{n}")); var filename = $"{song.File.Filename} - Song {song.Index} - {song.File.Chip} #{channel}.wav"; filenames.Add(filename); return new ProcessWrapper( _sidPlayPath, $"{muting} -os -o{song.Index} -w\"{filename}\" \"{song.File.Filename}\""); }).ToList(); // sidplayfp doesn't emit anything to stdout... so we just block until they all exit foreach (var wrapper in _processWrappers) { wrapper.WaitForExit(); wrapper.Dispose(); } _processWrappers.Clear(); // Older sidplayfp versions use the filename we give; newer ones always add .wav to the end. // We check which one appeared. return filenames.Select(x => File.Exists(x + ".wav") ? x + ".wav" : x); } public void Dispose() { foreach (var wrapper in _processWrappers) { wrapper.Dispose(); } } } private readonly string _filename; private readonly SidPlayWrapper _wrapper; public IEnumerable Filenames { get; private set; } public SidPlayForm(string filename, string sidPlayPath) { _filename = filename; _wrapper = new SidPlayWrapper(sidPlayPath); InitializeComponent(); } private void OkButtonClick(object sender, EventArgs e) { OKButton.Enabled = false; if (Subsongs.SelectedItem is SidPlayWrapper.Song song) { ProgressBar.Style = ProgressBarStyle.Marquee; // We start a task to wrap the load task Task.Factory.StartNew(() => { try { Filenames = _wrapper.Dump(song).ToList(); BeginInvoke(new Action(() => { DialogResult = DialogResult.OK; Close(); })); } catch (Exception) { BeginInvoke(new Action(() => { DialogResult = DialogResult.Cancel; Filenames = null; Close(); })); } }); } } private void SubSongSelectionForm_Load(object sender, EventArgs e) { Subsongs.Items.Clear(); Subsongs.Items.Add($"Checking {_filename}..."); // Start a task to load the metadata Task.Factory.StartNew(() => { var songs = _wrapper.GetSongs(_filename); Subsongs.BeginInvoke(new Action(() => { // Back on the GUI thread... Subsongs.Items.Clear(); Subsongs.Items.AddRange(songs.ToArray()); Subsongs.SelectedIndex = 0; OKButton.Enabled = true; if (Subsongs.Items.Count == 1) { // If only one song, select it OkButtonClick(this, EventArgs.Empty); } })); }); } private void SubSongSelectionForm_Closing(object sender, EventArgs e) { _wrapper.Dispose(); } } } ================================================ FILE: SidWiz/SidPlayForm.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ================================================ FILE: SidWiz/SidWizPlusGUI.Designer.cs ================================================ using System.Windows.Forms; namespace SidWizPlusGUI { partial class SidWizPlusGui { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SidWizPlusGui)); this.splitContainer1 = new System.Windows.Forms.SplitContainer(); this.Preview = new System.Windows.Forms.PictureBox(); this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); this.moveLeftupToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.moveRightdownToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator(); this.cloneChannelToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.removeChannelToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.splitChannelToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator(); this.copySettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.pasteSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.applySettingsToOtherChannelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.setAsdefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.resetToDefaultSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.PreviewTrackbar = new System.Windows.Forms.TrackBar(); this.tabControl = new System.Windows.Forms.TabControl(); this.channelsTab = new System.Windows.Forms.TabPage(); this.ChannelsHelpLabel = new System.Windows.Forms.Label(); this.PropertyGrid = new System.Windows.Forms.PropertyGrid(); this.toolStrip1 = new System.Windows.Forms.ToolStrip(); this.AddFileButton = new System.Windows.Forms.ToolStripButton(); this.AddChannelButton = new System.Windows.Forms.ToolStripButton(); this.ChannelToolstripItemsSeparator = new System.Windows.Forms.ToolStripSeparator(); this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator(); this.RemoveLabelsButton = new System.Windows.Forms.ToolStripButton(); this.RemoveButton = new System.Windows.Forms.ToolStripDropDownButton(); this.removeselectedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.removeemptyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator(); this.removeallToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.tabPage1 = new System.Windows.Forms.TabPage(); this.groupBox2 = new System.Windows.Forms.GroupBox(); this.MarginBottomControl = new System.Windows.Forms.NumericUpDown(); this.MarginRightControl = new System.Windows.Forms.NumericUpDown(); this.MarginLeftControl = new System.Windows.Forms.NumericUpDown(); this.MarginTopControl = new System.Windows.Forms.NumericUpDown(); this.label9 = new System.Windows.Forms.Label(); this.label8 = new System.Windows.Forms.Label(); this.label7 = new System.Windows.Forms.Label(); this.label6 = new System.Windows.Forms.Label(); this.AutoScaleButton = new System.Windows.Forms.Button(); this.label1 = new System.Windows.Forms.Label(); this.Columns = new System.Windows.Forms.NumericUpDown(); this.label16 = new System.Windows.Forms.Label(); this.label4 = new System.Windows.Forms.Label(); this.WidthControl = new System.Windows.Forms.TextBox(); this.label17 = new System.Windows.Forms.Label(); this.label5 = new System.Windows.Forms.Label(); this.VerticalScaling = new System.Windows.Forms.TextBox(); this.HeightControl = new System.Windows.Forms.TextBox(); this.label11 = new System.Windows.Forms.Label(); this.label12 = new System.Windows.Forms.Label(); this.tabPage4 = new System.Windows.Forms.TabPage(); this.label20 = new System.Windows.Forms.Label(); this.label21 = new System.Windows.Forms.Label(); this.BackgroundImageControl = new System.Windows.Forms.PictureBox(); this.tabPage3 = new System.Windows.Forms.TabPage(); this.label19 = new System.Windows.Forms.Label(); this.MasterAudioPath = new System.Windows.Forms.TextBox(); this.MasterMixReplayGain = new System.Windows.Forms.CheckBox(); this.AutogenerateMasterMix = new System.Windows.Forms.CheckBox(); this.tabPage2 = new System.Windows.Forms.TabPage(); this.RenderThreadsControl = new System.Windows.Forms.NumericUpDown(); this.label2 = new System.Windows.Forms.Label(); this.groupBox4 = new System.Windows.Forms.GroupBox(); this.PreviewCheckBox = new System.Windows.Forms.CheckBox(); this.label10 = new System.Windows.Forms.Label(); this.PreviewFrameskip = new System.Windows.Forms.NumericUpDown(); this.label13 = new System.Windows.Forms.Label(); this.groupBox3 = new System.Windows.Forms.GroupBox(); this.ExtraFFMPEGParameters = new System.Windows.Forms.TextBox(); this.label22 = new System.Windows.Forms.Label(); this.AudioCodec = new System.Windows.Forms.ComboBox(); this.label18 = new System.Windows.Forms.Label(); this.VideoCodec = new System.Windows.Forms.ComboBox(); this.label3 = new System.Windows.Forms.Label(); this.EncodeCheckBox = new System.Windows.Forms.CheckBox(); this.FrameRateControl = new System.Windows.Forms.NumericUpDown(); this.label14 = new System.Windows.Forms.Label(); this.label15 = new System.Windows.Forms.Label(); this.RenderButton = new System.Windows.Forms.Button(); this.tabPage6 = new System.Windows.Forms.TabPage(); this.LoadButton = new System.Windows.Forms.Button(); this.SaveButton = new System.Windows.Forms.Button(); this.tabPage5 = new System.Windows.Forms.TabPage(); this.ProgramSettingsGrid = new System.Windows.Forms.PropertyGrid(); this.imageList1 = new System.Windows.Forms.ImageList(this.components); this.BackgroundColorButton = new SidWizPlusGUI.ColorButton(this.components); ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); this.splitContainer1.Panel1.SuspendLayout(); this.splitContainer1.Panel2.SuspendLayout(); this.splitContainer1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.Preview)).BeginInit(); this.contextMenuStrip1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.PreviewTrackbar)).BeginInit(); this.tabControl.SuspendLayout(); this.channelsTab.SuspendLayout(); this.toolStrip1.SuspendLayout(); this.tabPage1.SuspendLayout(); this.groupBox2.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.MarginBottomControl)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.MarginRightControl)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.MarginLeftControl)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.MarginTopControl)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.Columns)).BeginInit(); this.tabPage4.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.BackgroundImageControl)).BeginInit(); this.tabPage3.SuspendLayout(); this.tabPage2.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.RenderThreadsControl)).BeginInit(); this.groupBox4.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.PreviewFrameskip)).BeginInit(); this.groupBox3.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.FrameRateControl)).BeginInit(); this.tabPage6.SuspendLayout(); this.tabPage5.SuspendLayout(); this.SuspendLayout(); // // splitContainer1 // this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; this.splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2; this.splitContainer1.Location = new System.Drawing.Point(0, 0); this.splitContainer1.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.splitContainer1.Name = "splitContainer1"; // // splitContainer1.Panel1 // this.splitContainer1.Panel1.AutoScroll = true; this.splitContainer1.Panel1.Controls.Add(this.Preview); this.splitContainer1.Panel1.Controls.Add(this.PreviewTrackbar); // // splitContainer1.Panel2 // this.splitContainer1.Panel2.Controls.Add(this.tabControl); this.splitContainer1.Size = new System.Drawing.Size(1140, 648); this.splitContainer1.SplitterDistance = 695; this.splitContainer1.TabIndex = 0; // // Preview // this.Preview.ContextMenuStrip = this.contextMenuStrip1; this.Preview.Dock = System.Windows.Forms.DockStyle.Fill; this.Preview.Location = new System.Drawing.Point(0, 0); this.Preview.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.Preview.Name = "Preview"; this.Preview.Size = new System.Drawing.Size(695, 603); this.Preview.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; this.Preview.TabIndex = 0; this.Preview.TabStop = false; this.Preview.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Preview_MouseClick); // // contextMenuStrip1 // this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.moveLeftupToolStripMenuItem, this.moveRightdownToolStripMenuItem, this.toolStripMenuItem1, this.cloneChannelToolStripMenuItem, this.removeChannelToolStripMenuItem, this.splitChannelToolStripMenuItem, this.toolStripSeparator3, this.copySettingsToolStripMenuItem, this.pasteSettingsToolStripMenuItem, this.applySettingsToOtherChannelsToolStripMenuItem, this.setAsdefaultsToolStripMenuItem, this.resetToDefaultSettingsToolStripMenuItem}); this.contextMenuStrip1.Name = "contextMenuStrip1"; this.contextMenuStrip1.Size = new System.Drawing.Size(245, 236); // // moveLeftupToolStripMenuItem // this.moveLeftupToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("moveLeftupToolStripMenuItem.Image"))); this.moveLeftupToolStripMenuItem.Name = "moveLeftupToolStripMenuItem"; this.moveLeftupToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.moveLeftupToolStripMenuItem.Text = "Move &left/up"; this.moveLeftupToolStripMenuItem.Click += new System.EventHandler(this.LeftButton_Click); // // moveRightdownToolStripMenuItem // this.moveRightdownToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("moveRightdownToolStripMenuItem.Image"))); this.moveRightdownToolStripMenuItem.Name = "moveRightdownToolStripMenuItem"; this.moveRightdownToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.moveRightdownToolStripMenuItem.Text = "Move &right/down"; this.moveRightdownToolStripMenuItem.Click += new System.EventHandler(this.RightButton_Click); // // toolStripMenuItem1 // this.toolStripMenuItem1.Name = "toolStripMenuItem1"; this.toolStripMenuItem1.Size = new System.Drawing.Size(241, 6); // // cloneChannelToolStripMenuItem // this.cloneChannelToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("cloneChannelToolStripMenuItem.Image"))); this.cloneChannelToolStripMenuItem.Name = "cloneChannelToolStripMenuItem"; this.cloneChannelToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.cloneChannelToolStripMenuItem.Text = "Cl&one"; this.cloneChannelToolStripMenuItem.Click += new System.EventHandler(this.CloneChannelButton_Click); // // removeChannelToolStripMenuItem // this.removeChannelToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("removeChannelToolStripMenuItem.Image"))); this.removeChannelToolStripMenuItem.Name = "removeChannelToolStripMenuItem"; this.removeChannelToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.removeChannelToolStripMenuItem.Text = "Re&move"; this.removeChannelToolStripMenuItem.Click += new System.EventHandler(this.RemoveButton_Click); // // splitChannelToolStripMenuItem // this.splitChannelToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("splitChannelToolStripMenuItem.Image"))); this.splitChannelToolStripMenuItem.Name = "splitChannelToolStripMenuItem"; this.splitChannelToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.splitChannelToolStripMenuItem.Text = "&Split"; this.splitChannelToolStripMenuItem.Click += new System.EventHandler(this.SplitChannel); // // toolStripSeparator3 // this.toolStripSeparator3.Name = "toolStripSeparator3"; this.toolStripSeparator3.Size = new System.Drawing.Size(241, 6); // // copySettingsToolStripMenuItem // this.copySettingsToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("copySettingsToolStripMenuItem.Image"))); this.copySettingsToolStripMenuItem.Name = "copySettingsToolStripMenuItem"; this.copySettingsToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.copySettingsToolStripMenuItem.Text = "&Copy settings"; this.copySettingsToolStripMenuItem.Click += new System.EventHandler(this.CopyChannelSettings); // // pasteSettingsToolStripMenuItem // this.pasteSettingsToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("pasteSettingsToolStripMenuItem.Image"))); this.pasteSettingsToolStripMenuItem.Name = "pasteSettingsToolStripMenuItem"; this.pasteSettingsToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.pasteSettingsToolStripMenuItem.Text = "&Paste settings"; this.pasteSettingsToolStripMenuItem.Click += new System.EventHandler(this.PasteChannelSettings); // // applySettingsToOtherChannelsToolStripMenuItem // this.applySettingsToOtherChannelsToolStripMenuItem.Image = global::SidWizPlusGUI.Properties.Resources.lightning; this.applySettingsToOtherChannelsToolStripMenuItem.Name = "applySettingsToOtherChannelsToolStripMenuItem"; this.applySettingsToOtherChannelsToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.applySettingsToOtherChannelsToolStripMenuItem.Text = "&Apply settings to other channels"; this.applySettingsToOtherChannelsToolStripMenuItem.Click += new System.EventHandler(this.CopySettingsButton_Click); // // setAsdefaultsToolStripMenuItem // this.setAsdefaultsToolStripMenuItem.Image = global::SidWizPlusGUI.Properties.Resources.page_save; this.setAsdefaultsToolStripMenuItem.Name = "setAsdefaultsToolStripMenuItem"; this.setAsdefaultsToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.setAsdefaultsToolStripMenuItem.Text = "Save as &default settings"; this.setAsdefaultsToolStripMenuItem.Click += new System.EventHandler(this.SaveAsDefaultSettings); // // resetToDefaultSettingsToolStripMenuItem // this.resetToDefaultSettingsToolStripMenuItem.Image = global::SidWizPlusGUI.Properties.Resources.page_refresh; this.resetToDefaultSettingsToolStripMenuItem.Name = "resetToDefaultSettingsToolStripMenuItem"; this.resetToDefaultSettingsToolStripMenuItem.Size = new System.Drawing.Size(244, 22); this.resetToDefaultSettingsToolStripMenuItem.Text = "R&eset to default settings"; this.resetToDefaultSettingsToolStripMenuItem.Click += new System.EventHandler(this.ResetToDefaultSettings); // // PreviewTrackbar // this.PreviewTrackbar.Dock = System.Windows.Forms.DockStyle.Bottom; this.PreviewTrackbar.LargeChange = 60; this.PreviewTrackbar.Location = new System.Drawing.Point(0, 603); this.PreviewTrackbar.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.PreviewTrackbar.Maximum = 1000; this.PreviewTrackbar.Name = "PreviewTrackbar"; this.PreviewTrackbar.Size = new System.Drawing.Size(695, 45); this.PreviewTrackbar.TabIndex = 0; this.PreviewTrackbar.TickFrequency = 60; this.PreviewTrackbar.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // tabControl // this.tabControl.Controls.Add(this.channelsTab); this.tabControl.Controls.Add(this.tabPage1); this.tabControl.Controls.Add(this.tabPage4); this.tabControl.Controls.Add(this.tabPage3); this.tabControl.Controls.Add(this.tabPage2); this.tabControl.Controls.Add(this.tabPage6); this.tabControl.Controls.Add(this.tabPage5); this.tabControl.Dock = System.Windows.Forms.DockStyle.Fill; this.tabControl.ImageList = this.imageList1; this.tabControl.Location = new System.Drawing.Point(0, 0); this.tabControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.tabControl.Multiline = true; this.tabControl.Name = "tabControl"; this.tabControl.SelectedIndex = 0; this.tabControl.Size = new System.Drawing.Size(441, 648); this.tabControl.SizeMode = System.Windows.Forms.TabSizeMode.FillToRight; this.tabControl.TabIndex = 0; // // channelsTab // this.channelsTab.Controls.Add(this.ChannelsHelpLabel); this.channelsTab.Controls.Add(this.PropertyGrid); this.channelsTab.Controls.Add(this.toolStrip1); this.channelsTab.ImageKey = "music.png"; this.channelsTab.Location = new System.Drawing.Point(4, 44); this.channelsTab.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.channelsTab.Name = "channelsTab"; this.channelsTab.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4); this.channelsTab.Size = new System.Drawing.Size(433, 600); this.channelsTab.TabIndex = 4; this.channelsTab.Text = "Channels"; this.channelsTab.UseVisualStyleBackColor = true; // // ChannelsHelpLabel // this.ChannelsHelpLabel.Dock = System.Windows.Forms.DockStyle.Fill; this.ChannelsHelpLabel.Location = new System.Drawing.Point(3, 29); this.ChannelsHelpLabel.Name = "ChannelsHelpLabel"; this.ChannelsHelpLabel.Size = new System.Drawing.Size(427, 567); this.ChannelsHelpLabel.TabIndex = 2; this.ChannelsHelpLabel.Text = "Click a channel to view its properties\r\n\r\nOpen files using the first toolbar butt" + "on to add channels"; this.ChannelsHelpLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // PropertyGrid // this.PropertyGrid.Dock = System.Windows.Forms.DockStyle.Fill; this.PropertyGrid.Location = new System.Drawing.Point(3, 29); this.PropertyGrid.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.PropertyGrid.Name = "PropertyGrid"; this.PropertyGrid.Size = new System.Drawing.Size(427, 567); this.PropertyGrid.TabIndex = 1; this.PropertyGrid.ToolbarVisible = false; this.PropertyGrid.SelectedObjectsChanged += new System.EventHandler(this.PropertyGrid_SelectedObjectsChanged); // // toolStrip1 // this.toolStrip1.CanOverflow = false; this.toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden; this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.AddFileButton, this.AddChannelButton, this.ChannelToolstripItemsSeparator, this.toolStripSeparator5, this.RemoveLabelsButton, this.RemoveButton}); this.toolStrip1.Location = new System.Drawing.Point(3, 4); this.toolStrip1.Name = "toolStrip1"; this.toolStrip1.Padding = new System.Windows.Forms.Padding(0); this.toolStrip1.Size = new System.Drawing.Size(427, 25); this.toolStrip1.TabIndex = 0; this.toolStrip1.Text = "toolStrip1"; // // AddFileButton // this.AddFileButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.AddFileButton.Image = ((System.Drawing.Image)(resources.GetObject("AddFileButton.Image"))); this.AddFileButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.AddFileButton.Name = "AddFileButton"; this.AddFileButton.Size = new System.Drawing.Size(23, 22); this.AddFileButton.Text = "Add a file"; this.AddFileButton.Click += new System.EventHandler(this.AddAFileClick); // // AddChannelButton // this.AddChannelButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.AddChannelButton.Image = global::SidWizPlusGUI.Properties.Resources.page; this.AddChannelButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.AddChannelButton.Name = "AddChannelButton"; this.AddChannelButton.Size = new System.Drawing.Size(23, 22); this.AddChannelButton.Text = "Add a channel"; this.AddChannelButton.Click += new System.EventHandler(this.AddChannelButton_Click); // // ChannelToolstripItemsSeparator // this.ChannelToolstripItemsSeparator.Name = "ChannelToolstripItemsSeparator"; this.ChannelToolstripItemsSeparator.Size = new System.Drawing.Size(6, 25); // // toolStripSeparator5 // this.toolStripSeparator5.Name = "toolStripSeparator5"; this.toolStripSeparator5.Size = new System.Drawing.Size(6, 25); // // RemoveLabelsButton // this.RemoveLabelsButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.RemoveLabelsButton.Image = ((System.Drawing.Image)(resources.GetObject("RemoveLabelsButton.Image"))); this.RemoveLabelsButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.RemoveLabelsButton.Name = "RemoveLabelsButton"; this.RemoveLabelsButton.Size = new System.Drawing.Size(23, 22); this.RemoveLabelsButton.Text = "Remove labels"; this.RemoveLabelsButton.Click += new System.EventHandler(this.RemoveAllLabels); // // RemoveButton // this.RemoveButton.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right; this.RemoveButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.RemoveButton.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.removeselectedToolStripMenuItem, this.removeemptyToolStripMenuItem, this.toolStripMenuItem2, this.removeallToolStripMenuItem}); this.RemoveButton.Image = ((System.Drawing.Image)(resources.GetObject("RemoveButton.Image"))); this.RemoveButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.RemoveButton.Name = "RemoveButton"; this.RemoveButton.Size = new System.Drawing.Size(29, 22); this.RemoveButton.Text = "Remove channel"; // // removeselectedToolStripMenuItem // this.removeselectedToolStripMenuItem.Name = "removeselectedToolStripMenuItem"; this.removeselectedToolStripMenuItem.Size = new System.Drawing.Size(163, 22); this.removeselectedToolStripMenuItem.Text = "Remove &selected"; this.removeselectedToolStripMenuItem.Click += new System.EventHandler(this.RemoveButton_Click); // // removeemptyToolStripMenuItem // this.removeemptyToolStripMenuItem.Name = "removeemptyToolStripMenuItem"; this.removeemptyToolStripMenuItem.Size = new System.Drawing.Size(163, 22); this.removeemptyToolStripMenuItem.Text = "Remove s&ilent"; this.removeemptyToolStripMenuItem.Click += new System.EventHandler(this.RemoveSilentChannels); // // toolStripMenuItem2 // this.toolStripMenuItem2.Name = "toolStripMenuItem2"; this.toolStripMenuItem2.Size = new System.Drawing.Size(160, 6); // // removeallToolStripMenuItem // this.removeallToolStripMenuItem.Name = "removeallToolStripMenuItem"; this.removeallToolStripMenuItem.Size = new System.Drawing.Size(163, 22); this.removeallToolStripMenuItem.Text = "Remove &all"; this.removeallToolStripMenuItem.Click += new System.EventHandler(this.RemoveAllChannels); // // tabPage1 // this.tabPage1.Controls.Add(this.groupBox2); this.tabPage1.Controls.Add(this.AutoScaleButton); this.tabPage1.Controls.Add(this.label1); this.tabPage1.Controls.Add(this.Columns); this.tabPage1.Controls.Add(this.label16); this.tabPage1.Controls.Add(this.label4); this.tabPage1.Controls.Add(this.WidthControl); this.tabPage1.Controls.Add(this.label17); this.tabPage1.Controls.Add(this.label5); this.tabPage1.Controls.Add(this.VerticalScaling); this.tabPage1.Controls.Add(this.HeightControl); this.tabPage1.Controls.Add(this.label11); this.tabPage1.Controls.Add(this.label12); this.tabPage1.ImageKey = "layout.png"; this.tabPage1.Location = new System.Drawing.Point(4, 44); this.tabPage1.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.tabPage1.Name = "tabPage1"; this.tabPage1.Padding = new System.Windows.Forms.Padding(2, 3, 2, 3); this.tabPage1.Size = new System.Drawing.Size(433, 600); this.tabPage1.TabIndex = 0; this.tabPage1.Text = "Layout"; this.tabPage1.UseVisualStyleBackColor = true; // // groupBox2 // this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.groupBox2.Controls.Add(this.MarginBottomControl); this.groupBox2.Controls.Add(this.MarginRightControl); this.groupBox2.Controls.Add(this.MarginLeftControl); this.groupBox2.Controls.Add(this.MarginTopControl); this.groupBox2.Controls.Add(this.label9); this.groupBox2.Controls.Add(this.label8); this.groupBox2.Controls.Add(this.label7); this.groupBox2.Controls.Add(this.label6); this.groupBox2.Location = new System.Drawing.Point(4, 93); this.groupBox2.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.groupBox2.Name = "groupBox2"; this.groupBox2.Padding = new System.Windows.Forms.Padding(2, 3, 2, 3); this.groupBox2.Size = new System.Drawing.Size(425, 146); this.groupBox2.TabIndex = 8; this.groupBox2.TabStop = false; this.groupBox2.Text = "Margins"; // // MarginBottomControl // this.MarginBottomControl.Location = new System.Drawing.Point(114, 109); this.MarginBottomControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.MarginBottomControl.Maximum = new decimal(new int[] { 1000, 0, 0, 0}); this.MarginBottomControl.Name = "MarginBottomControl"; this.MarginBottomControl.Size = new System.Drawing.Size(61, 23); this.MarginBottomControl.TabIndex = 7; this.MarginBottomControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.MarginBottomControl.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // MarginRightControl // this.MarginRightControl.Location = new System.Drawing.Point(114, 80); this.MarginRightControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.MarginRightControl.Maximum = new decimal(new int[] { 1000, 0, 0, 0}); this.MarginRightControl.Name = "MarginRightControl"; this.MarginRightControl.Size = new System.Drawing.Size(61, 23); this.MarginRightControl.TabIndex = 5; this.MarginRightControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.MarginRightControl.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // MarginLeftControl // this.MarginLeftControl.Location = new System.Drawing.Point(114, 51); this.MarginLeftControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.MarginLeftControl.Maximum = new decimal(new int[] { 1000, 0, 0, 0}); this.MarginLeftControl.Name = "MarginLeftControl"; this.MarginLeftControl.Size = new System.Drawing.Size(61, 23); this.MarginLeftControl.TabIndex = 3; this.MarginLeftControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.MarginLeftControl.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // MarginTopControl // this.MarginTopControl.Location = new System.Drawing.Point(114, 22); this.MarginTopControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.MarginTopControl.Maximum = new decimal(new int[] { 1000, 0, 0, 0}); this.MarginTopControl.Name = "MarginTopControl"; this.MarginTopControl.Size = new System.Drawing.Size(61, 23); this.MarginTopControl.TabIndex = 1; this.MarginTopControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.MarginTopControl.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // label9 // this.label9.AutoSize = true; this.label9.Location = new System.Drawing.Point(10, 111); this.label9.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label9.Name = "label9"; this.label9.Size = new System.Drawing.Size(47, 15); this.label9.TabIndex = 6; this.label9.Text = "Bottom"; // // label8 // this.label8.AutoSize = true; this.label8.Location = new System.Drawing.Point(10, 82); this.label8.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label8.Name = "label8"; this.label8.Size = new System.Drawing.Size(35, 15); this.label8.TabIndex = 4; this.label8.Text = "Right"; // // label7 // this.label7.AutoSize = true; this.label7.Location = new System.Drawing.Point(10, 53); this.label7.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label7.Name = "label7"; this.label7.Size = new System.Drawing.Size(27, 15); this.label7.TabIndex = 2; this.label7.Text = "Left"; // // label6 // this.label6.AutoSize = true; this.label6.Location = new System.Drawing.Point(10, 24); this.label6.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label6.Name = "label6"; this.label6.Size = new System.Drawing.Size(26, 15); this.label6.TabIndex = 0; this.label6.Text = "Top"; // // AutoScaleButton // this.AutoScaleButton.Location = new System.Drawing.Point(118, 274); this.AutoScaleButton.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.AutoScaleButton.Name = "AutoScaleButton"; this.AutoScaleButton.Size = new System.Drawing.Size(61, 33); this.AutoScaleButton.TabIndex = 12; this.AutoScaleButton.Text = "Apply"; this.AutoScaleButton.UseVisualStyleBackColor = true; this.AutoScaleButton.Click += new System.EventHandler(this.AutoScale_Click); // // label1 // this.label1.AutoSize = true; this.label1.Location = new System.Drawing.Point(14, 66); this.label1.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(55, 15); this.label1.TabIndex = 6; this.label1.Text = "Columns"; // // Columns // this.Columns.Location = new System.Drawing.Point(118, 64); this.Columns.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.Columns.Minimum = new decimal(new int[] { 1, 0, 0, 0}); this.Columns.Name = "Columns"; this.Columns.Size = new System.Drawing.Size(61, 23); this.Columns.TabIndex = 7; this.Columns.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.Columns.Value = new decimal(new int[] { 1, 0, 0, 0}); this.Columns.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // label16 // this.label16.AutoSize = true; this.label16.Location = new System.Drawing.Point(182, 248); this.label16.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label16.Name = "label16"; this.label16.Size = new System.Drawing.Size(59, 15); this.label16.TabIndex = 11; this.label16.Text = "% of peak"; // // label4 // this.label4.AutoSize = true; this.label4.Location = new System.Drawing.Point(14, 9); this.label4.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label4.Name = "label4"; this.label4.Size = new System.Drawing.Size(39, 15); this.label4.TabIndex = 0; this.label4.Text = "Width"; // // WidthControl // this.WidthControl.Location = new System.Drawing.Point(118, 6); this.WidthControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.WidthControl.Name = "WidthControl"; this.WidthControl.Size = new System.Drawing.Size(60, 23); this.WidthControl.TabIndex = 1; this.WidthControl.Text = "1280"; this.WidthControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.WidthControl.TextChanged += new System.EventHandler(this.ControlValueChanged); // // label17 // this.label17.AutoSize = true; this.label17.Location = new System.Drawing.Point(14, 248); this.label17.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label17.Name = "label17"; this.label17.Size = new System.Drawing.Size(85, 15); this.label17.TabIndex = 9; this.label17.Text = "Vertical scaling"; // // label5 // this.label5.AutoSize = true; this.label5.Location = new System.Drawing.Point(14, 38); this.label5.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label5.Name = "label5"; this.label5.Size = new System.Drawing.Size(43, 15); this.label5.TabIndex = 3; this.label5.Text = "Height"; // // VerticalScaling // this.VerticalScaling.Location = new System.Drawing.Point(118, 245); this.VerticalScaling.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.VerticalScaling.Name = "VerticalScaling"; this.VerticalScaling.Size = new System.Drawing.Size(60, 23); this.VerticalScaling.TabIndex = 10; this.VerticalScaling.Text = "80"; this.VerticalScaling.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.VerticalScaling.TextChanged += new System.EventHandler(this.ControlValueChanged); // // HeightControl // this.HeightControl.Location = new System.Drawing.Point(118, 35); this.HeightControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.HeightControl.Name = "HeightControl"; this.HeightControl.Size = new System.Drawing.Size(60, 23); this.HeightControl.TabIndex = 4; this.HeightControl.Text = "720"; this.HeightControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.HeightControl.TextChanged += new System.EventHandler(this.ControlValueChanged); // // label11 // this.label11.AutoSize = true; this.label11.Location = new System.Drawing.Point(182, 9); this.label11.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label11.Name = "label11"; this.label11.Size = new System.Drawing.Size(20, 15); this.label11.TabIndex = 2; this.label11.Text = "px"; // // label12 // this.label12.AutoSize = true; this.label12.Location = new System.Drawing.Point(182, 38); this.label12.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label12.Name = "label12"; this.label12.Size = new System.Drawing.Size(20, 15); this.label12.TabIndex = 5; this.label12.Text = "px"; // // tabPage4 // this.tabPage4.Controls.Add(this.label20); this.tabPage4.Controls.Add(this.label21); this.tabPage4.Controls.Add(this.BackgroundImageControl); this.tabPage4.Controls.Add(this.BackgroundColorButton); this.tabPage4.ImageKey = "picture.png"; this.tabPage4.Location = new System.Drawing.Point(4, 44); this.tabPage4.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.tabPage4.Name = "tabPage4"; this.tabPage4.Padding = new System.Windows.Forms.Padding(2, 3, 2, 3); this.tabPage4.Size = new System.Drawing.Size(433, 600); this.tabPage4.TabIndex = 3; this.tabPage4.Text = "Background"; this.tabPage4.UseVisualStyleBackColor = true; // // label20 // this.label20.AutoSize = true; this.label20.Location = new System.Drawing.Point(14, 15); this.label20.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label20.Name = "label20"; this.label20.Size = new System.Drawing.Size(36, 15); this.label20.TabIndex = 0; this.label20.Text = "Color"; // // label21 // this.label21.AutoSize = true; this.label21.Location = new System.Drawing.Point(14, 49); this.label21.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label21.Name = "label21"; this.label21.Size = new System.Drawing.Size(40, 15); this.label21.TabIndex = 2; this.label21.Text = "Image"; // // BackgroundImageControl // this.BackgroundImageControl.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.BackgroundImageControl.Cursor = System.Windows.Forms.Cursors.Hand; this.BackgroundImageControl.Location = new System.Drawing.Point(119, 45); this.BackgroundImageControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.BackgroundImageControl.Name = "BackgroundImageControl"; this.BackgroundImageControl.Size = new System.Drawing.Size(113, 97); this.BackgroundImageControl.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; this.BackgroundImageControl.TabIndex = 16; this.BackgroundImageControl.TabStop = false; this.BackgroundImageControl.Click += new System.EventHandler(this.BackgroundImageControl_Click); // // tabPage3 // this.tabPage3.Controls.Add(this.label19); this.tabPage3.Controls.Add(this.MasterAudioPath); this.tabPage3.Controls.Add(this.MasterMixReplayGain); this.tabPage3.Controls.Add(this.AutogenerateMasterMix); this.tabPage3.ImageKey = "sound.png"; this.tabPage3.Location = new System.Drawing.Point(4, 44); this.tabPage3.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.tabPage3.Name = "tabPage3"; this.tabPage3.Padding = new System.Windows.Forms.Padding(2, 3, 2, 3); this.tabPage3.Size = new System.Drawing.Size(433, 600); this.tabPage3.TabIndex = 2; this.tabPage3.Text = "Audio"; this.tabPage3.UseVisualStyleBackColor = true; // // label19 // this.label19.AutoSize = true; this.label19.Location = new System.Drawing.Point(11, 59); this.label19.Name = "label19"; this.label19.Size = new System.Drawing.Size(53, 15); this.label19.TabIndex = 3; this.label19.Text = "Location"; // // MasterAudioPath // this.MasterAudioPath.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.MasterAudioPath.Cursor = System.Windows.Forms.Cursors.Hand; this.MasterAudioPath.Location = new System.Drawing.Point(14, 78); this.MasterAudioPath.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.MasterAudioPath.Name = "MasterAudioPath"; this.MasterAudioPath.ReadOnly = true; this.MasterAudioPath.Size = new System.Drawing.Size(411, 23); this.MasterAudioPath.TabIndex = 4; this.MasterAudioPath.Click += new System.EventHandler(this.SetMasterAudioPath); // // MasterMixReplayGain // this.MasterMixReplayGain.AutoSize = true; this.MasterMixReplayGain.Checked = true; this.MasterMixReplayGain.CheckState = System.Windows.Forms.CheckState.Checked; this.MasterMixReplayGain.Location = new System.Drawing.Point(14, 34); this.MasterMixReplayGain.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.MasterMixReplayGain.Name = "MasterMixReplayGain"; this.MasterMixReplayGain.Size = new System.Drawing.Size(119, 19); this.MasterMixReplayGain.TabIndex = 1; this.MasterMixReplayGain.Text = "Apply ReplayGain"; this.MasterMixReplayGain.UseVisualStyleBackColor = true; // // AutogenerateMasterMix // this.AutogenerateMasterMix.AutoSize = true; this.AutogenerateMasterMix.Checked = true; this.AutogenerateMasterMix.CheckState = System.Windows.Forms.CheckState.Checked; this.AutogenerateMasterMix.Location = new System.Drawing.Point(14, 9); this.AutogenerateMasterMix.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.AutogenerateMasterMix.Name = "AutogenerateMasterMix"; this.AutogenerateMasterMix.Size = new System.Drawing.Size(189, 19); this.AutogenerateMasterMix.TabIndex = 0; this.AutogenerateMasterMix.Text = "Autogenerate master audio file"; this.AutogenerateMasterMix.UseVisualStyleBackColor = true; this.AutogenerateMasterMix.CheckedChanged += new System.EventHandler(this.AutogenerateMasterMix_CheckedChanged); // // tabPage2 // this.tabPage2.Controls.Add(this.RenderThreadsControl); this.tabPage2.Controls.Add(this.label2); this.tabPage2.Controls.Add(this.groupBox4); this.tabPage2.Controls.Add(this.groupBox3); this.tabPage2.Controls.Add(this.FrameRateControl); this.tabPage2.Controls.Add(this.label14); this.tabPage2.Controls.Add(this.label15); this.tabPage2.Controls.Add(this.RenderButton); this.tabPage2.ImageKey = "film.png"; this.tabPage2.Location = new System.Drawing.Point(4, 44); this.tabPage2.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.tabPage2.Name = "tabPage2"; this.tabPage2.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4); this.tabPage2.Size = new System.Drawing.Size(433, 600); this.tabPage2.TabIndex = 5; this.tabPage2.Text = "Video"; this.tabPage2.UseVisualStyleBackColor = true; // // RenderThreadsControl // this.RenderThreadsControl.Location = new System.Drawing.Point(118, 265); this.RenderThreadsControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.RenderThreadsControl.Maximum = new decimal(new int[] { 1000, 0, 0, 0}); this.RenderThreadsControl.Minimum = new decimal(new int[] { 1, 0, 0, 0}); this.RenderThreadsControl.Name = "RenderThreadsControl"; this.RenderThreadsControl.Size = new System.Drawing.Size(63, 23); this.RenderThreadsControl.TabIndex = 11; this.RenderThreadsControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.RenderThreadsControl.Value = new decimal(new int[] { 4, 0, 0, 0}); this.RenderThreadsControl.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // label2 // this.label2.AutoSize = true; this.label2.Location = new System.Drawing.Point(14, 267); this.label2.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(86, 15); this.label2.TabIndex = 10; this.label2.Text = "Render threads"; // // groupBox4 // this.groupBox4.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.groupBox4.Controls.Add(this.PreviewCheckBox); this.groupBox4.Controls.Add(this.label10); this.groupBox4.Controls.Add(this.PreviewFrameskip); this.groupBox4.Controls.Add(this.label13); this.groupBox4.Location = new System.Drawing.Point(6, 36); this.groupBox4.Name = "groupBox4"; this.groupBox4.Size = new System.Drawing.Size(421, 80); this.groupBox4.TabIndex = 9; this.groupBox4.TabStop = false; this.groupBox4.Text = "Preview"; // // PreviewCheckBox // this.PreviewCheckBox.AutoSize = true; this.PreviewCheckBox.Location = new System.Drawing.Point(11, 23); this.PreviewCheckBox.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.PreviewCheckBox.Name = "PreviewCheckBox"; this.PreviewCheckBox.Size = new System.Drawing.Size(68, 19); this.PreviewCheckBox.TabIndex = 3; this.PreviewCheckBox.Text = "Enabled"; this.PreviewCheckBox.UseVisualStyleBackColor = true; this.PreviewCheckBox.CheckedChanged += new System.EventHandler(this.ControlValueChanged); // // label10 // this.label10.AutoSize = true; this.label10.Location = new System.Drawing.Point(8, 52); this.label10.Name = "label10"; this.label10.Size = new System.Drawing.Size(89, 15); this.label10.TabIndex = 4; this.label10.Text = "Show 1 in every"; // // PreviewFrameskip // this.PreviewFrameskip.Location = new System.Drawing.Point(112, 50); this.PreviewFrameskip.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.PreviewFrameskip.Name = "PreviewFrameskip"; this.PreviewFrameskip.Size = new System.Drawing.Size(63, 23); this.PreviewFrameskip.TabIndex = 5; this.PreviewFrameskip.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.PreviewFrameskip.Value = new decimal(new int[] { 60, 0, 0, 0}); this.PreviewFrameskip.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // label13 // this.label13.AutoSize = true; this.label13.Location = new System.Drawing.Point(181, 52); this.label13.Name = "label13"; this.label13.Size = new System.Drawing.Size(43, 15); this.label13.TabIndex = 6; this.label13.Text = "frames"; // // groupBox3 // this.groupBox3.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.groupBox3.Controls.Add(this.ExtraFFMPEGParameters); this.groupBox3.Controls.Add(this.label22); this.groupBox3.Controls.Add(this.AudioCodec); this.groupBox3.Controls.Add(this.label18); this.groupBox3.Controls.Add(this.VideoCodec); this.groupBox3.Controls.Add(this.label3); this.groupBox3.Controls.Add(this.EncodeCheckBox); this.groupBox3.Location = new System.Drawing.Point(6, 123); this.groupBox3.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.groupBox3.Name = "groupBox3"; this.groupBox3.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4); this.groupBox3.Size = new System.Drawing.Size(421, 135); this.groupBox3.TabIndex = 7; this.groupBox3.TabStop = false; this.groupBox3.Text = "FFMPEG"; // // ExtraFFMPEGParameters // this.ExtraFFMPEGParameters.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.ExtraFFMPEGParameters.Location = new System.Drawing.Point(112, 102); this.ExtraFFMPEGParameters.Name = "ExtraFFMPEGParameters"; this.ExtraFFMPEGParameters.Size = new System.Drawing.Size(303, 23); this.ExtraFFMPEGParameters.TabIndex = 10; // // label22 // this.label22.AutoSize = true; this.label22.Location = new System.Drawing.Point(8, 105); this.label22.Name = "label22"; this.label22.Size = new System.Drawing.Size(95, 15); this.label22.TabIndex = 9; this.label22.Text = "Extra parameters"; // // AudioCodec // this.AudioCodec.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.AudioCodec.Location = new System.Drawing.Point(112, 73); this.AudioCodec.Name = "AudioCodec"; this.AudioCodec.Size = new System.Drawing.Size(303, 23); this.AudioCodec.TabIndex = 8; this.AudioCodec.Text = "aac"; this.AudioCodec.DropDown += new System.EventHandler(this.VideoCodec_DropDown); this.AudioCodec.TextChanged += new System.EventHandler(this.ControlValueChanged); // // label18 // this.label18.AutoSize = true; this.label18.Location = new System.Drawing.Point(8, 76); this.label18.Name = "label18"; this.label18.Size = new System.Drawing.Size(74, 15); this.label18.TabIndex = 7; this.label18.Text = "Audio codec"; // // VideoCodec // this.VideoCodec.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.VideoCodec.Location = new System.Drawing.Point(112, 44); this.VideoCodec.Name = "VideoCodec"; this.VideoCodec.Size = new System.Drawing.Size(303, 23); this.VideoCodec.TabIndex = 6; this.VideoCodec.Text = "libx264"; this.VideoCodec.DropDown += new System.EventHandler(this.VideoCodec_DropDown); this.VideoCodec.TextChanged += new System.EventHandler(this.ControlValueChanged); // // label3 // this.label3.AutoSize = true; this.label3.Location = new System.Drawing.Point(8, 47); this.label3.Name = "label3"; this.label3.Size = new System.Drawing.Size(72, 15); this.label3.TabIndex = 5; this.label3.Text = "Video codec"; // // EncodeCheckBox // this.EncodeCheckBox.AutoSize = true; this.EncodeCheckBox.Location = new System.Drawing.Point(11, 24); this.EncodeCheckBox.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.EncodeCheckBox.Name = "EncodeCheckBox"; this.EncodeCheckBox.Size = new System.Drawing.Size(98, 19); this.EncodeCheckBox.TabIndex = 0; this.EncodeCheckBox.Text = "Encode to file"; this.EncodeCheckBox.UseVisualStyleBackColor = true; this.EncodeCheckBox.CheckedChanged += new System.EventHandler(this.ControlValueChanged); // // FrameRateControl // this.FrameRateControl.Location = new System.Drawing.Point(118, 7); this.FrameRateControl.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.FrameRateControl.Maximum = new decimal(new int[] { 1000, 0, 0, 0}); this.FrameRateControl.Minimum = new decimal(new int[] { 1, 0, 0, 0}); this.FrameRateControl.Name = "FrameRateControl"; this.FrameRateControl.Size = new System.Drawing.Size(63, 23); this.FrameRateControl.TabIndex = 1; this.FrameRateControl.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; this.FrameRateControl.Value = new decimal(new int[] { 60, 0, 0, 0}); this.FrameRateControl.ValueChanged += new System.EventHandler(this.ControlValueChanged); // // label14 // this.label14.AutoSize = true; this.label14.Location = new System.Drawing.Point(185, 9); this.label14.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label14.Name = "label14"; this.label14.Size = new System.Drawing.Size(23, 15); this.label14.TabIndex = 2; this.label14.Text = "fps"; // // label15 // this.label15.AutoSize = true; this.label15.Location = new System.Drawing.Point(14, 9); this.label15.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label15.Name = "label15"; this.label15.Size = new System.Drawing.Size(63, 15); this.label15.TabIndex = 0; this.label15.Text = "Frame rate"; // // RenderButton // this.RenderButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.RenderButton.Location = new System.Drawing.Point(6, 295); this.RenderButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.RenderButton.Name = "RenderButton"; this.RenderButton.Size = new System.Drawing.Size(421, 61); this.RenderButton.TabIndex = 8; this.RenderButton.Text = "Render"; this.RenderButton.UseVisualStyleBackColor = true; this.RenderButton.Click += new System.EventHandler(this.RenderButton_Click); // // tabPage6 // this.tabPage6.Controls.Add(this.LoadButton); this.tabPage6.Controls.Add(this.SaveButton); this.tabPage6.ImageKey = "script.png"; this.tabPage6.Location = new System.Drawing.Point(4, 44); this.tabPage6.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.tabPage6.Name = "tabPage6"; this.tabPage6.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4); this.tabPage6.Size = new System.Drawing.Size(433, 600); this.tabPage6.TabIndex = 6; this.tabPage6.Text = "Settings"; this.tabPage6.UseVisualStyleBackColor = true; // // LoadButton // this.LoadButton.Image = ((System.Drawing.Image)(resources.GetObject("LoadButton.Image"))); this.LoadButton.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft; this.LoadButton.Location = new System.Drawing.Point(128, 8); this.LoadButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.LoadButton.Name = "LoadButton"; this.LoadButton.Size = new System.Drawing.Size(115, 39); this.LoadButton.TabIndex = 1; this.LoadButton.Text = "Load"; this.LoadButton.UseVisualStyleBackColor = true; this.LoadButton.Click += new System.EventHandler(this.LoadProject); // // SaveButton // this.SaveButton.Image = ((System.Drawing.Image)(resources.GetObject("SaveButton.Image"))); this.SaveButton.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft; this.SaveButton.Location = new System.Drawing.Point(7, 8); this.SaveButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.SaveButton.Name = "SaveButton"; this.SaveButton.Size = new System.Drawing.Size(115, 39); this.SaveButton.TabIndex = 0; this.SaveButton.Text = "Save"; this.SaveButton.UseVisualStyleBackColor = true; this.SaveButton.Click += new System.EventHandler(this.SaveProject); // // tabPage5 // this.tabPage5.Controls.Add(this.ProgramSettingsGrid); this.tabPage5.ImageKey = "application_xp_terminal.png"; this.tabPage5.Location = new System.Drawing.Point(4, 44); this.tabPage5.Name = "tabPage5"; this.tabPage5.Padding = new System.Windows.Forms.Padding(3); this.tabPage5.Size = new System.Drawing.Size(433, 600); this.tabPage5.TabIndex = 7; this.tabPage5.Text = "Programs"; this.tabPage5.UseVisualStyleBackColor = true; // // ProgramSettingsGrid // this.ProgramSettingsGrid.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.ProgramSettingsGrid.Location = new System.Drawing.Point(6, 6); this.ProgramSettingsGrid.Name = "ProgramSettingsGrid"; this.ProgramSettingsGrid.Size = new System.Drawing.Size(421, 588); this.ProgramSettingsGrid.TabIndex = 0; this.ProgramSettingsGrid.ToolbarVisible = false; // // imageList1 // this.imageList1.ImageStream = ((System.Windows.Forms.ImageListStreamer)(resources.GetObject("imageList1.ImageStream"))); this.imageList1.TransparentColor = System.Drawing.Color.Transparent; this.imageList1.Images.SetKeyName(0, "sound.png"); this.imageList1.Images.SetKeyName(1, "film.png"); this.imageList1.Images.SetKeyName(2, "script.png"); this.imageList1.Images.SetKeyName(3, "music.png"); this.imageList1.Images.SetKeyName(4, "layout.png"); this.imageList1.Images.SetKeyName(5, "picture.png"); this.imageList1.Images.SetKeyName(6, "application_xp_terminal.png"); // // BackgroundColorButton // this.BackgroundColorButton.BackColor = System.Drawing.Color.Black; this.BackgroundColorButton.Color = System.Drawing.Color.Black; this.BackgroundColorButton.ForeColor = System.Drawing.Color.White; this.BackgroundColorButton.Location = new System.Drawing.Point(118, 6); this.BackgroundColorButton.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.BackgroundColorButton.Name = "BackgroundColorButton"; this.BackgroundColorButton.Size = new System.Drawing.Size(114, 33); this.BackgroundColorButton.TabIndex = 1; this.BackgroundColorButton.Text = "Black"; this.BackgroundColorButton.UseVisualStyleBackColor = false; this.BackgroundColorButton.ColorChanged += new System.EventHandler(this.ControlValueChanged); // // SidWizPlusGui // this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; this.ClientSize = new System.Drawing.Size(1140, 648); this.Controls.Add(this.splitContainer1); this.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.Name = "SidWizPlusGui"; this.Text = "SidWizPlusGUI"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.HandleClosing); this.Load += new System.EventHandler(this.Initialize); this.splitContainer1.Panel1.ResumeLayout(false); this.splitContainer1.Panel1.PerformLayout(); this.splitContainer1.Panel2.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); this.splitContainer1.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.Preview)).EndInit(); this.contextMenuStrip1.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.PreviewTrackbar)).EndInit(); this.tabControl.ResumeLayout(false); this.channelsTab.ResumeLayout(false); this.channelsTab.PerformLayout(); this.toolStrip1.ResumeLayout(false); this.toolStrip1.PerformLayout(); this.tabPage1.ResumeLayout(false); this.tabPage1.PerformLayout(); this.groupBox2.ResumeLayout(false); this.groupBox2.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.MarginBottomControl)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.MarginRightControl)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.MarginLeftControl)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.MarginTopControl)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.Columns)).EndInit(); this.tabPage4.ResumeLayout(false); this.tabPage4.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.BackgroundImageControl)).EndInit(); this.tabPage3.ResumeLayout(false); this.tabPage3.PerformLayout(); this.tabPage2.ResumeLayout(false); this.tabPage2.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.RenderThreadsControl)).EndInit(); this.groupBox4.ResumeLayout(false); this.groupBox4.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.PreviewFrameskip)).EndInit(); this.groupBox3.ResumeLayout(false); this.groupBox3.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.FrameRateControl)).EndInit(); this.tabPage6.ResumeLayout(false); this.tabPage5.ResumeLayout(false); this.ResumeLayout(false); } #endregion private System.Windows.Forms.NumericUpDown Columns; private System.Windows.Forms.Label label1; private System.Windows.Forms.TextBox HeightControl; private System.Windows.Forms.Label label5; private System.Windows.Forms.TextBox WidthControl; private System.Windows.Forms.Label label4; private System.Windows.Forms.CheckBox MasterMixReplayGain; private System.Windows.Forms.CheckBox AutogenerateMasterMix; private System.Windows.Forms.Label label12; private System.Windows.Forms.Label label11; private System.Windows.Forms.Label label16; private System.Windows.Forms.Label label17; private System.Windows.Forms.TextBox VerticalScaling; private System.Windows.Forms.TabControl tabControl; private System.Windows.Forms.TabPage tabPage1; private System.Windows.Forms.TabPage tabPage3; private System.Windows.Forms.TabPage tabPage4; private System.Windows.Forms.Label label21; private ColorButton BackgroundColorButton; private System.Windows.Forms.Label label20; private System.Windows.Forms.PictureBox BackgroundImageControl; private System.Windows.Forms.Button AutoScaleButton; private System.Windows.Forms.PropertyGrid PropertyGrid; private System.Windows.Forms.SplitContainer splitContainer1; private System.Windows.Forms.ToolStrip toolStrip1; private System.Windows.Forms.PictureBox Preview; private System.Windows.Forms.TabPage channelsTab; private System.Windows.Forms.ToolStripButton AddFileButton; private System.Windows.Forms.ToolStripSeparator ChannelToolstripItemsSeparator; private System.Windows.Forms.GroupBox groupBox2; private System.Windows.Forms.Label label9; private System.Windows.Forms.Label label8; private System.Windows.Forms.Label label7; private System.Windows.Forms.Label label6; private System.Windows.Forms.NumericUpDown MarginBottomControl; private System.Windows.Forms.NumericUpDown MarginRightControl; private System.Windows.Forms.NumericUpDown MarginLeftControl; private System.Windows.Forms.NumericUpDown MarginTopControl; private System.Windows.Forms.TabPage tabPage2; private System.Windows.Forms.Label label13; private System.Windows.Forms.NumericUpDown PreviewFrameskip; private System.Windows.Forms.Label label10; private System.Windows.Forms.CheckBox PreviewCheckBox; private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; private System.Windows.Forms.ToolStripMenuItem moveLeftupToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem moveRightdownToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1; private System.Windows.Forms.ToolStripMenuItem removeChannelToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator3; private System.Windows.Forms.ToolStripMenuItem applySettingsToOtherChannelsToolStripMenuItem; private System.Windows.Forms.ImageList imageList1; private System.Windows.Forms.GroupBox groupBox3; private System.Windows.Forms.CheckBox EncodeCheckBox; private System.Windows.Forms.NumericUpDown FrameRateControl; private System.Windows.Forms.Label label14; private System.Windows.Forms.Label label15; private System.Windows.Forms.Button RenderButton; private System.Windows.Forms.TrackBar PreviewTrackbar; private System.Windows.Forms.Label label19; private System.Windows.Forms.TextBox MasterAudioPath; private System.Windows.Forms.ToolStripDropDownButton RemoveButton; private System.Windows.Forms.ToolStripMenuItem removeselectedToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem removeemptyToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem2; private System.Windows.Forms.ToolStripMenuItem removeallToolStripMenuItem; private System.Windows.Forms.TabPage tabPage6; private System.Windows.Forms.Button SaveButton; private System.Windows.Forms.Button LoadButton; private System.Windows.Forms.GroupBox groupBox4; private System.Windows.Forms.ToolStripMenuItem cloneChannelToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem copySettingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem pasteSettingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem splitChannelToolStripMenuItem; private System.Windows.Forms.ToolStripButton RemoveLabelsButton; private System.Windows.Forms.TabPage tabPage5; private System.Windows.Forms.PropertyGrid ProgramSettingsGrid; private System.Windows.Forms.ToolStripMenuItem setAsdefaultsToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator5; private System.Windows.Forms.ToolStripMenuItem resetToDefaultSettingsToolStripMenuItem; private System.Windows.Forms.ToolStripButton AddChannelButton; private System.Windows.Forms.Label ChannelsHelpLabel; private System.Windows.Forms.NumericUpDown RenderThreadsControl; private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label3; private ComboBox AudioCodec; private System.Windows.Forms.Label label18; private ComboBox VideoCodec; private TextBox ExtraFFMPEGParameters; private Label label22; } } ================================================ FILE: SidWiz/SidWizPlusGUI.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Design; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Forms.Design; using LibSidWiz; using LibSidWiz.Outputs; using LibSidWiz.Triggers; using Microsoft.WindowsAPICodePack.Dialogs; using Newtonsoft.Json; using SkiaSharp; namespace SidWizPlusGUI { public partial class SidWizPlusGui : Form { [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] public class ProgramSettings { [Category("FFMPEG")] [Editor(typeof(FileNameEditor), typeof(UITypeEditor))] [DisplayName("Path")] [Description("Path to FFMPEG. Download from https://ffmpeg.org/download.html")] public string FfmpegPath { get; set; } [Category("FFMPEG")] [DisplayName("Global Extra Parameters")] [Description("Extra parameters for FFMPEG. These are stored in your user settings, and will apply to all encodes. Per-encode settings can be entered on the Video tab.")] public string FfmpegExtraParameters { get; set; } [Category("MultiDumper")] [Editor(typeof(FileNameEditor), typeof(UITypeEditor))] [DisplayName("Path")] [Description("Path to Multidumper. Download from https://github.com/maxim-zhao/multidumper/releases")] public string MultiDumperPath { get; set; } [Category("MultiDumper")] [DisplayName("Sampling Rate")] [Description("Sampling rate for generated WAV files. Default is 44100. Requires a build of MultiDumper that supports this!")] [DefaultValue(44100)] public int MultiDumperSamplingRate { get; set; } = 44100; [Category("MultiDumper")] [DisplayName("Loop Count")] [Description("Default is 2 (so looped sections will be played twice in their entirety). Requires a build of MultiDumper that supports this!")] [DefaultValue(2)] public int MultiDumperLoopCount { get; set; } = 2; [Category("MultiDumper")] [DisplayName("Fade out time (ms)")] [Description("Default is 8000 (8s). Requires a build of MultiDumper that supports this!")] [DefaultValue(8000)] public int MultiDumperFadeMs { get; set; } = 8000; [Category("MultiDumper")] [DisplayName("Gap time (ms)")] [Description("Default is 1000 (1s). Requires a build of MultiDumper that supports this!")] [DefaultValue(1000)] public int MultiDumperGapMs { get; set; } = 1000; [Category("MultiDumper")] [DisplayName("Extra settings")] [Description("Extra settings to pass to MultiDumper")] [DefaultValue("")] public string MultiDumperExtraSettings { get; set; } = ""; [Category("SidPlay")] [Editor(typeof(FileNameEditor), typeof(UITypeEditor))] [DisplayName("Path")] public string SidPlayPath { get; set; } [Browsable(false)] public Channel DefaultChannelSettings { get; set; } = new(true) { Algorithm = new PeakSpeedTrigger(), LabelColor = Color.White, LabelFont = new Font(DefaultFont, FontStyle.Regular) }; } private ProgramSettings _programSettings = new(); // ReSharper disable MemberCanBePrivate.Local private class Settings { private bool _ignoreFromControls; public int Columns { get; set; } = 1; public List Channels { get; } = []; public float AutoScaleHeight { get; set; } = 100; public int Width { get; set; } = 1280; public int Height { get; set; } = 720; public int MarginTop { get; set; } public int MarginLeft { get; set; } public int MarginRight { get; set; } public int MarginBottom { get; set; } public int FrameRate { get; set; } = 60; public Color BackgroundColor { get; set; } = Color.Black; public string BackgroundImageFilename { get; set; } public PreviewSettings Preview { get; } = new() {Enabled = true, Frameskip = 1}; public EncodeSettings EncodeVideo { get; } = new() {Enabled = false, VideoCodec = "libx264", AudioCodec = "aac", ExtraParameters = ""}; public int RenderThreads { get; set; } = Environment.ProcessorCount; public MasterAudioSettings MasterAudio { get; } = new() {IsAutomatic = true, ApplyReplayGain = true}; public class MasterAudioSettings { public bool IsAutomatic { get; set; } public bool ApplyReplayGain { get; set; } public string Path { get; set; } } public class EncodeSettings { public bool Enabled { get; set; } public string VideoCodec { get; set; } public string AudioCodec { get; set; } public string ExtraParameters { get; set; } } public class PreviewSettings { public bool Enabled { get; set; } public int Frameskip { get; set; } } public void FromControls(SidWizPlusGui form) { if (_ignoreFromControls) { return; } AutoScaleHeight = float.Parse(form.VerticalScaling.Text); Width = int.Parse(form.WidthControl.Text); Height = int.Parse(form.HeightControl.Text); Columns = (int) form.Columns.Value; MarginTop = (int) form.MarginTopControl.Value; MarginLeft = (int) form.MarginLeftControl.Value; MarginRight = (int) form.MarginRightControl.Value; MarginBottom = (int) form.MarginBottomControl.Value; FrameRate = (int) form.FrameRateControl.Value; BackgroundColor = form.BackgroundColorButton.Color; Preview.Enabled = form.PreviewCheckBox.Checked; Preview.Frameskip = (int) form.PreviewFrameskip.Value; EncodeVideo.Enabled = form.EncodeCheckBox.Checked; EncodeVideo.VideoCodec = form.VideoCodec.Text.Split([' '], 2).FirstOrDefault() ?? "libx264"; EncodeVideo.AudioCodec = form.AudioCodec.Text.Split([' '], 2).FirstOrDefault() ?? "aac"; EncodeVideo.ExtraParameters = form.ExtraFFMPEGParameters.Text; MasterAudio.IsAutomatic = form.AutogenerateMasterMix.Checked; MasterAudio.ApplyReplayGain = form.MasterMixReplayGain.Checked; MasterAudio.Path = form.MasterAudioPath.Text; RenderThreads = (int) form.RenderThreadsControl.Value; } public void ToControls(SidWizPlusGui form) { // Disable control notifications as we load values into them _ignoreFromControls = true; form.VerticalScaling.Text = AutoScaleHeight.ToString(CultureInfo.CurrentCulture); form.WidthControl.Text = Width.ToString(); form.HeightControl.Text = Height.ToString(); form.Columns.Value = Columns; form.MarginTopControl.Value = MarginTop; form.MarginLeftControl.Value = MarginLeft; form.MarginRightControl.Value = MarginRight; form.MarginBottomControl.Value = MarginBottom; form.FrameRateControl.Value = FrameRate; form.BackgroundColorButton.Color = BackgroundColor; form.PreviewCheckBox.Checked = Preview.Enabled; form.PreviewFrameskip.Value = Preview.Frameskip; form.EncodeCheckBox.Checked = EncodeVideo.Enabled; form.VideoCodec.Text = EncodeVideo.VideoCodec; form.AudioCodec.Text = EncodeVideo.AudioCodec; form.ExtraFFMPEGParameters.Text = EncodeVideo.ExtraParameters; form.AutogenerateMasterMix.Checked = MasterAudio.IsAutomatic; form.MasterMixReplayGain.Checked = MasterAudio.ApplyReplayGain; form.MasterAudioPath.Text = MasterAudio.Path; form.RenderThreadsControl.Value = RenderThreads; _ignoreFromControls = false; } public Rectangle GetBounds() { return new Rectangle( MarginLeft, MarginTop, Width - MarginLeft - MarginRight, Height - MarginTop - MarginBottom); } } // ReSharper restore MemberCanBePrivate.Local private readonly Settings _settings = new(); // We do rendering on a worker thread, this means we have some complicated interactions // to make sure it doesn't render more or less than needed. This lot deals with that. private readonly object _renderLock = new(); private bool _renderNeeded; private bool _renderActive; private float _renderPosition; // We use this to allow cancelling the render private MainFormProgressOutput _progress; private FileSystemWatcher _settingsWatcher; public SidWizPlusGui() { InitializeComponent(); } private class FileTypeHandler { private readonly Action _handler; private readonly HashSet _extensions; public FileTypeHandler(string name, Action handler, params string[] extensions) { Name = name; _handler = handler; _extensions = [..extensions]; Filter = "*." + string.Join("; *.", extensions); } public string Name { get; } public string Filter { get; } public bool TryHandle(string filename) { var extension = Path.GetExtension(filename)?.ToLowerInvariant(); if (extension == null) { return false; } if (extension.StartsWith(".")) { extension = extension.Substring(1); } if (_extensions.Contains(extension)) { _handler(filename); return true; } return false; } } private void AddAFileClick(object sender, EventArgs e) { var handlers = new[] { // ReSharper disable once StringLiteralTypo new FileTypeHandler("Multidumper compatible files", LoadMultiDumper, "ay", "gbs", "gym", "hes", "kss", "nsf", "nsfe", "sap", "sfm", "sgc", "spc", "vgm", "vgz", "spu"), new FileTypeHandler("Wave audio files", AddChannel, "wav", "mp3"), new FileTypeHandler("SID files", LoadSid, "sid") }; var allFilesMask = string.Join("; ", handlers.Select(h => h.Filter)); var filter = string.Join("|", new[] { $"All supported files ({allFilesMask})", allFilesMask } .Concat(handlers.SelectMany(h => new[] {$"{h.Name} ({h.Filter})", h.Filter})) .Concat( [ "All files", "*.*" ])); using var ofd = new OpenFileDialog(); ofd.CheckFileExists = true; ofd.Filter = filter; ofd.Multiselect = true; if (ofd.ShowDialog(this) != DialogResult.OK) { return; } var errors = new List(); foreach (var filename in ofd.FileNames.OrderByAlphaNumeric(x => x)) { var path = Path.GetFullPath(filename); if (!handlers.Any(h => h.TryHandle(path))) { errors.Add($"Could not load \"{filename}\" - unknown extension"); } } if (errors.Count > 0) { MessageBox.Show(this, "Error(s) loading files:\n" + string.Join("\n", errors)); } } private void AddChannelButton_Click(object sender, EventArgs e) { AddChannel(""); } private void CloneChannelButton_Click(object sender, EventArgs e) { lock (_settings) { var source = PropertyGrid.SelectedObject as Channel; var index = _settings.Channels.IndexOf(source); if (index == -1 || source == null) { return; } // Duplicate the channel var channel = new Channel(true); channel.FromJson(source.ToJson(), false); // Insert it after the selected one _settings.Channels.Insert(index + 1, channel); // We attach to the event last, so we must also trigger it to load the data. channel.Changed += ChannelOnChanged; channel.LoadDataAsync(); } Render(); } private void AddChannel(string filename) { // We create a new Channel var channel = new Channel(true); channel.FromJson(_programSettings.DefaultChannelSettings.ToJson(), true); channel.Changed += ChannelOnChanged; lock (_settings) { _settings.Channels.Add(channel); } // Setting the filename triggers a load channel.Filename = filename; // We trigger a render to show the "loading" state Render(); } private void ChannelOnChanged(Channel channel, bool filenameChanged) { // If the filename changed then we do a load if (filenameChanged) { channel.LoadDataAsync(); } BeginInvoke(new Action(() => { // We check if the trackbar range needs to be changed lock (_settings) { var frameRate = _settings.FrameRate; var maxLength = _settings.Channels.Max(ch => ch.Length); var frames = maxLength.TotalSeconds * frameRate; PreviewTrackbar.Maximum = (int) frames; PreviewTrackbar.LargeChange = frameRate; PreviewTrackbar.TickFrequency = frameRate; } Render(); })); } private void LoadMultiDumper(string filename) { LocateProgram("multidumper.exe", _programSettings.MultiDumperPath, p => _programSettings.MultiDumperPath = p); try { // Normalize path filename = Path.GetFullPath(filename); using var form = new MultiDumperForm(filename, _programSettings.MultiDumperPath, _programSettings.MultiDumperSamplingRate, _programSettings.MultiDumperLoopCount, _programSettings.MultiDumperFadeMs, _programSettings.MultiDumperGapMs, _programSettings.MultiDumperExtraSettings); if (form.ShowDialog(this) != DialogResult.OK || form.Filenames == null) { return; } foreach (var wavFile in form.Filenames) { AddChannel(wavFile); } } catch (Exception ex) { MessageBox.Show($"Error running MultiDumper: {ex}"); } } private void LoadSid(string filename) { LocateProgram("sidplayfp.exe", _programSettings.SidPlayPath, p => _programSettings.SidPlayPath = p); try { using var form = new SidPlayForm(filename, _programSettings.SidPlayPath); if (form.ShowDialog(this) != DialogResult.OK || form.Filenames == null) { return; } foreach (var wavFile in form.Filenames) { AddChannel(wavFile); } } catch (Exception ex) { MessageBox.Show($"Error running SidPlay: {ex}"); } } private void LocateProgram(string filename, string currentValue, Action saveToSettings) { // Get path if we don't already have it if (string.IsNullOrEmpty(currentValue) || !File.Exists(currentValue)) { // Check if it's in the program directory var directory = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location); if (directory != null) { var path = Path.Combine(directory, filename); if (File.Exists(path)) { saveToSettings(path); SaveProgramSettings(); return; } } // Else browse for it using var ofd = new OpenFileDialog(); ofd.Title = $"Please locate {filename}"; ofd.Filter = $"{filename}|{filename}|All files (*.*)|*.*"; if (ofd.ShowDialog(this) == DialogResult.OK) { saveToSettings(ofd.FileName); SaveProgramSettings(); } } } private void AutoScale_Click(object sender, EventArgs e) { lock (_settings) { if (_settings.Channels.Count <= 0) { return; } // Compute the scale for the channel with the highest value var scale = _settings.AutoScaleHeight / 100 / _settings.Channels.Max(channel => channel.Max); foreach (var channel in _settings.Channels) { channel.Scale = scale; } } } private void Render() { // If this is called while a render is in process, then wait until that is done, and do one at the end. lock (_renderLock) { // We update this here so the worker thread will get the latest value. _renderPosition = (float) PreviewTrackbar.Value / PreviewTrackbar.Maximum; // We have two flags to signal the need to render. // One indicates that we need to render; this can be set while rendering // to cause it to render again when done. if (_renderNeeded) { return; } _renderNeeded = true; // The second flag indicates that we have started the task to render. // This ensures we don't start two render tasks at the same time. if (_renderActive) { return; } _renderActive = true; } // And finally we start the task. Task.Factory.StartNew(() => { // We repeatedly render while the flag says we need to render. for (;;) { float renderPosition; lock (_renderLock) { _renderNeeded = false; renderPosition = _renderPosition; } // Create a renderer var renderer = CreateWaveformRenderer(); // Render a bitmap var bitmap = renderer.RenderFrame(renderPosition); BeginInvoke(new Action(() => { // Swap it with whatever is in the preview control var oldImage = Preview.Image; // ReSharper disable once AccessToModifiedClosure Preview.Image = bitmap; Preview.Refresh(); oldImage?.Dispose(); })); // If the flag got set while we were rendering, do it again lock (_renderLock) { if (_renderNeeded) { continue; } _renderActive = false; break; } } }); } private WaveformRenderer CreateWaveformRenderer() { // This can be on a worker thread, so we need to lock... lock (_settings) { var renderer = new WaveformRenderer { BackgroundColor = _settings.BackgroundColor, BackgroundImage = File.Exists(_settings.BackgroundImageFilename) ? Image.FromFile(_settings.BackgroundImageFilename) : null, Width = _settings.Width, Height = _settings.Height, Columns = _settings.Columns, FramesPerSecond = _settings.FrameRate, RenderingBounds = _settings.GetBounds() }; if (_settings.Channels.Count > 0) { // We don't support multiple sampling rates, but this lets us ignore "empty" tracks. renderer.SamplingRate = _settings.Channels.Max(c => c.SampleRate); } foreach (var channel in _settings.Channels) { renderer.AddChannel(channel); } return renderer; } } private void LeftButton_Click(object sender, EventArgs e) { MoveChannel(PropertyGrid.SelectedObject as Channel, -1); } private void RightButton_Click(object sender, EventArgs e) { MoveChannel(PropertyGrid.SelectedObject as Channel, +1); } private void MoveChannel(Channel channel, int delta) { lock (_settings) { var index = _settings.Channels.IndexOf(channel); if (index == -1) { return; } var newIndex = index + delta; if (newIndex < 0 || newIndex >= _settings.Channels.Count) { return; } _settings.Channels.RemoveAt(index); _settings.Channels.Insert(newIndex, channel); } Render(); } private void RemoveButton_Click(object sender, EventArgs e) { if (PropertyGrid.SelectedObject is not Channel channel) { return; } channel.Changed -= ChannelOnChanged; lock (_settings) { _settings.Channels.Remove(channel); } channel.Dispose(); PropertyGrid.SelectedObject = null; Render(); } private void Preview_MouseClick(object sender, MouseEventArgs e) { PropertyGrid.SelectedObject = GetClickedChannel(e.X, e.Y); if (PropertyGrid.SelectedObject != null) { tabControl.SelectedTab = channelsTab; } } private Channel GetClickedChannel(int clickX, int clickY) { // Determine which channel was clicked // This is tricky because the preview is scaling the image to fit, but we don't know the details // So we need to map the click into the image space // First we map the click to the preview space in the range 0..1 in each dimension var x = (double) clickX / Preview.Width; var y = (double) clickY / Preview.Height; // Next we map that to image space var imageAspectRatio = (double) Preview.Image.Width / Preview.Image.Height; var previewAspectRatio = (double) Preview.Width / Preview.Height; if (previewAspectRatio > imageAspectRatio) { // Preview is wider, we have pillar-boxing // So the y one is correct, x needs to be modified: // +--+-----+--+ // | | | | // +--+-----+--+ x = (x - 0.5) * previewAspectRatio / imageAspectRatio + 0.5; if (x is < 0 or > 1) { return null; } } else { // Image is wider, we have letterboxing y = (y - 0.5) * imageAspectRatio / previewAspectRatio + 0.5; if (y is < 0 or > 1) { return null; } } lock (_settings) { // Then we map that into the range for the borders in the image // 0,0------------------+ // | | // | 0,0--------+ | // | | | | // | +--------1,1 | // | | // +--------------------1,1 x = (x - (double)_settings.MarginLeft / _settings.Width) * _settings.Width / (_settings.Width - _settings.MarginLeft - _settings.MarginRight); if (x is < 0.0 or > 1.0) { return null; } y = (y - (double)_settings.MarginTop / _settings.Height) * _settings.Height / (_settings.Height - _settings.MarginTop - _settings.MarginBottom); if (y is < 0.0 or > 1.0) { return null; } // Then we map that to the row/column space var column = (int) (_settings.Columns * x); var numRows = _settings.Channels.Count / _settings.Columns + (_settings.Channels.Count % _settings.Columns == 0 ? 0 : 1); var row = (int) (numRows * y); var index = row * _settings.Columns + column; if (index >= _settings.Channels.Count || index < 0) { return null; } return _settings.Channels[index]; } } private void CopySettingsButton_Click(object sender, EventArgs e) { if (PropertyGrid.SelectedObject is not Channel source) { return; } lock (_settings) { var json = source.ToJson(); foreach (var channel in _settings.Channels.Where(channel => channel != source)) { channel.FromJson(json, true); } } } private void ControlValueChanged(object sender, EventArgs e) { try { lock (_settings) { _settings.FromControls(this); } } catch (Exception exception) { MessageBox.Show(exception.Message); } Render(); } private void BackgroundImageControl_Click(object sender, EventArgs e) { using var ofd = new OpenFileDialog(); ofd.Title = "Select an image"; ofd.Filter = "Image files (*.png;*.gif;*.jpg;*.jpeg;*.bmp;*.wmf)|*.png;*.gif;*.jpg;*.jpeg;*.bmp;*.wmf|All files (*.*)|*.*"; if (ofd.ShowDialog(this) != DialogResult.OK) { BackgroundImageControl.Image = null; lock (_settings) { _settings.BackgroundImageFilename = null; } } else { BackgroundImageControl.ImageLocation = ofd.FileName; lock (_settings) { _settings.BackgroundImageFilename = ofd.FileName; } } Render(); } private void RenderButton_Click(object sender, EventArgs e) { if (_progress != null) { // Cancel the rendering _progress.Cancel(); return; } _progress = new MainFormProgressOutput(this); var outputs = new List {_progress}; lock (_settings) { if (_settings.Preview.Enabled) { outputs.Add(new PreviewOutput(_settings.Preview.Frameskip)); } if (_settings.EncodeVideo.Enabled) { LocateProgram("ffmpeg.exe", _programSettings.FfmpegPath, p => _programSettings.FfmpegPath = p); using var saveFileDialog = new SaveFileDialog(); saveFileDialog.Title = "Select destination"; saveFileDialog.Filter = "Video files (*.mp4;*.mkv;*.avi;*.qt)|*.mp4;*.mkv;*.avi;*.qt|All files (*.*)|*.*"; if (saveFileDialog.ShowDialog(this) != DialogResult.OK) { // Cancel the whole operation _progress = null; foreach (var output in outputs) { output.Dispose(); } return; } var outputFilename = saveFileDialog.FileName; if (_settings.MasterAudio.IsAutomatic) { try { var filename = outputFilename + ".wav"; Mixer.MixToFile(_settings.Channels, filename, MasterMixReplayGain.Checked); MasterAudioPath.Text = filename; _settings.MasterAudio.Path = filename; } catch (Exception ex) { MessageBox.Show(this, $"Failed to mix audio: {ex.Message}"); } } else { _settings.MasterAudio.Path = MasterAudioPath.Text; } outputs.Add(new FfmpegOutput( _programSettings.FfmpegPath, outputFilename, _settings.Width, _settings.Height, _settings.FrameRate, string.Join(" ", _programSettings.FfmpegExtraParameters, _settings.EncodeVideo.ExtraParameters), _settings.MasterAudio.Path, _settings.EncodeVideo.VideoCodec, _settings.EncodeVideo.AudioCodec, true)); } } RenderButton.Text = "Cancel render"; // Start a background thread to do the rendering work Task.Factory.StartNew(() => { try { var renderer = CreateWaveformRenderer(); renderer.Render(outputs, _settings.RenderThreads, false); } catch (Exception exception) { BeginInvoke(new Action(() => MessageBox.Show(this, exception.Message))); } finally { foreach (var graphicsOutput in outputs) { graphicsOutput.Dispose(); } _progress = null; BeginInvoke(new Action(() => RenderButton.Text = "Render")); } }); } private class MainFormProgressOutput : IGraphicsOutput { private readonly SidWizPlusGui _form; private readonly Stopwatch _stopwatch; private int _frameIndex; private DateTime _updateTime = DateTime.MinValue; private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(100); private bool _cancelRequested; public MainFormProgressOutput(SidWizPlusGui form) { _form = form; _stopwatch = Stopwatch.StartNew(); } public void Cancel() { _cancelRequested = true; } public void Dispose() { _form.BeginInvoke(new Action(() => { if (_form.IsDisposed || !_form.Visible) { return; } _form.Text = "SidWizPlusGUI"; })); } public void Write(SKImage image, byte[] data, double fractionComplete, TimeSpan length) { if (_cancelRequested) { throw new Exception("Render cancelled"); } ++_frameIndex; // We don't need the data, just the progress var now = DateTime.UtcNow; if (now - _updateTime < _updateInterval) { return; } _updateTime = now; var elapsedSeconds = _stopwatch.Elapsed.TotalSeconds; var fps = _frameIndex / elapsedSeconds; var eta = TimeSpan.FromSeconds(elapsedSeconds / fractionComplete - elapsedSeconds); _form.BeginInvoke(new Action(() => { if (_form.IsDisposed || !_form.Visible) { return; } _form.Text = $"SidWizPlusGUI - {fractionComplete:P} of {length} @ {fps:F}fps, ETA {eta:g}"; })); } } private void AutogenerateMasterMix_CheckedChanged(object sender, EventArgs e) { MasterMixReplayGain.Enabled = AutogenerateMasterMix.Checked; } private void SetMasterAudioPath(object sender, EventArgs e) { using var ofd = new OpenFileDialog(); ofd.Title = "Select master audio file"; ofd.Filter = "Wave audio files (*.wav)|*.wav|All files (*.*)|*.*"; if (ofd.ShowDialog(this) != DialogResult.OK) { return; } MasterAudioPath.Text = ofd.FileName; } private void LoadProgramSettings(string settingsPath) { try { _programSettings = JsonConvert.DeserializeObject(File.ReadAllText(settingsPath)); } catch (Exception) { // Ignore it } } private void SaveProgramSettings() { var path = GetSettingsPath(); var directory = Path.GetDirectoryName(path); if (directory == null) { return; } Directory.CreateDirectory(directory); File.WriteAllText(path, JsonConvert.SerializeObject(_programSettings)); // It may be filled in automatically, so we need to refresh the UI ProgramSettingsGrid.SelectedObject = _programSettings; } private static string GetSettingsPath() { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SidWizPlus", "settings.json"); } private void Initialize(object sender, EventArgs e) { HighDpiHelper.AdjustControlImagesDpiScale(this); var settingsPath = GetSettingsPath(); LoadProgramSettings(settingsPath); if (File.Exists(settingsPath)) { _settingsWatcher = new FileSystemWatcher { Path = Path.GetDirectoryName(settingsPath), Filter = Path.GetFileName(settingsPath) }; _settingsWatcher.Changed += (_, _) => LoadProgramSettings(settingsPath); } ProgramSettingsGrid.BeginInvoke(new Action(() => { ProgramSettingsGrid.SelectedObject = _programSettings; })); lock (_settings) { _settings.ToControls(this); } // Use exe icon as form icon Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); // Replicate the right-click menu into the toolbar automatically, so I don't have to maintain both int indexToInsertAt = toolStrip1.Items.IndexOf(ChannelToolstripItemsSeparator) + 1; foreach (var item in contextMenuStrip1.Items.Cast().Reverse()) { ToolStripItem newItem = null; switch (item) { case ToolStripMenuItem when item == removeChannelToolStripMenuItem: // This is handled separately continue; case ToolStripMenuItem: newItem = new ToolStripButton { Image = item.Image, Text = item.Text, DisplayStyle = ToolStripItemDisplayStyle.Image }; newItem.Click += (_, _) => item.PerformClick(); break; case ToolStripSeparator: newItem = new ToolStripSeparator(); break; } if (newItem != null) { toolStrip1.Items.Insert(indexToInsertAt, newItem); } } // Load settings if passed on the commandline foreach (var arg in Environment.GetCommandLineArgs() .Skip(1) .Where(x => x.ToLowerInvariant().EndsWith(".sidwizplus.json"))) { LoadSettingsFromFile(arg); } } private void RemoveSilentChannels(object sender, EventArgs e) { lock (_settings) { foreach (var channel in _settings.Channels.Where(c => c.IsSilent).ToList()) { _settings.Channels.Remove(channel); if (PropertyGrid.SelectedObject == channel) { PropertyGrid.SelectedObject = null; } channel.Dispose(); } } Render(); } private void RemoveAllChannels(object sender, EventArgs e) { lock (_settings) { foreach (var channel in _settings.Channels) { channel.Dispose(); } _settings.Channels.Clear(); } PropertyGrid.SelectedObject = null; Render(); } private void SaveProject(object sender, EventArgs e) { using var sfd = new SaveFileDialog(); sfd.Filter = "SidWizPlus settings (*.sidwizplus.json)|*.sidwizplus.json|All files (*.*)|*.*"; if (sfd.ShowDialog(this) != DialogResult.OK) { return; } lock (_settings) { File.WriteAllText(sfd.FileName, JsonConvert.SerializeObject(_settings, new JsonSerializerSettings { Formatting = Formatting.Indented })); } } private void LoadProject(object sender, EventArgs e) { using var ofd = new OpenFileDialog(); ofd.Filter = "SidWizPlus settings (*.sidwizplus.json)|*.sidwizplus.json|All files (*.*)|*.*"; if (ofd.ShowDialog(this) != DialogResult.OK) { return; } LoadSettingsFromFile(ofd.FileName); } private void LoadSettingsFromFile(string path) { lock (_settings) { var newSettings = JsonConvert.DeserializeObject(File.ReadAllText(path)); // We inspect to decide what to do... var clearExistingChannels = false; var discardNewChannels = false; var removeMissingFiles = false; var removeDuplicates = false; if (newSettings.Channels.Any()) { // No new channels means nothing to check... // If we already have channels, ask if the user wants to add or replace. if (_settings.Channels.Any()) { using var dialog = new TaskDialog(); dialog.InstructionText = "Loading new channels"; dialog.Text = $"The settings being loaded contain {newSettings.Channels.Count} channels. What do you want to do?"; dialog.Cancelable = true; dialog.Icon = TaskDialogStandardIcon.Warning; dialog.StandardButtons = TaskDialogStandardButtons.Cancel; dialog.DetailsExpandedText = $""" Existing channels: {string.Join("\n", PrintFilenames(_settings.Channels, null))} New channels: {string.Join("\n", PrintFilenames(newSettings.Channels, _settings.Channels))} """; AddDialogButton(dialog, "Add", "Add to the existing channels", () => {}); AddDialogButton(dialog, "Replace", "Replace the existing channels", () => { clearExistingChannels = true; }); AddDialogButton(dialog, "Discard", "Discard the new channels", () => { discardNewChannels = true; }); if (dialog.Show() == TaskDialogResult.Cancel) { return; } } } if (!discardNewChannels && !clearExistingChannels) { // If the new channels refer to files that don't exist, the user may not want them. var missingFiles = newSettings.Channels .Select(x => x.Filename).Where(x => !File.Exists(x)) .Distinct() .ToList(); if (missingFiles.Any()) { using var dialog = new TaskDialog(); dialog.InstructionText = "Missing audio files"; dialog.Text = $"The settings being loaded contain {newSettings.Channels.Count} channels, but {missingFiles.Count} refer to files that don't exist. Do you want to add them anyway?"; dialog.Cancelable = true; dialog.Icon = TaskDialogStandardIcon.Warning; dialog.StandardButtons = TaskDialogStandardButtons.Yes | TaskDialogStandardButtons.No | TaskDialogStandardButtons.Cancel; dialog.DetailsExpandedText = string.Join("\n", PrintFilenames(newSettings.Channels, null)); switch (dialog.Show()) { case TaskDialogResult.Cancel: return; case TaskDialogResult.Yes: removeMissingFiles = true; // Also clear the channels from the settings for the next check newSettings.Channels.RemoveAll(x => missingFiles.Contains(x.Filename)); break; } } // Now also check for duplicates if (_settings.Channels.Any()) { var duplicates = newSettings.Channels .Where(x => _settings.Channels.Any(y => Path.GetFullPath(y.Filename) == Path.GetFullPath(x.Filename))) .ToList(); if (duplicates.Any()) { using var dialog = new TaskDialog(); dialog.InstructionText = "Duplicate audio files"; dialog.Text = $"The settings being loaded contain {duplicates.Count} channels where the file is already loaded. Do you want to add them anyway?"; dialog.Cancelable = true; dialog.Icon = TaskDialogStandardIcon.Warning; dialog.StandardButtons = TaskDialogStandardButtons.Yes | TaskDialogStandardButtons.No | TaskDialogStandardButtons.Cancel; dialog.DetailsExpandedText = "New channels:\n" + string.Join("\n", PrintFilenames(newSettings.Channels, _settings.Channels)); switch (dialog.Show()) { case TaskDialogResult.Cancel: return; case TaskDialogResult.No: removeDuplicates = true; break; } } } } // Deserialize again on top, so partial JSON loads will work if (clearExistingChannels) { _settings.Channels.Clear(); } var countOfOldChannels = _settings.Channels.Count; JsonConvert.PopulateObject(File.ReadAllText(path), _settings); if (discardNewChannels) { var oldChannels = _settings.Channels.Take(countOfOldChannels).ToList(); _settings.Channels.Clear(); _settings.Channels.AddRange(oldChannels); } // Then remove channels as specified if (removeMissingFiles) { var channelsToRemove = _settings.Channels .Skip(countOfOldChannels) .Where(x => !File.Exists(x.Filename)) .ToList(); foreach (var channel in channelsToRemove) { _settings.Channels.Remove(channel); } } if (removeDuplicates) { var oldChannels = _settings.Channels .Take(countOfOldChannels) .Select(x => Path.GetFullPath(x.Filename)) .ToList(); var channelsToRemove = _settings.Channels .Skip(countOfOldChannels) .Where(x => oldChannels.Contains(Path.GetFullPath(x.Filename))) .ToList(); foreach (var channel in channelsToRemove) { _settings.Channels.Remove(channel); } } // Finally, trigger a load on the new channels (and attach our event handler) foreach (var channel in _settings.Channels.Skip(countOfOldChannels)) { channel.Changed += ChannelOnChanged; channel.LoadDataAsync(); } _settings.ToControls(this); } } private static IEnumerable PrintFilenames(List channels, List existingChannels) { foreach (var channel in channels) { string icon; if (!File.Exists(channel.Filename)) { icon = "❌"; } else if ( existingChannels != null && !existingChannels.Contains(channel) && existingChannels.Any(x => x.Filename == channel.Filename)) { icon = "💥"; } else { icon = "🔉"; } yield return $@"{icon} {channel.Filename}"; } yield return "❌ = file not found"; yield return "💥 = file is already present"; } private static void AddDialogButton(TaskDialog dialog, string text, string instruction, Action action) { var button = new TaskDialogCommandLink { Text = text, Instruction = instruction, }; button.Click += (_, _) => { action(); // ReSharper disable once AccessToDisposedClosure dialog.Close(TaskDialogResult.Ok); }; dialog.Controls.Add(button); } private void CopyChannelSettings(object sender, EventArgs e) { if (PropertyGrid.SelectedObject is not Channel source) { return; } Clipboard.SetText(source.ToJson()); } private void PasteChannelSettings(object sender, EventArgs e) { if (PropertyGrid.SelectedObject is not Channel channel) { return; } try { channel.FromJson(Clipboard.GetText(), true); } catch (Exception exception) { MessageBox.Show(this, $"Error while pasting: \n\n{exception}"); } } private void SplitChannel(object sender, EventArgs e) { if (PropertyGrid.SelectedObject is not Channel channel) { return; } if (channel.IsMono()) { if (MessageBox.Show( this, "Data is not stereo, do you want to clone the channel instead?", "Split channel", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes) { CloneChannelButton_Click(sender, e); } return; } // We clone the channel for the right side var right = new Channel(true); var json = channel.ToJson(); right.FromJson(json, false); right.Side = Channel.Sides.Right; right.Changed += ChannelOnChanged; right.LoadDataAsync(); // We switch the existing channel to the left side channel.Side = Channel.Sides.Left; channel.LoadDataAsync(); lock (_settings) { var index = _settings.Channels.IndexOf(channel); _settings.Channels.Insert(index + 1, right); } // We trigger a render to show the "loading" state Render(); } private void RemoveAllLabels(object sender, EventArgs e) { lock (_settings) { foreach (var channel in _settings.Channels) { channel.Label = ""; } } } private void HandleClosing(object sender, FormClosingEventArgs e) { _settingsWatcher?.Dispose(); SaveProgramSettings(); } private void SaveAsDefaultSettings(object sender, EventArgs e) { if (PropertyGrid.SelectedObject is not Channel source) { return; } _programSettings.DefaultChannelSettings.FromJson(source.ToJson(), true); SaveProgramSettings(); } private void ResetToDefaultSettings(object sender, EventArgs e) { if (PropertyGrid.SelectedObject is not Channel channel) { return; } lock (_settings) { channel.FromJson(_programSettings.DefaultChannelSettings.ToJson(), true); } } private void PropertyGrid_SelectedObjectsChanged(object sender, EventArgs e) { PropertyGrid.Visible = PropertyGrid.SelectedObject != null; ChannelsHelpLabel.Visible = PropertyGrid.SelectedObject == null; } private void VideoCodec_DropDown(object sender, EventArgs e) { if (!File.Exists(_programSettings.FfmpegPath) || AudioCodec.Items.Count > 0 || VideoCodec.Items.Count > 0) { return; } try { // We run FFMPEG to find what codecs it supports using var process = new ProcessWrapper(_programSettings.FfmpegPath, "-encoders"); process.WaitForExit(); foreach (var grouping in process .Lines() .Select(x => Regex.Match(x, "^ (?[AV])[^ ]+ (?[^ ]+) +(?.+)$")) .Where(m => m.Success && !m.Groups["name"].Value.Contains("=")) .Select(m => new Codec(m.Groups["type"].Value, m.Groups["description"].Value, m.Groups["name"].Value)) .GroupBy(c => c.Type)) { var combo = grouping.Key switch { "A" => AudioCodec, "V" => VideoCodec, _ => null }; combo?.Items.Clear(); combo?.Items.AddRange(grouping.OrderBy(x => x.ToString()).ToArray()); } } catch (Exception ex) { MessageBox.Show($"Error querying FFMPEG: {ex.Message}"); } } private record Codec(string Type, string Description, string Name) { public string Type { get; } = Type; public string Description { get; } = Description; public string Name { get; } = Name; public override string ToString() { return $"{Name} {Description}"; } } } } ================================================ FILE: SidWiz/SidWizPlusGUI.csproj ================================================  net48 WinExe true true true Properties\app.manifest icon.ico Component ================================================ FILE: SidWiz/SidWizPlusGUI.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 122, 17 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 rkJggg== iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJnSURBVDhPddLNT9NwHAbwHtX4J+mFiK+JMYwNb56M MVEvusMSX9C5sSsSOanx7MVEuSBGgwy7tB1sghIofCVjsBdexlY2XNv18WmNFy2HJ78m6/fZ9/dJlXMJ 9U00bcwNZQyJZXQzljHMoRHdvJLUCn2J7J1TN94dA6AcFWUwpRX4YNasntSZasuVjYYtlZYtmam6RFPz L07fnDjx7+DfKLG0vrLb9mRWXFHXHfmy2pWJhbZYXU+yJVvGpnfk/IOZ4bBhP0o0rZv7HU/yJVfyG67k ftrycflQ1mpt2bZssX45cv15fjVs2E9Q0Dz05NumK8WyK0bJkexaV17O1DH+eQvPpspBbr0utm+/KnSu jRsrtHlIm+NBwSALWoeQ7xUvSHGzJ9q6LeOfNuHxDdqANqANaAPagDagzXvanFQiKV4hKID8qPRkcasn BW4yxn+lDWgD2oA2oA1oA9qANqDNmDLwVOMVIMs1iFn/cy6xzF+bNqANaAPagDagDWgD2iCS1DxlIGmY e20ntIA2oA1oA9qANpha6uBt0UKZ17k6YnhK5Mm8VJqN/wpGWUAb0CUIbUAbZKUbbFJp9TDkF1y+n1vY sVypNvnx7NtSazpSbzky+oFXCAoA2oA2oA30Dd/DRtXyEEuz4GJCnbyUyM31x3Nr/XF1kSejLtKGVwC4 EbhZcHIzXgnQSwh+i6Z0L/Tj8EMbhzZHFjQ6DqJJbhA27Ic2oM2RBdVWA7HHc+HDfi4k1F3agDagDWgD 2gTZZvYOehgc1rqhw3767s5O98e/btHEo4nFk/FP1Tobzx2cuTdbjTzSmr8B1uo6cuIIh6sAAAAASUVO RK5CYII= iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKqSURBVDhPdZPLTxNRFIfdmvhHuHbhwo0bXYjgQiMh qRQxEk2MotEoxjbRGAME1BIVgooQUcDEmkCkkPJICBoDKa+WAqXQ57SUATptebSdoWCnM/Pzzq0YTMri y7nJPec75965cwgApeXG0WME5CCxl5MLtfAEwawmf9WfhBIfhOCvoqjrL/eP74mMBE0uQSg09V7mWROW PhdC3hpAYkFPUdfB1guI2uoRsb7hSa6QSwB+uRfszycItOSDaSZ8OEvIA9OUB3/TGSy0aRAer6OT5BQk A10IDemwNFiBQN89MOa7qO3xotrkRuX3LBUd03jYbkd5Qx+0BpvrVLn58D9B3GdEcOABmN7b8HZeJ1xD dbcbCsmICjJihAgvYSUuguNFGIZjKK2b4U7f6T9CBZuuNtqV6bkFX1dWoE6wmVIwFpQwEcpglEmj35mC kFZgYUW8G9lAicHGUMH6fAt8ppvwkmLPtzK4jVepILGjwM5KsK9ImFwS8cO7i0A0hXVBhPA7A02NVaGC qL2RdvUYy+DquAJXeyk5ggfJXQXzaxIcqxKm2QwsgTSG3TvocQhYJce5/HI6K+CmXtGuix2lcH4qgfOj ll4cvwsscgrFsSbDGhJhCabpJBwvo2RPELYYaFdnqxaOZg0cTRoqSFAB4OJkLIRlzJFJbCvqfYiICAq0 L/4K2F+1tKtaPPu2CLONRXjW6SJHALxRwB/LRjeRza8BNhZ0r/i5LStYHqqiXedI8Ux9IeyvL1LBVipz oCC+k0FxTXaCUU+3DgHzUzCmx+Qz6uHr1BGBD1wyfqAgwsehrZ6hf+F5wqw6yX7KG6awIUiIJMnjSYiI JjOI8VnWCVvbMi5VWdP/vev95D0aGynQj4cL9JNKgX5CIJGgxgnhnH5yO183FtFUWpN/AGPgB8LRi7cv AAAAAElFTkSuQmCC 17, 17 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 RJJ+Ao18U3cRbJU0AAAAAElFTkSuQmCC iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAILSURBVDhPpY9LaxNRGIa7cO/On+CqFdFmRHQpiCs3 ohtvIAW1WUiUQkXQLnoBIVAaRUQshVLBFotGBJfWRTW17dhEM5mkteklNMFm7pecubx+mWomQhBLYZ75 zjnwPu85bQD2RMvD3dDycDcEv9zbTr44c5mtfbzaxBWWf3dyWXjTfoDY/xfJgH0NgZjkbN/3aOnTV2dn 7VgVbMx219Znu+0/lOZ7rc3UnWLu9ZHzoWA6IvuuBXsjDmNlGMbqKMziFHxHIpcO39UIZWfvVAGP1cRp LhMKXnKqxxQKJ6AXRqB+68N26iYqc1FI6X5ISw8g8XdRXewhYnSzMsRXnVoomIwwx9yClEmgujCE8ofb MMufqVX93V6fcngDep44yVmhYOIYC97v2YQJuEYQdE0BdikBa70f5o8eGPnr0IWLcPUsxPFIk2CMq/mu CVf9BEd6D/ZzCrWtZxR8CHP1HsxCFEbuEvTvZ6GlT8HVeIijzYKnnO0xFWw7iVp5nFofwVqj1pV6axf0 7AVomTNQv56AsnAYjvoF4pNmQSJi+Uyi8BjszWFYxT4Kx2CI1yh8jlpPQ+WPQ5k/BHnuIAlSqGcaglz8 aInJBVlffuHp+efQxMfQhDi07ADUzH2o6V4oSzEofBTK4g3YlRmfMnJDIAy03xKGOiaEwY4KTeufDAZT ocxIQ7AXWh7+P2j7BY3RGzIVTOkAAAAAAElFTkSuQmCC iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKWSURBVDhPjZJbTxNBGIb7l4AunhJ/hdeGU4FSMICI nDSaeKFGkGg0QhG0ECggimIBu4AGRShULBhMORTaIpQCPUN32bb7+s1UuSKEiyeTbud9vnd3RpNtGKsg mgk7ESSUbIOoEsguSXOhdAyXro3jYpmI8wYxdLlcnBP0w1cAaJjAuOCOY8EtQU6okIj4UQrhwyR2wwoC 0QT/LSkp7EUS2PDL6rhjP3H1gW1f0FvymcC5sBHHnT4/4gqFaWPBoxkEYwnsUiAaT3JxhFbv/hFWfTKs jiBIAmoiM0HYQYLb5h2+icEE/oiC4EGCT2YNdqiNa0fG6raEuk4Pvi6FIOhHVCZQ7K5DEvj4JkZhow1+ WmNyCjK1Ch0kqTqbLmF5S0KNyY3eSR+E4mGQQFRtawe41eODhyoyNgNHND3Jp8ekFLYCynHYuRXHzVfr 6Jr4A23RRy7A9DIJurd5dR1NL2qyofTJHMoIffMsdPSc/fefqrYVdFg90BYOkYCOacoZRX3XJr0bQWt9 lxf1nQwPak0emriB6o51VLW7UNm2iqqXa2i1uKDVvU8LJpei6TCRDhIkYeGa125e+UY7CSh43bhCLOPZ 4AqyCt6lBROLYQp7/+E5ns7DNLm6w8XDlRSsaHGivOU3mvqXkJU/wARWiI4Q6kxeft5noez5LzzsWURm Xj80dDUxOh/idSU6svtf8k6F3YmSpz9xr3Membl9JCgVMWwPUoO0YHLt86kwQXHzD9xtt5PADM05g1Ud mg2QwH3mBrrGWTQYbcjI6VE1Qol1feD7Hgamdk9835NoaJ1B7YtpJgiwj/hY0H+yCPrRoFA8QtfTwm+Y tvADsnSDdFRvkZn/hj5YH1XupZCZT87I6Y5k5Jq//QWbzBTt9kMQwwAAAABJRU5ErkJggg== 277, 17 AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAAB0 HgAAAk1TRnQBSQFMAgEBBwEAATgBAgE4AQIBEAEAARABAAT/ASEBAAj/AUIBTQE2BwABNgMAASgDAAFA AwABIAMAAQEBAAEgBgABIP8AFwADTAGPA1YBtQJiAV4B7gG6ATwBFgH/AbYBOgEUAf8BtAE5ARQB/wGx ATcBEwH/Aa0BNQESAf8BqgE0ARIB/wGoATIBEQH/AacBMQEQAf8BgAF4AVcB/gJlAV4B8QJbAVoBxAgA A0oBigNcAcMDXgHwA14B8ANeAfADXgHwA14B8ANeAfADXgHwA14B8ANeAfADXgHwA14B8ANMAZAEAANQ AZsBwwGOATgB/wHAAYsBNgH/Ab4BiAE0Af8BuwGFATEB/wG5AYMBLwH/AbYBgAEuAf8BtAFOASwB/wGy AUwBKgH/AbEBSwEoAf8BrgFJAScB/wGtAUYBJgH/AasBRQEkAf8BqQFDASMB/wGpAUEBIQH/A1ABm0QA AmEBWwHeMP8DXgHtCAADYgHhAfEB7wHtAf8B9wHzAfEB/wH4AfQB8QH/AfgB9AHwAf8B9wH0AfAB/wH3 AfMB8AH/AfcB8wHvAf8B9wHzAe8B/wH3AfMB7wH/AfcB8wHvAf8B+AHzAe8B/wHyAe8B6wH/A2IB7wQA AcgBkgE8Af8DIgH/AyMB/wMkAf8DJQH/AyYB/wMnAf8DJwH/AygB/wMpAf8DKgH/AysB/wMsAf8DLAH/ Ay0B/wGpAUIBIQH/RAACgAFiAf4E/wGvASoBAAH/Aa0BJwEAAf8BrQEnAQAF/wHwAeUB3gH/AfAB5QHe Af8B8AHlAd4B/wHwAeUB3gH/AfAB5QHeAf8B8AHlAd4F/wGAAXkBWAH+CAADXgHwAfYB8QHtAf8BvwHc AcIB/wG/AdwBwgH/Ab8B3AHCAf8BvwHcAcIB/wG/AdwBwgH/Ab8B3AHCAf8BvwHcAcIB/wG/AdwBwgH/ Ab8B3AHCAf8BvwHcAcIB/wH3Ae0B5gH/A14B8AQAAcoBlAE+Af8DHgH/Aw0B/wMNAf8DDgH/Aw8B/wMR Af8DEgH/AxMB/wMUAf8DFQH/AxYB/wMWAf8DGAH/AyoB/wGqAUMBIwH/RAABxAGGAR0F/wGvASoBAAH/ Ac0BnwE3Af8BrQEnAQAF/wHwAeUB3gH/AfYB7wHrAf8B9gHvAesB/wH2Ae8B6wH/AfYB7wHrAf8B8AHl Ad4F/wGoATQBEgH/CAADXgHwAfcB8QHtAf8BvwHcAcIB/wG/AdwBwgH/Ab8B3AHCAf8BrwHTAcUB/wGc AcgByQH/ATEBrwHRAf8BugHZAcMB/wG/AdwBwgH/Ab8B3AHCAf8BvwHcAcIB/wH2Ae0B5gH/A14B8AQA AcwBlwE/Af8DGwH/AwgB/wMJAf8DCgH/AwwB/wMNAf8DDwH/Aw8B/wMRAf8DEgH/AxIB/wMUAf8DFQH/ AycB/wGsAUUBJAH/RAABxQGGAR4F/wGvASoBAAH/Ac0BoAE4Af8BrQEnAQAF/wHwAeUB3gH/AfYB7wHr Af8B9QHsAeYB/wH1AewB5gH/AfYB7wHrAf8B8AHlAd4F/wGsATUBEwH/CAADXgHwAfcB8QHsAf8BEQGq AT0B/wEPAagBOgH/AQ0BpgE3Af8BAAE+AZoB/wEXAZ8B0wH/ARcBnwHRAf8BAgGGAa8B/wEDAZoBKgH/ AQEBmAEoAf8BAAGWASYB/wH2Ae4B5gH/A14B8AQAAc8BmgFCAf8DFwH/AwMB/wMEAf8DBgH/AwcB/wMJ Af8DCgH/AwsB/wMMAf8DDgH/Aw8B/wMQAf8DEQH/AyUB/wGtAUgBJgH/RAABxwGIAR0F/wG0ATEBAAH/ AdABpQFBAf8BsgEvAQAF/wHwAeUB3gH/AfYB7wHrAf8B9QHsAeYB/wH1AewB5gH/AfYB7wHrAf8B8AHl Ad4F/wGyATkBFAH/CAADXgHwAfcB8gHtAf8B6AG9AaEB/wHnAbsBnwH/AdABswGSAf8BIQGCATkB/wEH AY4BhgH/AQQBiwGHAf8BGQGDAYAB/wHHAaYBggH/AeABrgGOAf8B3gGsAYsB/wH2Ae4B5gH/A14B8AQA AdEBnAFDAf8DEgH/AwAB/wMAAf8DAQH/AwMB/wMEAf8DBgH/AwYB/wMIAf8DCgH/AwsB/wMNAf8DDQH/ AyIB/wGwAUoBKAH/RAAByAGKAR4F/wG3ATkBAwH/AdEBpgGDAf8BtgE2AQAF/wHwAeUB3gH/AfYB7wHr Af8B9gHvAesB/wH2Ae8B6wH/AfYB7wHrAf8B8AHlAd4F/wG2ATsBFgH/CAADXgHwAfcB8wHuAf8B5wG7 AZ8B/wHRAbYBkwH/AZ8BqgE7Af8BMgGyAYcB/wEoAb0BigH/ASQBuwGHAf8BLgGrAT4B/wGRAZMBJwH/ AcUBogFAAf8B3QGpAYgB/wH2Ae4B5wH/A14B8AQAAdQBngFFAf8DDQH/AwAB/wPRAf8DrQH/AwAB/wMA Af8DAQH/AwIB/wMEAf8DBgH/AwcB/wMIAf8DCgH/Ax4B/wGyAUwBKgH/RAABygGMAR8F/wG8AUIBFwH/ AboBQAEPAf8BugE/AQ0F/wHwAeUB3gH/AfAB5QHeAf8B8AHlAd4B/wHwAeUB3gH/AfAB5QHeAf8B8AHl Ad4F/wG5AT8BFwH/CAADXgHwAfcB9AHvAf8B5gG5AZ0B/wG2AbMBhwH/AYgBwgGTAf8BJgHFAY8B/wEW Ab4BgAH/ARIBugE9Af8BGwG9AUIB/wE7AbABPwH/AaMBlgEtAf8B3AGnAYQB/wH2Ae4B5wH/A14B8AQA AdUBoAFGAf8DCQH/AwAB/wMAAf8D3gH/A0QB/wMAAf8DAAH/AwAB/wMAAf8DAQH/AwIB/wMEAf8DBQH/ AxsB/wG1AU4BLAH/RAACgAFnAf4w/wG8AUEBGAH/CAADXgHwAfcB9QHwAf8B5QG4AZsB/wGhAbMBQgH/ AUABzQGgAf8BIQHFAZAB/wEZAcABhwH/ARUBvgGBAf8BFQG8AUEB/wEvAb0BhwH/AYkBkAEhAf8B2gGk AYEB/wH1Ae8B5wH/A14B8AQAAdgBogFJAf8DBAH/AwAB/wPPAf8DqAH/AwAB/wMAAf8DAAH/AwAB/wMA Af8DAAH/AwAB/wMAAf8DAQH/AxYB/wG3AYEBLgH/RAADTQH6BP8B7QHEAZsB/wHtAcQBmwH/Ae0BxAGb Af8B7QHEAZsB/wHtAcQBmwH/Ae0BxAGbAf8B7QHEAZsB/wHtAcQBmwH/Ae0BxAGbAf8B7QHEAZsF/wG+ AYEBGgH/CAADXgHwAfcB9QHyAf8B5AG3AZkB/wGjAbYBgAH/AYIB0AGnAf8BKAHJAZgB/wEgAcYBkQH/ ARwBwgGLAf8BGwHBAYcB/wE0AcIBjgH/AYwBkgEiAf8B2QGiAUAB/wH2AfAB6AH/A14B8AQAAdkBowFJ Af8DBAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAQH/AxYB/wG6 AYUBMAH/RAADXgHwBP8B7QHEAZwB/wH0AdoBwQH/AfQB2gHBAf8B9AHbAcIB/wH0AdsBwgH/AfQB2wHC Af8B9AHbAcIB/wH0AdsBwgH/AfQB2wHCAf8B7QHEAZsF/wKAAWIB/ggAA14B8AH3AfUB9AH/AeMBtQGX Af8BuAG3AYcB/wGTAcsBoQH/ATcB0gGoAf8BKgHLAZsB/wEmAcgBlwH/AS0ByQGYAf8BgwG7AYsB/wGl AZkBLQH/AdcBnwE9Af8B9wHwAekB/wNeAfAEAAHbAaQBSgH/AwEB/wMCAf8DAwH/AwQB/wMFAf8DBgH/ AwcB/wMJAf8DCwH/AwwB/wMNAf8DDwH/AxEB/wMTAf8BvQGHATMB/0QAA18B2AT/Ae0BxAGcAf8B7QHE AZwB/wHtAcQBnAH/Ae0BxAGbAf8B7QHEAZsB/wHtAcQBmwH/Ae0BxAGbAf8B7QHEAZsB/wHtAcQBmwH/ Ae0BxAGbBf8DXAH4CAADXgHwAfQB8wHyAf8B4wG0AZUB/wHQAbQBjQH/AakBtQGAAf8BkwHMAaEB/wGE AdEBqgH/AYIB0AGmAf8BiwHCAZQB/wGaAZ4BLAH/AcMBnQE2Af8B1gGdAToB/wH3AfIB6wH/A14B8AQA AdwBpwFLAf8B2wGkAUoB/wHaAaMBSQH/AdgBogFJAf8B1wGhAUgB/wHVAZ8BRgH/AdMBngFEAf8B0QGc AUMB/wHPAZoBQgH/Ac0BlwFAAf8BywGVAT4B/wHJAZQBPAH/AccBkQE7Af8BxAGPATkB/wHDAY0BNwH/ AcABiwE2Af9EAANQAZsw/wHWAaoBiQH/CAADYwHpA4AB/gL0AfMB/wH3AfUB9AH/AfcB9QH0Af8B9wH1 AfQB/wH3AfUB9AH/AfcB9QH0Af8B9wH1AfQB/wH3AfUB9AH/AfcB9QH0Af8B9wHzAewB/wHyAe8B6QH/ A2IB7wQAAqgBhQH9AfEB3AHOAf8B6gHBAaAB/wHoAbkBkgH/AegBuQGSAf8B6AG5AZIB/wHoAbkBkgH/ AegBuQGSAf8B6AG5AZIB/wHNAcgBxQH/AegBuQGSAf8BzQHIAcUB/wHoAbkBkgH/ARQBNAL/AegBxAGn Af8BqAGQAUAB/UQAA0ABcQNMAZADXQHMAc4BkwEiAf8CgAFpAf4BzQGSASIB/wHMAZABIgH/Ac0BkgEk Af8BzQGTASYB/wHLAZEBJAH/AmIBWQHvA1wBywHYAa4BiwH/AdcBrAGLAf8IAANLAY0DYAHoA14B8ANe AfADXgHwA14B8ANeAfADXgHwA14B8ANeAfADXgHwA14B8ANeAfADTAGPBAADWgHCA2UB9AHcAacBSwH/ AdwBpgFKAf8B2gGkAUoB/wHYAaIBSQH/AdgBoQFJAf8B1QGgAUYB/wHUAZ4BRQH/AdIBnQFDAf8BzwGa AUIB/wHOAZkBQAH/AcsBlgE/Af8ByQGUATwB/wNlAfQDWgHC/wCJAANbAdgDNAH6A10BzBgAA1wB2QMA Af8DXQHcCAADDwEUA0gBhANSAaMDVgG2A10ByQFhAl8B2gNhAesCTQFEAfoBlQEkAQAB/wGaASwBBAH/ AaMBNwESAf8BrQGCASIB/wFeAlwB2QMkATSQAANNAfoDOgFhAwAB/wMaAf8DGgH/AxoB/wMaAf8DGgH/ AxoB/wMAAf8DMgFPA1kB7wgAAz0BaAG4AZABMAH/AdYBugGjAf8B3wHGAbMB/wHnAdQBwwH/Ae4B3wHT Af8B9QHqAeIB/wH7AfQB7wH/Af0B+gH2Av8B/gH9Af8B+wHrAd8B/wH7Ae8B5gH/AcEBnQGAAf8DQAFv DAADKwFCA10BzANbAc0DTwGZAxMBGkAAAzgBWwNhAeYrAAH/A1kBxgNMAfUBeAKAAf4DgAH+AX0CgAH+ AWgCgAH+AV8CgAH+AVoCgAH+A1MB9ANRAaQDAAH/CAADSgGJAccBpAGFEv8B/gH8Af8B/gH7AfcB/wH+ AfcB8QH/Af4B9gHxAf8B5AGuAYYB/wH6AegB2wH/Ac4BrwGVAf8DSgGLCAADPwFtAzUB/wOFAf8DigH/ AyYB/wMAAf8DPwFsCAADKwFCA10BzANbAc0DTwGZAxMBGhgAAwIBAwNJAYUBYAJiAfYBIwInAf8UAAM9 AWgDEwEaDAADWwHQAz8BbANfAdUBEgHLAR8B/wEaAdcBPQH/AQUB0QEtAf8BMwHHASEB/wG2AbgBEwH/ AcsBrgECAf8DXgHXAz8BbANbAdAIAAM2AVcCYgFIAfYBtAGFARwB/wHZAaUBPgH/AdgBngEyAf8B1wGb AS0B/wHYAZYBJgH/AdYBkgEgAf8B1AGPARwB/wHTAY4BGgH/AeIBnQEsAf8B+gHjAdEB/wHYAbsBogH/ A1EBnggAAz4BagORAf8DqQH/A64B/wOrAf8DiwH/Aw0B/wMrAUEDPwFtAxYB/wMxAf8DOQH/AxEB/wMA Af8DPwFsBAADJwE6AycBOgMnAToDJwE6A2EB3AEsAjAB/wE1AjoB/wEjAicB/xAAAkYBRwGBAgAB7QH/ A1wBywwAA10B3wNZAcMDWwHQARIBxAEIAf8BDgG6AQAB/wHYAb0BIwL/AboBJQL/AbkBKAH/AdsBuwFA Af8DXgHSA1kBwwNdAd8IAAMPARQCVgFUAasB1QGuAYwB/wH9AfAB5QH/AfcBxwGiAf8B9wHPAa0B/wH5 AdUBtQH/AfgB2gG9Af8B+AHeAcIB/wH6AeEBxgH/AfoB5AHMAf8B/QH1AewB/wHiAc4BugH/A1YBswwA A0ABcAOJAf8DngH/A7AB/wOsAf8DOAH/A1IBoAM+AWoDNgH/A5EB/wOYAf8DlgH/AzkB/wMAAf8DKwFB ATQCNwH/ATICNgH/AS4CMgH/ASwCMAH/ASwCLwH/ARMCFAH/ASICJAH/ASQCKAH/DAADPgFqAx8BLAIA AeYB/wECAQAB7QH/Az4BawgAA18B2gNRAaIDXQHPAUIBvwEAAf8B3QHFASwC/wHCATMC/wG/ASoB/wGu AcsBrAH/ASsB4AH5Af8DWwHQA1EBogNfAdoMAAMdASgBtwGGARkB/wL+Af0B/wH6Ad4BwgH/AfoB3AG/ Af8B+QHbAcAB/wH5AdsBwAH/AfkB3QHBAf8B+gHbAcAB/wH6AdwBwwH/Af0B6wHeAf8B7AHcAc0B/wJd AVsByhAAAysBQgNgAeMDNgH/A4kB/wOdAf8DXgHtBAADQAFwAy4B/wOFAf8DmgH/A5cB/wMiAf8DUgGg ATsCPgH/AZkCmgH/AToCOwH/ATECMgH/ARMCFAH/AwIB/wEgAiIB/wEmAioB/wgAAz0BaQEYARAB7QH/ A0QBeAFAAT8BQAFuAQgBAAHtAf8CWwFfAdgIAANdAdQDRwGBA10BzAH4AcYBMQL/AcgBOgL/AcUBNQL/ AcQBLwH/AZgB2AHPAf8BmQHkAesB/wNdAc4DRgGBA10B1AwAAxYBHgG5AYYBFQH/Af4B/AH5Af8B+QHc Ab8B/wH4AdsBvwH/AfgB3AHAAf8B+QHbAcIB/wH5AdwBwQH/AfkB3QHCAf8B+gHdAcMB/wH7AecB1AH/ AfUB7QHiAf8CYQFbAeQcAAM0Af8DsQH/AwwB/wgAAysBQgNgAeMDFAH/Ay8B/wOHAf8DXgHtAYECgwH/ AZkCmwH/AygB/wMiAf8DFgH/AwwB/wEqAiwB/wEpAiwB/wQAA14B7QMsAUMBHQETAekB/wESAQMB4QH/ BAABBQEAAeEB/wIAAeEB/wgAA14B4gNbAdADKwH/AxoB/wMaAf8DGgH/AxoB/wMaAf8DGgH/A0gB9gNb AdADXgHiDAADBwEJAbkBhQEOAf8B/gH7AfcB/wH5AdwBwQH/AfgB3AG/Af8B+AHcAb8B/wH4AdsBwAH/ AfkB3QHAAf8B+QHdAcAB/wH5Ad0BwwH/AfsB4gHLAf8B/AH5AfUB/wJfAVEB+wQBGAADNwH/A7gB/wMR Af8UAAMSAf8DmgH/AwAB/wGFAocB/wGlAqYB/wOHAf8DhQH/A0AB/wM1Af8BhAKFAf8BLAIwAf8EAAJd AWEB3AMsAUMBJAEcAekB/wEaAQ0B4QH/BAABDQEAAeEB/wEGAQAB4QH/CAADXQHUA0cBgQNbAc0BNQHi AZ4B/wE/AeMBpAH/ATsB4wGgAf8BJgHgAZUB/wEdAeEBlQH/ARgB4gGTAf8DWwHQA0YBgQNdAdQQAAJq AUcB+QH8AfYB8AH/AfkB3wHHAf8B+QHcAb0B/wH6AdwBvwH/AfoB2wHBAf8B+gHdAcMB/wH6Ad0BwgH/ AfkB3QHEAf8B+wHgAckC/wH8AfoB/wHBAY0BFgH/AwwBDxgAAzwB/wO8Af8DFwH/FAADEwH/A6EB/wMA Af8BiAKLAf8BwgLDAf8DvAH/A7oB/wOeAf8DgwH/A5EB/wExATQBNQH/CAADOwFkAS4BKAHsAf8DRAF4 A0ABbgEdARYB7QH/AlsBXwHYCAADXwHaA1EBogNdAc8BEgHLAR8B/wEaAdcBPQH/AQUB0QEtAf8BMwHH ASEB/wG2AbgBEwH/AcsBrgECAf8DWwHQA1EBogNfAdoQAAJhAV8B2gH1AecB2AH/AfoB5QHSAf8B+QHa AbwB/wH5AdsBvAH/AfoB2wG/Af8B+gHdAcEB/wH6Ad0BwQH/AfkB3QHEAf8B+wHhAcgC/wH9AfsB/wHI AZQBGgH/Aw4BEhgAA0IB/wPBAf8DHAH/FAADGQH/A6UB/wMAAf8BiwKNAf8BkQKSAf8BjwKRAf8BjwKQ Af8BoQKjAf8DqQH/AaECogH/ATgBOgE7Af8MAAM7AWUDHwEsASYBHQHmAf8BJAEeAe0B/wM+AWsIAANd Ad8DXAHDA1sB0AESAcQBCAH/AQ4BugEAAf8B2AG9ASMC/wG6ASUC/wG5ASgB/wHbAbsBQAH/A14B0gNZ AcMDXQHfEAADWQG7AfAB2QHBAf8B+wHtAeEB/wH5AdoBwAH/AfkB3AHCAf8B+QHeAcUB/wH6AeABxwH/ AfoB4gHKAf8B+gHiAc0B/wH6AeUB0AL/Af4B/QH/AcsBjwEdAf8CZQFeAfEDLQFFFAADhQH/A8UB/wMg Af8UAAMgAf8DqgH/AwAB/wMnAToDJwE6AycBOgMnAToDYAHbAZsCnQH/Aa0CrwH/AT8CQgH/EAADRAF7 ATABLAHsAf8DXAHLDAADXQHRA0gBhANgAdQBQgG/AQAB/wHdAcUBLAL/AcIBMwL/Ab8BKgH/Aa4BywGs Af8BKwHgAfkB/wNcAdYDPwFsA1sB0BAAA1IBpAHtAdABsgL/AfYB8AH/AfoB4QHKAf8B+wHjAcwB/wH7 AeMB0AH/AfsB5gHTAf8B+wHpAdUB/wH8AekB2AH/AfwB6gHbA/8B/QH/AdIBnQE0Af8B7gHZAcEB/wJl AV8B5RQAA4oB/wPKAf8DIQH/FAADHgH/A68B/wMDAf8QAAMCAQMDSAGDA2IB9gGBAoMB/xQAAzsBZAMT ARoMAAMvAf8DXgHtA00B+gNiAfYDYgH2A2IB9gNiAfYDYgH2A2IB9gNNAfoDXQHMAwAB/xAAA00BkgHr AcoBpQL/Af0B+wH/Af0B6QHVAf8B/QHrAdgB/wH9AeoB2wH/Af0B7QHfAf8B/QHwAeIB/wH9AfEB5AH/ AfwB8AHkBf8B4AGgATMC/wH7AfkB/wHfAbgBhwH/FAADkAH/A80B/wOEAf8DHAH/AxMB/wMQAf8DDAH/ AxkB/wNCAf8DtAH/AwkB/xgAAzYBWQNlAeUoAANcAfgDUAGbAywB/wMaAf8DGgH/AxoB/wMaAf8DGgH/ AxoB/wMQAf8DSwGNA18B1RAAA0gBhAHrAcYBmgX/AfwB7wHiAf8B/QHwAecB/wH9AfEB6wH/Af0B9QHu Af8B/QH4AfEB/wH9AfoB9wL/AfwB+gX/Af4B+wH3Af8B9AHaAcAB/wNdAeoUAAOTAf8D0QH/A84B/wPL Af8DyAH/A8YB/wPCAf8DvwH/A7wB/wO5Af8DDQH/SAADYAHUA2AB8wNeAd0YAANcAdYDRwH5A14B1xAA Az8BbQHqAcABjBH/Af0B+QH0Af8B+wHzAeoB/wH4AesB2QH/AfgB5gHTAf8B9QHfAcYB/wHpAcsBpgH/ A14B7QM4AV0UAAOcAf8DmQH/A5UB/wORAf8DjQH/A4gB/wOEAf8DQgH/Az0B/wM5Af8DKQH/iAADJQE2 A1wBxgHqAbwBgQH/AegBtwE6Af8B5gGyATAB/wHkAbABKwH/A14B8ANgAeMDXQHPA10BzANZAbsDVAGo AzABSwMEAQVAAAFCAU0BPgcAAT4DAAEoAwABQAMAASADAAEBAQABAQYAAQEWAAP/AQAG/wIAAYABAQGA AQEEAAGAAQEBgAEBBAABgAEBAYABAQQAAYABAQGAAQEEAAGAAQEBgAEBBAABgAEBAYABAQQAAYABAQGA AQEEAAGAAQEBgAEBBAABgAEBAYABAQQAAYABAQGAAQEEAAGAAQEBgAEBBAABgAEBAYABAQQAAYABAQGA AQEEAAGAAQEBgAEBBAAG/wIAAv8BxwHjAQABAwT/AcABAwEAAQMBgwH/AfwB/wHAAQMBAAEDAQEBgwHw AfkBwAEDAQABAwEAAQEBAAHxAcABAwEAAQMBgAIAAeABwAEDAYABAwHAAYABAALAAQMBgAEDAfgBwAEA AYQBwAEDAYABAQL4AQABhAHAAQMBwAEBAvgBAALAAQMBwAEBAvgBAAHgAcABAwHAAQAC+AEAAfEBwAED AcABAAL4AfAB+QHAAQMBwAEAAfgBAAH8Af8BwAEDAcABAAH4AQAC/wHHAeMBwAEAAfgBAAT/AcABAAL/ Cw== ================================================ FILE: SidWiz/app.config ================================================ ================================================ FILE: SidWiz.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.35004.147 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SidWizPlusGUI", "SidWiz\SidWizPlusGUI.csproj", "{190749A3-C233-4825-9678-66CA9CEF295E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibSidWiz", "LibSidWiz\LibSidWiz.csproj", "{75DD5F75-A354-4348-8A2F-7EE0F6AD6D18}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SidWizPlus", "SidWizPlus\SidWizPlus.csproj", "{6E0D8279-E2FA-4FF5-921F-A408915A93A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibVgm", "LibVgm\LibVgm.csproj", "{50DBEB28-7D52-4658-A276-724E7415B7E1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E67AB1BB-89A3-43C3-BB08-C3E10462D84D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {190749A3-C233-4825-9678-66CA9CEF295E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {190749A3-C233-4825-9678-66CA9CEF295E}.Debug|Any CPU.Build.0 = Debug|Any CPU {190749A3-C233-4825-9678-66CA9CEF295E}.Release|Any CPU.ActiveCfg = Release|Any CPU {190749A3-C233-4825-9678-66CA9CEF295E}.Release|Any CPU.Build.0 = Release|Any CPU {75DD5F75-A354-4348-8A2F-7EE0F6AD6D18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75DD5F75-A354-4348-8A2F-7EE0F6AD6D18}.Debug|Any CPU.Build.0 = Debug|Any CPU {75DD5F75-A354-4348-8A2F-7EE0F6AD6D18}.Release|Any CPU.ActiveCfg = Release|Any CPU {75DD5F75-A354-4348-8A2F-7EE0F6AD6D18}.Release|Any CPU.Build.0 = Release|Any CPU {6E0D8279-E2FA-4FF5-921F-A408915A93A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6E0D8279-E2FA-4FF5-921F-A408915A93A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E0D8279-E2FA-4FF5-921F-A408915A93A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E0D8279-E2FA-4FF5-921F-A408915A93A0}.Release|Any CPU.Build.0 = Release|Any CPU {50DBEB28-7D52-4658-A276-724E7415B7E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {50DBEB28-7D52-4658-A276-724E7415B7E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {50DBEB28-7D52-4658-A276-724E7415B7E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {50DBEB28-7D52-4658-A276-724E7415B7E1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3609C250-9081-41B1-B36F-4DE0AA0E2F38} EndGlobalSection EndGlobal ================================================ FILE: SidWiz.sln.DotSettings ================================================  True True True True True True True True True True True True ================================================ FILE: SidWizPlus/App.config ================================================ ================================================ FILE: SidWizPlus/BackgroundRenderer.cs ================================================ using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.Windows.Forms; namespace SidWizPlus { class BackgroundRenderer: IDisposable { public Image Image { get; } public Rectangle WaveArea { get; private set; } private readonly Graphics _graphics; private readonly double _aspectRatio; private readonly int _width; private readonly int _height; public BackgroundRenderer(int width, int height, Color backgroundColor) { _width = width; _height = height; _aspectRatio = _width / (double) _height; Image = new Bitmap(_width, _height, PixelFormat.Format32bppArgb); WaveArea = new Rectangle(0, 0, _width, _height); _graphics = Graphics.FromImage(Image); // Fill it in using (var brush = new SolidBrush(backgroundColor)) { _graphics.FillRectangle(brush, WaveArea); } _graphics.TextRenderingHint = TextRenderingHint.AntiAlias; } public void Dispose() { _graphics?.Dispose(); Image?.Dispose(); } public void Add(ImageInfo imageInfo) { int imageWidth = imageInfo.Image.Width; int imageHeight = imageInfo.Image.Height; // Compute size if stretching if (imageInfo.StretchToFit) { double imageAspectRatio = imageWidth / (double) imageHeight; if (imageAspectRatio > _aspectRatio) { // Image has a wider aspect ratio imageWidth = _width; imageHeight = (int) Math.Round(_width / imageAspectRatio); } else { imageHeight = _height; imageWidth = (int) Math.Round(_height * imageAspectRatio); } } // Compute where to draw it var rect = AlignedRect(imageInfo.Alignment, _width, _height, imageWidth, imageHeight); var ia = new ImageAttributes(); if (imageInfo.StretchToFit) { // Make sure stretched images don't have fuzzy edges ia.SetWrapMode(WrapMode.TileFlipXY); } // Apply any alpha if (imageInfo.Alpha < 1.0) { ia.SetColorMatrix(new ColorMatrix {Matrix33 = imageInfo.Alpha}); } _graphics.DrawImage(imageInfo.Image, rect, 0, 0, imageInfo.Image.Width, imageInfo.Image.Height, GraphicsUnit.Pixel, ia); // Apply constriction Constrain(rect, imageInfo.ConstrainWaves); } public void Add(TextInfo textInfo) { using var font = new Font(textInfo.FontName, textInfo.FontSize, textInfo.FontStyle); // Measure it var size = _graphics.MeasureString(textInfo.Text, font, new SizeF(_width, _height)); // Draw it var rect = AlignedRect(textInfo.Alignment, _width, _height, (int) Math.Ceiling(size.Width), (int) Math.Ceiling(size.Height)); using (var brush = new SolidBrush(textInfo.Color)) { _graphics.DrawString(textInfo.Text, font, brush, rect, new StringFormat { Alignment = textInfo.Alignment switch { ContentAlignment.BottomCenter or ContentAlignment.MiddleCenter or ContentAlignment.TopCenter => StringAlignment.Center, ContentAlignment.BottomLeft or ContentAlignment.MiddleLeft or ContentAlignment.TopLeft => StringAlignment.Near, _ => StringAlignment.Far, } }); } // Apply constriction Constrain(rect, textInfo.ConstrainWaves); } private Rectangle AlignedRect(ContentAlignment alignment, int width, int height, int imageWidth, int imageHeight) { return alignment switch { ContentAlignment.TopLeft => new Rectangle(0, 0, imageWidth, imageHeight), ContentAlignment.TopCenter => new Rectangle((width - imageWidth) / 2, 0, imageWidth, imageHeight), ContentAlignment.TopRight => new Rectangle(width - imageWidth, 0, imageWidth, imageHeight), ContentAlignment.MiddleLeft => new Rectangle(0, (height - imageHeight) / 2, imageWidth, imageHeight), ContentAlignment.MiddleCenter => new Rectangle((width - imageWidth) / 2, (height - imageHeight) / 2, imageWidth, imageHeight), ContentAlignment.MiddleRight => new Rectangle(width - imageWidth, (height - imageHeight) / 2, imageWidth, imageHeight), ContentAlignment.BottomLeft => new Rectangle(0, height - imageHeight, imageWidth, imageHeight), ContentAlignment.BottomCenter => new Rectangle((width - imageWidth) / 2, height - imageHeight, imageWidth, imageHeight), ContentAlignment.BottomRight => new Rectangle(width - imageWidth, height - imageHeight, imageWidth, imageHeight), _ => throw new Exception("Unhandled enum value " + alignment), }; } private void Constrain(Rectangle source, DockStyle dockStyle) { var result = WaveArea; switch (dockStyle) { case DockStyle.Top: result.Y += source.Height; result.Height -= source.Height; break; case DockStyle.Bottom: result.Height -= source.Height; break; case DockStyle.Left: result.X += source.Width; result.Width -= source.Width; break; case DockStyle.Right: result.Width -= source.Width; break; } WaveArea = result; } } } ================================================ FILE: SidWizPlus/ImageInfo.cs ================================================ using System.Drawing; using System.Windows.Forms; namespace SidWizPlus { internal class ImageInfo { public Image Image { get; } public ContentAlignment Alignment { get; } public bool StretchToFit { get; } public DockStyle ConstrainWaves { get; } public float Alpha { get; } public ImageInfo(Image image, ContentAlignment alignment, bool stretchToFit, DockStyle constrainWaves, float alpha) { Image = image; Alignment = alignment; StretchToFit = stretchToFit; ConstrainWaves = constrainWaves; Alpha = alpha; } } } ================================================ FILE: SidWizPlus/Program.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using CommandLine; using Google; using Google.Apis.Auth.OAuth2; using Google.Apis.Auth.OAuth2.Responses; using Google.Apis.Services; using Google.Apis.Upload; using Google.Apis.YouTube.v3; using Google.Apis.YouTube.v3.Data; using LibSidWiz; using LibSidWiz.Outputs; using LibSidWiz.Triggers; using LibVgm; using Channel = LibSidWiz.Channel; namespace SidWizPlus { // ReSharper disable once ClassNeverInstantiated.Global internal class Program { // ReSharper disable once ClassNeverInstantiated.Local private class Settings { // ReSharper disable UnusedAutoPropertyAccessor.Local [Option('f', "files", Separator = ',', HelpText = "Input WAV files, comma-separated. Wildcards are accepted.", Group = "Inputs")] public IEnumerable InputFiles { get; set; } [Option('v', "vgm", Required = false, HelpText = "VGM file, if specified GD3 text is drawn", Group = "Inputs")] public string VgmFile { get; set; } [Option('m', "master", Required = false, HelpText = "Master audio file, if not specified then the inputs will be mixed to a new file")] public string MasterAudioFile { get; set; } // ReSharper disable once StringLiteralTypo [Option("nomastermix", HelpText = "Disable automatic generation of master audio file (on by default)")] public bool NoMasterMix { get; set;} // ReSharper disable once StringLiteralTypo [Option("nomastermixreplaygain", HelpText = "Disable automatic ReplayGain adjustment of automatically generated master audio file (on by default)")] public bool NoMasterMixReplayGain { get; set;} [Option('o', "output", Required = false, HelpText = "Output file", Group = "Outputs")] public string OutputFile { get; set; } // ReSharper disable once StringLiteralTypo [Option('p', "previewframeskip", Required = false, HelpText = "Enable a preview window with the specified frameskip - higher values give faster rendering by not drawing every frame to the screen.", Group = "Outputs")] public int PreviewFrameskip { get; set; } [Option('w', "width", Required = false, HelpText = "Width of image rendered", Default = 720*16/9)] public int Width { get; set; } [Option('h', "height", Required = false, HelpText = "Height of image rendered", Default = 720)] public int Height { get; set; } [Option('c', "columns", Required = false, HelpText = "Number of columns to render", Default = 1)] public int Columns { get; set; } // ReSharper disable once StringLiteralTypo [Option("maxaspectratio", Required = false, HelpText = "Maximum aspect ratio, used to automatically determine the number of columns", Default = -1.0)] public double MaximumAspectRatio { get; set; } // ReSharper disable once StringLiteralTypo [Option("viewms", Required = false, HelpText = "Rendered view width in ms", Default = 35)] public int ViewWidthMs { get; set; } [Option('r', "fps", Required = false, HelpText = "Frame rate", Default = 60)] public int FramesPerSecond { get; set; } // ReSharper disable once StringLiteralTypo [Option("linewidth", Required = false, HelpText = "Line width", Default = 3)] public float LineWidth { get; set; } // ReSharper disable once StringLiteralTypo [Option("linecolor", Required = false, HelpText = "Line color, can be hex or a .net color name", Default = "white")] public string LineColor { get; set; } // ReSharper disable once StringLiteralTypo [Option("fillcolor", Required = false, HelpText = "Fill color, can be hex or a .net color name", Default = "transparent")] public string FillColor { get; set; } // ReSharper disable once StringLiteralTypo [Option("fillbase", Required = false, HelpText = "Fill baseline, values in range -1..+1 make sense", Default = 0.0)] public double FillBase { get; set; } // ReSharper disable once StringLiteralTypo [Option('a', "autoscale", Required = false, HelpText = "Automatic scaling percentage. A value of 100 will make the peak amplitude just fit in the rendered area.")] public float AutoScalePercentage { get; set; } // ReSharper disable once StringLiteralTypo [Option("autoscaleignorepercussion", Required = false, HelpText = "Makes automatic scaling ignore YM2413 percussion channels")] public bool AutoScaleIgnoreYm2413Percussion { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelsfromvgm", Required = false, HelpText = "Attempt to label channels based on their filename")] public bool ChannelLabelsFromVgm { get; set; } // ReSharper disable once StringLiteralTypo [Option('t', "triggeralgorithm", Required = false, HelpText = "Trigger algorithm name", Default = nameof(PeakSpeedTrigger))] public string TriggerAlgorithm { get; set; } // ReSharper disable once StringLiteralTypo [Option("triggerlookahead", Required = false, HelpText = "Number of frames to allow the trigger to look ahead, zero means no lookahead", Default = 0)] public int TriggerLookahead { get; set; } // ReSharper disable once StringLiteralTypo [Option("triggerlookaheadonfailure", Required = false, HelpText = "Number of frames to allow the trigger to look ahead when failing to find a match with the default", Default = 1)] public int TriggerLookaheadOnFailureFrames { get; set; } // ReSharper disable once StringLiteralTypo [Option("highpass", Required = false, HelpText = "Enable high-pass filtering", Default = false)] public bool HighPass { get; set; } // ReSharper disable once StringLiteralTypo [Option("ffmpeg", Required = false, HelpText = "Path to FFMPEG. Required if rendering to a file. Will be discovered if on the path.")] public string FfMpegPath { get; set; } // ReSharper disable once StringLiteralTypo [Option("vcodec", Required = false, HelpText = "Video codec for FFMPEG", Default = "libx264")] public string VideoCodec { get; set; } // ReSharper disable once StringLiteralTypo [Option("acodec", Required = false, HelpText = "Audio codec for FFMPEG", Default = "aac")] public string AudioCodec { get; set; } // ReSharper disable once StringLiteralTypo [Option("ffmpegoptions", Required = false, HelpText = "Extra commandline options for FFMPEG, e.g. to set the output format. Surround value with quotes and start with a space, e.g. \" -crf 20\", to avoid conflicts with other parameters.", Default = "")] public string FfMpegExtraOptions { get; set; } // ReSharper disable once StringLiteralTypo [Option("multidumper", Required = false, HelpText = "Path to MultiDumper, if specified with --vgm and no --files then it will be invoked for the VGM")] // ReSharper disable once IdentifierTypo public string MultidumperPath { get; set; } // ReSharper disable once StringLiteralTypo [Option("multidumpersamplingrate", Required = false, HelpText = "Sampling rate for MultiDumper", Default = 44100)] // ReSharper disable once IdentifierTypo public int MultidumperSamplingRate { get; set; } // ReSharper disable once StringLiteralTypo [Option("multidumperloopcount", Required = false, HelpText = "Loop count for MultiDumper", Default = 2)] // ReSharper disable once IdentifierTypo public int MultidumperLoopCount { get; set; } // ReSharper disable once StringLiteralTypo [Option("multidumperfadeoutms", Required = false, HelpText = "Fade out time after looping for MultiDumper, in ms", Default = 8000)] // ReSharper disable once IdentifierTypo public int MultidumperFadeOutMs { get; set; } // ReSharper disable once StringLiteralTypo [Option("multidumpergapms", Required = false, HelpText = "Gap time for non-looping tracks for MultiDumper, in ms", Default = 1000)] // ReSharper disable once IdentifierTypo public int MultidumperGapMs { get; set; } // ReSharper disable once StringLiteralTypo [Option("multidumperoptions", Required = false, HelpText = "Extra arguments for MultiDumper. Surround value with quotes and start with a space, e.g. \" --default_filter\", to avoid conflicts with other parameters.", Default = "")] // ReSharper disable once IdentifierTypo public string MultidumperOptions { get; set; } // ReSharper disable once StringLiteralTypo [Option("silencethreshold", Required = false, HelpText = "Amplitude range treated as silent auto-importing from MultiDumper", Default = 0.01f)] // ReSharper disable once IdentifierTypo public float SilenceThreshold { get; set; } // ReSharper disable once StringLiteralTypo [Option("backgroundcolor", Required = false, HelpText = "Background color, can be hex or a .net color name", Default = "black")] public string BackgroundColor { get; set; } [Option("background", Required = false, HelpText = "Background image, drawn transparently in the background")] public string BackgroundImageFile { get; set; } [Option("logo", Required = false, HelpText = "Logo image, drawn in the lower right")] public string LogoImageFile { get; set; } // ReSharper disable once StringLiteralTypo [Option("gridcolor", Required = false, HelpText = "Grid color, can be hex or a .net color name", Default = "white")] public string GridColor { get; set; } // ReSharper disable once StringLiteralTypo [Option("gridwidth", Required = false, HelpText = "Grid line width", Default = 0)] public float GridLineWidth { get; set; } // ReSharper disable once StringLiteralTypo [Option("gridborder", Required = false, HelpText = "Draw a border around the waves as well as between them", Default = true)] public bool GridBorder { get; set; } // ReSharper disable once StringLiteralTypo [Option("zerolinecolor", Required = false, HelpText = "Zero line color", Default = "white")] public string ZeroLineColor { get; set; } // ReSharper disable once StringLiteralTypo [Option("zerolinewith", Required = false, HelpText = "Zero line width", Default = 0)] public float ZeroLineWidth { get; set; } // ReSharper disable once StringLiteralTypo [Option("gd3font", Required = false, HelpText = "Font for GD3 info", Default = "Tahoma")] public string Gd3Font { get; set; } [Option("gd3size", Required = false, HelpText = "Font size (in points) for GD3 info", Default = 16)] public float Gd3FontSize { get; set; } [Option("gd3color", Required = false, HelpText = "Font color for GD3 info", Default = "white")] public string Gd3FontColor { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelsfont", Required = false, HelpText = "Font for channel labels")] public string ChannelLabelsFont { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelssize", HelpText = "Font size for channel labels", Default = 8)] public float ChannelLabelsSize { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelscolor", HelpText = "Font color for channel labels", Default = "white")] public string ChannelLabelsColor { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelspadding", HelpText = "Padding for channel labels - more specific settings override this", Default = 0)] public int ChannelLabelsPadding { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelspaddingleft", HelpText = "Left padding for channel labels")] public int? ChannelLabelsPaddingLeft { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelspaddingright", HelpText = "Right padding for channel labels")] public int? ChannelLabelsPaddingRight { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelspaddingtop", HelpText = "Top padding for channel labels")] public int? ChannelLabelsPaddingTop { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelspaddingbottom", HelpText = "Bottom padding for channel labels")] public int? ChannelLabelsPaddingBottom { get; set; } // ReSharper disable once StringLiteralTypo [Option("labelsalignment", HelpText = "Alignment for channel labels", Default = ContentAlignment.TopLeft)] public ContentAlignment ChannelLabelsAlignment { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubesecret", HelpText = "YouTube client secret JSON file. Use this to specify a custom OAth key if the embedded one doesn't work.")] public string YouTubeUploadClientSecret { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubetitle", HelpText = "YouTube video title. If a VGM is specified then you can reference GD3 tags like [title], [system], [game], [composer]")] public string YouTubeTitle { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubecategory", HelpText = "YouTube video category", Default = "Gaming")] public string YouTubeCategory { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubetags", HelpText = "YouTube video tags, comma separated")] public string YouTubeTags { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubetagsfromgd3", HelpText = "Populate additional tags from the GD3 tag")] public bool YouTubeTagsFromGd3 { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubeplaylist", HelpText = "YouTube playlist title. If a VGM is specified then you can reference GD3 tags like [title], [system], [game], [composer]")] public string YouTubePlaylist { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubeplaylistdescriptionfile", HelpText = "Use the specified file for the playlist description")] public string YouTubePlaylistDescriptionFile { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubedescriptionsextratext", HelpText = "Extra text to append to descriptions")] public string YouTubeDescriptionsExtra { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubeonly", HelpText = "Only upload to YouTube")] public bool YouTubeOnly { get; set; } // ReSharper disable once StringLiteralTypo [Option("youtubemerge", HelpText = "Merge the specified videos (wildcard, results sorted alphabetically) to one file and upload to YouTube", Group="Inputs")] public string YouTubeMerge { get; set; } [Option("threads", HelpText = "Number of rendering threads to use. Defaults to as many CPUs as your computer has.", Default = -1)] public int ThreadCount { get; set; } [Option("verbose", HelpText = "Enable even more logging", Default = false)] public bool Verbose { get; set; } } static int Main(string[] args) { try { Console.OutputEncoding = Encoding.UTF8; // ReSharper disable once RedundantNameQualifier new CommandLine.Parser(settings => { settings.CaseSensitive = false; settings.AutoHelp = true; settings.AutoVersion = true; settings.HelpWriter = Console.Error; }) .ParseArguments(args) .WithParsed(Run); } catch (Exception e) { Console.Error.WriteLine($"Fatal: {e}"); return 1; } return 0; } private static void Run(Settings settings) { if (!settings.YouTubeOnly) { if (settings.VgmFile != null) { LogVerbose(settings, "Running MultiDumper because VGM file was specified"); RunMultiDumper(settings); } else { // We want to expand any wildcards in the input file list (and also fully qualify them) LogVerbose(settings, "Expanding wildcards in inputs..."); var inputs = new List(); foreach (var inputFile in settings.InputFiles) { if (File.Exists(inputFile)) { inputs.Add(Path.GetFullPath(inputFile)); continue; } var pattern = Path.GetFileName(inputFile) ?? throw new Exception($"Failed to match {inputFile}"); var pathPart = inputFile.Substring(0, inputFile.Length - pattern.Length); var directory = pathPart.Length > 0 ? Path.GetFullPath(pathPart) : Directory.GetCurrentDirectory(); var files = Directory.EnumerateFiles(directory, pattern).ToList(); if (files.Count == 0) { throw new Exception($"Failed to match {inputFile}"); } inputs.AddRange(files); } settings.InputFiles = inputs; LogVerbose(settings, string.Join("\n- ", Enumerable.Repeat("Input files are:", 1).Concat(inputs))); } if (settings.InputFiles == null || !settings.InputFiles.Any()) { throw new Exception("No inputs specified"); } LogVerbose(settings, "Creating channel objects..."); var channels = settings.InputFiles .AsParallel() .Select(filename => { var channel = new Channel(false) { Filename = filename, LineColor = ParseColor(settings.LineColor), LineWidth = settings.LineWidth, FillColor = ParseColor(settings.FillColor), FillBase = settings.FillBase, Label = Channel.GuessNameFromMultidumperFilename(filename), Algorithm = CreateTriggerAlgorithm(settings.TriggerAlgorithm), TriggerLookaheadFrames = settings.TriggerLookahead, TriggerLookaheadOnFailureFrames = settings.TriggerLookaheadOnFailureFrames, SilenceThreshold = settings.SilenceThreshold, ZeroLineWidth = settings.ZeroLineWidth, ZeroLineColor = ParseColor(settings.ZeroLineColor), LabelFont = settings.ChannelLabelsFont == null ? null : new Font(settings.ChannelLabelsFont, settings.ChannelLabelsSize), LabelColor = ParseColor(settings.ChannelLabelsColor), LabelAlignment = settings.ChannelLabelsAlignment, LabelMargins = new Padding( settings.ChannelLabelsPaddingLeft ?? settings.ChannelLabelsPadding, settings.ChannelLabelsPaddingTop ?? settings.ChannelLabelsPadding, settings.ChannelLabelsPaddingRight ?? settings.ChannelLabelsPadding, settings.ChannelLabelsPaddingBottom ?? settings.ChannelLabelsPadding), HighPassFilter = settings.HighPass }; channel.LoadDataAsync().Wait(); // We can only set this when the file is loaded channel.ViewWidthInMilliseconds = settings.ViewWidthMs; return channel; }) .Where(ch => ch.SampleCount > 0 && !ch.IsSilent) .OrderByAlphaNumeric(ch => ch.Filename) .ToList(); if (settings.AutoScalePercentage > 0) { LogVerbose(settings, "Auto-scaling..."); float max; static bool IsYm2413Percussion(Channel ch) => ch.Label.StartsWith("YM2413 ") && !ch.Label.StartsWith("YM2413 Tone"); if (settings.AutoScaleIgnoreYm2413Percussion) { var channelsToUse = channels.Where(channel => !IsYm2413Percussion(channel)).ToList(); if (channelsToUse.Count == 0) { // Fall back on overall max if all channels are percussion channelsToUse = channels; } max = channelsToUse.Max(ch => ch.Max); } else { max = channels.Max(ch => ch.Max); } var scale = settings.AutoScalePercentage / 100 / max; LogVerbose(settings, $"Applying scale of {scale} to all channels"); foreach (var channel in channels) { channel.Scale = scale; } } if (settings.ChannelLabelsFromVgm && settings.VgmFile != null) { LogVerbose(settings, "Guessing channel labels from VGM..."); TryGuessLabelsFromVgm(channels, settings.VgmFile); } if (settings.OutputFile != null) { // Emit normalized data to a WAV file for later mixing if (settings.MasterAudioFile == null && !settings.NoMasterMix) { settings.MasterAudioFile = settings.OutputFile + ".wav"; LogVerbose(settings, "Generating master audio file..."); Mixer.MixToFile(channels, settings.MasterAudioFile, !settings.NoMasterMixReplayGain); } } LogVerbose(settings, "Starting render..."); Render(settings, channels); LogVerbose(settings, "Render complete"); foreach (var channel in channels) { channel.Dispose(); } } if (settings.YouTubeTitle != null) { if (settings.YouTubeMerge != null) { UploadMergedToYouTube(settings).Wait(); } else { UploadToYouTube(settings).Wait(); } } } private class InstrumentState { public int Instrument { private get; set; } public int Ticks { get; private set; } private static readonly string[] Names = [ "Custom Instrument", "Violin", "Guitar", "Piano", "Flute", "Clarinet", "Oboe", "Trumpet", "Organ", "Horn", "Synthesizer", "Harpsichord", "Vibraphone", "Synthesizer Bass", "Acoustic Bass", "Electric Guitar" ]; public string Name => Names[Instrument]; public override string ToString() => $"{Name} ({TimeSpan.FromSeconds((double)Ticks / 44100)})"; public void AddTime(int ticks) { Ticks += ticks; } } private class ChannelState { private readonly List _instruments = []; private readonly Dictionary _instrumentsByChannel = []; private InstrumentState _currentInstrument; public bool KeyDown { private get; set; } public void SetInstrument(int instrument) { if (!_instrumentsByChannel.TryGetValue(instrument, out var state)) { state = new InstrumentState {Instrument = instrument}; _instruments.Add(state); _instrumentsByChannel.Add(instrument, state); } _currentInstrument = state; } public void AddTime(int ticks) { if (KeyDown) { _currentInstrument?.AddTime(ticks); } } public IEnumerable Instruments => _instruments; public override string ToString() => string.Join(", ", Instruments.Where(x => x.Ticks > 0)); } private static void TryGuessLabelsFromVgm(List channels, string vgmFile) { var file = new VgmFile(vgmFile); var channelStates = new Dictionary(); ChannelState GetChannelState(int channelIndex) { if (!channelStates.TryGetValue(channelIndex, out var channelState)) { channelState = new ChannelState(); channelStates.Add(channelIndex, channelState); } return channelState; } foreach (var command in file.Commands()) { switch (command) { case VgmFile.WaitCommand waitCommand: // Wait foreach (var channelState in channelStates.Values) { channelState.AddTime(waitCommand.Ticks); } break; case VgmFile.AddressDataCommand c: { if (c.Address is >= 0x30 and <= 0x38) { // YM2413 instrument GetChannelState(c.Address & 0xf).SetInstrument(c.Data >> 4); } else if (c.Address is >= 0x20 and <= 0x28) { // YM2413 key down var channelState = GetChannelState(c.Address & 0xf); channelState.KeyDown = (c.Data & 0b00010000) != 0; } break; } } } foreach (var kvp in channelStates.OrderBy(x => x.Key)) { Console.WriteLine($"YM2413 channel {kvp.Key}: {kvp.Value}"); } foreach (var channel in channels.Where(c => c.Label.StartsWith("YM2413 Tone "))) { var match = Regex.Match(channel.Label, "^YM2413 Tone (?[0-9])$"); if (!match.Success) { continue; } var index = Convert.ToInt32(match.Groups["index"].Value) - 1; if (channelStates.TryGetValue(index, out var channelState)) { var instruments = channelState.Instruments .Where(x => x.Ticks > 0) .Select(x => x.Name); channel.Label += ": " + string.Join("/\x200b", instruments); Console.WriteLine($"Channel {index} is {channel.Label}"); } } } private static Color ParseColor(string value) { // If it looks like hex, use that. // We support 3, 6 or 8 hex chars. var match = Regex.Match(value, "^#?(?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?([0-9a-fA-F]{2})?)$"); if (match.Success) { var hex = match.Groups["hex"].Value; if (hex.Length == 3) { hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}"; } if (hex.Length == 6) { hex = "ff" + hex; } int alpha = Convert.ToInt32(hex.Substring(0, 2), 16); int red = Convert.ToInt32(hex.Substring(2, 2), 16); int green = Convert.ToInt32(hex.Substring(4, 2), 16); int blue = Convert.ToInt32(hex.Substring(6, 2), 16); return Color.FromArgb(alpha, red, green, blue); } // Then try named colors var property = typeof(Color) .GetProperties(BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public) .FirstOrDefault(p => p.PropertyType == typeof(Color) && p.Name.Equals(value, StringComparison.InvariantCultureIgnoreCase)); return property == null ? throw new Exception($"Could not parse color {value}") : (Color)property.GetValue(null); } private static void RunMultiDumper(Settings settings) { if (!File.Exists(settings.MultidumperPath)) { settings.MultidumperPath = FindExecutable("multidumper.exe"); } if (settings.MultidumperPath == null && settings.VgmFile != null) { throw new Exception("Please pass --multidumper parameter to load a VGM file"); } if (settings.VgmFile != null && settings.InputFiles.Any()) { throw new Exception("Can't pass both --files and --vgm"); } if (settings.VgmFile == null) { throw new Exception("VGM file not specified"); } // We normalize the VGM path here because we need to know its directory... settings.VgmFile = Path.GetFullPath(settings.VgmFile); // Check if we have WAVs. Note that we use "natural" sorting to make sure 10 comes after 9. settings.InputFiles = Directory.EnumerateFiles( Path.GetDirectoryName(settings.VgmFile) ?? throw new Exception($"Can't get path from VGM \"{settings.VgmFile}\""), Path.GetFileNameWithoutExtension(settings.VgmFile) + " - *.wav") .ToList(); if (!settings.InputFiles.Any()) { Console.WriteLine("Running MultiDumper..."); // Let's run it var wrapper = new MultiDumperWrapper( settings.MultidumperPath, settings.MultidumperSamplingRate, settings.MultidumperLoopCount, settings.MultidumperFadeOutMs, settings.MultidumperGapMs, settings.MultidumperOptions); var song = wrapper.GetSongs(settings.VgmFile).First(); var filenames = wrapper.Dump(song, d => Console.Write($"\r{d:P0}")); settings.InputFiles = filenames.ToList(); Console.WriteLine($" done. {settings.InputFiles.Count()} files found."); } else { Console.WriteLine($"Skipping MultiDumper as {settings.InputFiles.Count()} files were already present."); } } private static void LogVerbose(Settings settings, string message) { if (!settings.Verbose) { return; } Console.WriteLine(message); } private static void Render(Settings settings, List channels) { Console.WriteLine("Generating background image..."); var backgroundImage = new BackgroundRenderer(settings.Width, settings.Height, ParseColor(settings.BackgroundColor)); if (settings.BackgroundImageFile != null) { using var bm = Image.FromFile(settings.BackgroundImageFile); backgroundImage.Add(new ImageInfo(bm, ContentAlignment.MiddleCenter, true, DockStyle.None, 0.5f)); } if (!string.IsNullOrEmpty(settings.LogoImageFile)) { using var bm = Image.FromFile(settings.LogoImageFile); backgroundImage.Add(new ImageInfo(bm, ContentAlignment.BottomRight, false, DockStyle.None, 1)); } if (settings.VgmFile != null) { var gd3 = Gd3Tag.LoadFromVgm(settings.VgmFile); var gd3Text = gd3.ToString(); if (gd3Text.Length > 0) { backgroundImage.Add(new TextInfo(gd3Text, settings.Gd3Font, settings.Gd3FontSize, ContentAlignment.BottomLeft, FontStyle.Regular, DockStyle.Bottom, ParseColor(settings.Gd3FontColor))); } } if (settings.MaximumAspectRatio > 0.0) { Console.WriteLine($"Determining column count for maximum aspect ratio {settings.MaximumAspectRatio}:"); for (var columns = 1; columns < 100; ++columns) { var width = backgroundImage.WaveArea.Width / columns; var rows = channels.Count / columns + (channels.Count % columns == 0 ? 0 : 1); var height = backgroundImage.WaveArea.Height / rows; var ratio = (double) width / height; Console.WriteLine($"- {columns} columns => {width} x {height} pixels => ratio {ratio}"); if (ratio < settings.MaximumAspectRatio) { settings.Columns = columns; break; } } } // If we are doing only YM2413, we consider adding a blank channel after the tone channels if (channels.All(x => x.Label.Contains("YM2413") && channels.Count % settings.Columns != 0)) { var numTone = channels.Count(x => x.Label.Contains(" Tone ")); // We add enough to pad out the tones, or to right-fill the percussion, whichever is fewer. var numToAdd = Math.Min(numTone % settings.Columns, channels.Count % settings.Columns); // Add them if (numToAdd > 0) { var emptyChannel = new Channel(false) { Filename = "" }; emptyChannel.LoadDataAsync().Wait(); for (var i = 0; i < numToAdd; ++i) { channels.InsertRange(numTone, Enumerable.Repeat(emptyChannel, numToAdd)); } } } var renderer = new WaveformRenderer { BackgroundImage = backgroundImage.Image, Columns = settings.Columns, FramesPerSecond = settings.FramesPerSecond, Width = settings.Width, Height = settings.Height, SamplingRate = channels.First().SampleRate, RenderingBounds = backgroundImage.WaveArea }; if (settings.GridLineWidth > 0) { foreach (var channel in channels) { channel.BorderColor = ParseColor(settings.GridColor); channel.BorderWidth = settings.GridLineWidth; channel.BorderEdges = settings.GridBorder; } } // Add the data to the renderer foreach (var channel in channels) { renderer.AddChannel(channel); } var outputs = new List(); if (settings.OutputFile != null) { Console.WriteLine("Adding FFMPEG renderer..."); if (!File.Exists(settings.FfMpegPath)) { // Try to find it settings.FfMpegPath = FindExecutable("ffmpeg.exe"); } outputs.Add(new FfmpegOutput(settings.FfMpegPath, settings.OutputFile, settings.Width, settings.Height, settings.FramesPerSecond, settings.FfMpegExtraOptions, settings.MasterAudioFile, settings.VideoCodec, settings.AudioCodec, false)); } if (settings.PreviewFrameskip > 0) { Console.WriteLine("Adding preview renderer..."); outputs.Add(new PreviewOutput(settings.PreviewFrameskip, true)); } try { if (settings.ThreadCount < 1) { settings.ThreadCount = Environment.ProcessorCount; LogVerbose(settings, $"Defaulted thread count to {settings.ThreadCount}"); } Console.WriteLine($"Rendering on {settings.ThreadCount} threads..."); var sw = Stopwatch.StartNew(); renderer.Render(outputs, settings.ThreadCount, settings.Verbose); sw.Stop(); int numFrames = (int) (channels.Max(x => x.Length).TotalSeconds * settings.FramesPerSecond); Console.WriteLine($"Rendering complete in {sw.Elapsed:g}, average {numFrames / sw.Elapsed.TotalSeconds:N} fps"); } catch (Exception ex) { // Should mean it was cancelled Console.WriteLine($"Rendering cancelled: {ex.Message}\n{ex}"); } finally { foreach (var graphicsOutput in outputs) { graphicsOutput.Dispose(); } } } private static string FindExecutable(string name) { string fullPath; // Look in the current directory... if (File.Exists(name)) { fullPath = Path.GetFullPath(name); } else { fullPath = Environment.GetEnvironmentVariable("PATH") ?.Split(Path.PathSeparator) .Select(path => Path.Combine(path, name)) .FirstOrDefault(File.Exists) ?? throw new Exception($"Could not find path to {name}"); } Console.WriteLine($"Found {name} at {fullPath}"); return fullPath; } private static ITriggerAlgorithm CreateTriggerAlgorithm(string name) { var type = AppDomain.CurrentDomain .GetAssemblies() .SelectMany(a => a.GetTypes()) .FirstOrDefault(t => typeof(ITriggerAlgorithm).IsAssignableFrom(t) && t.Name.ToLowerInvariant().Equals(name.ToLowerInvariant())) ?? throw new Exception($"Unknown trigger algorithm \"{name}\""); return Activator.CreateInstance(type) as ITriggerAlgorithm; } private static async Task UploadToYouTube(Settings settings) { var youtubeService = await GetYouTubeService(settings); var video = new Video { Snippet = new VideoSnippet { Title = settings.YouTubeTitle, CategoryId = "10" // Music }, Status = new VideoStatus {PrivacyStatus = "public"} // or "private" or "public" }; var tags = new List(); if (settings.YouTubeTags != null) { tags.AddRange(settings.YouTubeTags.Split(',')); } Gd3Tag gd3 = null; if (settings.VgmFile != null) { gd3 = Gd3Tag.LoadFromVgm(settings.VgmFile); } if (gd3 != null) { video.Snippet.Description = $"Oscilloscope View of {gd3.Title}"; if (gd3.Game.English.Length > 0) { video.Snippet.Description += $" from the game {gd3.Game.English}"; } if (gd3.System.English.Length > 0) { video.Snippet.Description += $" for the {gd3.System.English}"; } if (gd3.Composer.English.Length > 0) { video.Snippet.Description += $", composed by {gd3.Composer}"; } video.Snippet.Description += "."; if (gd3.Ripper.Length > 0) { video.Snippet.Description += $"\nRipped by {gd3.Ripper}"; } if (gd3.Notes.Length > 0) { video.Snippet.Description += "\n\nNotes:\n" + gd3.Notes; } } if (settings.YouTubeDescriptionsExtra != null) { video.Snippet.Description += "\n" + settings.YouTubeDescriptionsExtra; } video.Snippet.Description += "\n\nVideo created using SidWizPlus - https://github.com/maxim-zhao/SidWizPlus"; if (settings.YouTubeTagsFromGd3 && gd3 != null) { // We have a VGM file tags.Add(gd3.Game.English); tags.Add(gd3.System.English); tags.AddRange(gd3.Composer.English.Split(';')); } video.Snippet.Tags = tags.Distinct().Where(t => !string.IsNullOrEmpty(t)).Select(t => t.Trim()).ToList(); if (settings.YouTubeCategory != null) { var request = youtubeService.VideoCategories.List("snippet"); request.RegionCode = "US"; var response = await request.ExecuteAsync(); video.Snippet.CategoryId = response.Items .Where(c => c.Snippet.Title.ToLowerInvariant().Contains(settings.YouTubeCategory.ToLowerInvariant())) .Select(c => c.Id) .FirstOrDefault(); if (video.Snippet.CategoryId == null) { await Console.Error.WriteLineAsync($"Warning: couldn't find category matching \"{settings.YouTubeCategory}\", defaulting to \"Music\""); } } if (gd3 != null) { video.Snippet.Title = FormatFromGd3(video.Snippet.Title, gd3); } if (video.Snippet.Title.Length > 100) { video.Snippet.Title = video.Snippet.Title.Substring(0, 97) + "..."; } // We now escape some strings as the API doesn't do it internally... video.Snippet.Title = RemoveAngledBrackets(video.Snippet.Title); video.Snippet.Description = RemoveAngledBrackets(video.Snippet.Description); video.Snippet.Tags = video.Snippet.Tags.Select(RemoveAngledBrackets).ToList(); await UploadVideo(settings.OutputFile, youtubeService, video); if (settings.YouTubePlaylist != null && !string.IsNullOrEmpty(video.Id)) { if (gd3 != null) { settings.YouTubePlaylist = RemoveAngledBrackets(FormatFromGd3(settings.YouTubePlaylist, gd3)); } // We need to decide if it's an existing playlist // We iterate over all channels... var playlistsRequest = youtubeService.Playlists.List("snippet"); playlistsRequest.Mine = true; var playlistsResponse = await playlistsRequest.ExecuteAsync(); var playlist = playlistsResponse.Items.FirstOrDefault(p => p.Snippet.Title == settings.YouTubePlaylist); if (playlist == null) { // Create it playlist = new Playlist { Snippet = new PlaylistSnippet { Title = settings.YouTubePlaylist }, Status = new PlaylistStatus { PrivacyStatus = "public" } }; if (settings.YouTubePlaylistDescriptionFile != null) { playlist.Snippet.Description = RemoveAngledBrackets(File.ReadAllText(settings.YouTubePlaylistDescriptionFile)); } if (settings.YouTubeDescriptionsExtra != null) { playlist.Snippet.Description += "\n\n" + settings.YouTubeDescriptionsExtra; } playlist = await youtubeService.Playlists.Insert(playlist, "snippet, status").ExecuteAsync(); Console.WriteLine($"Created playlist \"{settings.YouTubePlaylist}\" with ID {playlist.Id}"); } // Add to it var newPlaylistItem = new PlaylistItem { Snippet = new PlaylistItemSnippet { PlaylistId = playlist.Id, ResourceId = new ResourceId {Kind = "youtube#video", VideoId = video.Id} } }; newPlaylistItem = await youtubeService.PlaylistItems.Insert(newPlaylistItem, "snippet").ExecuteAsync(); Console.WriteLine($"Added video {video.Id} ({video.Snippet.Title}) to playlist {playlist.Id} ({playlist.Snippet.Title}) as item {newPlaylistItem.Id}"); } return video.Id; } private static async Task UploadVideo(string filename, YouTubeService youtubeService, Video video) { using var fileStream = new FileStream(filename, FileMode.Open); var videosInsertRequest = youtubeService.Videos.Insert(video, "snippet,status", fileStream, "video/*"); videosInsertRequest.ChunkSize = ResumableUpload.MinimumChunkSize; long totalSize = fileStream.Length; bool shouldRetry = true; var sw = Stopwatch.StartNew(); videosInsertRequest.ProgressChanged += progress => { switch (progress.Status) { case UploadStatus.Uploading: { var elapsedSeconds = sw.Elapsed.TotalSeconds; var fractionComplete = (double)progress.BytesSent / totalSize; var eta = TimeSpan.FromSeconds(elapsedSeconds / fractionComplete - elapsedSeconds); var sent = (double)progress.BytesSent / 1024 / 1024; var kbPerSecond = progress.BytesSent / sw.Elapsed.TotalSeconds / 1024; Console.Write( $"\r{fractionComplete:P0} {sent:f}MB sent, average {kbPerSecond:f}KB/s, ETA {eta:g}"); break; } case UploadStatus.Failed: Console.Error.WriteLine($"Upload failed: {progress.Exception}"); // Google API says we can retry if we get a non-API error, or one of these four 5xx error codes shouldRetry = progress.Exception is not GoogleApiException errorCode || new[] { HttpStatusCode.InternalServerError, HttpStatusCode.BadGateway, HttpStatusCode.ServiceUnavailable, HttpStatusCode.GatewayTimeout }.Contains(errorCode.HttpStatusCode); if (shouldRetry) { Console.WriteLine("Retrying..."); } break; case UploadStatus.Completed: Console.WriteLine($"Progress: {progress.Status}"); shouldRetry = false; break; default: Console.WriteLine($"Progress: {progress.Status}"); break; } }; videosInsertRequest.ResponseReceived += video1 => { video.Id = video1.Id; Console.WriteLine($"\nUpload completed: video ID is {video1.Id}"); }; try { await videosInsertRequest.UploadAsync(); } catch (Exception ex) { await Console.Error.WriteLineAsync($"Upload failed: {ex}"); } while (shouldRetry) { try { await videosInsertRequest.ResumeAsync(); } catch (Exception ex) { await Console.Error.WriteLineAsync($"Upload failed: {ex}"); } } } private static async Task GetYouTubeService(Settings settings) { if (settings.YouTubeUploadClientSecret == null) { throw new Exception("No YouTube client secret provided"); } var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync( (await GoogleClientSecrets.FromFileAsync(settings.YouTubeUploadClientSecret)).Secrets, // This OAuth 2.0 access scope allows an application to upload files to the // authenticated user's YouTube channel, but doesn't allow other types of access. [YouTubeService.Scope.YoutubeUpload, YouTubeService.Scope.YoutubeForceSsl], "SidWizPlus", CancellationToken.None ); var youtubeService = new YouTubeService(new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = Assembly.GetExecutingAssembly().GetName().Name, GZipEnabled = true }); return youtubeService; } private static async Task UploadMergedToYouTube(Settings settings) { if (!File.Exists(settings.FfMpegPath)) { settings.FfMpegPath = FindExecutable("ffmpeg.exe"); } // First we look for the videos and collect some metadata var outputPath = Path.GetFullPath(settings.OutputFile); var directoryName = Path.GetDirectoryName(settings.YouTubeMerge); if (string.IsNullOrEmpty(directoryName)) { directoryName = "."; } var files = Directory.EnumerateFiles( directoryName, Path.GetFileName(settings.YouTubeMerge)) .AsParallel() .Select(Path.GetFullPath) .Where(path => path != outputPath) .Select(path => new { Path = path, Length = GetVideoDuration(path, settings), Gd3 = GetGd3(path) }) .OrderBy(x => x.Path) .ToList(); foreach (var file in files) { Console.WriteLine($"{file.Path} is {file.Length}, tag is {file.Gd3?.ToString() ?? ""}"); } var mergedGd3 = MergeGd3Tags(files.Select(x => x.Gd3).Where(x => x != null).ToList()); // Next we start to build the description with "chapter markers" var description = new StringBuilder() .AppendLine( $"Oscilloscope View of music from the game {mergedGd3.Game} for the {mergedGd3.System}."); if (mergedGd3.Composer.English.Length > 0) { description.AppendLine($"Composed by {mergedGd3.Composer}."); } description.AppendLine($"Ripped by {mergedGd3.Ripper}"); description.AppendLine("\nPlaylist:"); var position = TimeSpan.Zero; foreach (var file in files) { description.AppendLine($"{position:hh':'mm':'ss} {file.Gd3?.Title.ToString() ?? Path.GetFileNameWithoutExtension(file.Path)}"); position += file.Length; } if (settings.YouTubeDescriptionsExtra != null) { description.AppendLine($"\n{settings.YouTubeDescriptionsExtra}"); } description.AppendLine("\nVideo created using SidWizPlus - https://github.com/maxim-zhao/SidWizPlus"); Console.WriteLine("Video description:"); Console.WriteLine(description); // Now we merge the files... // We need to write them to a list file for FFMPEG var listFile = Path.GetTempFileName(); File.WriteAllLines(listFile, files.Select(f => $"file '{f.Path.Replace("'", "'\\''")}'")); using (var wrapper = new ProcessWrapper( settings.FfMpegPath, $"-hide_banner -y -f concat -safe 0 -i \"{listFile}\" -c copy {settings.FfMpegExtraOptions} \"{settings.OutputFile}\"", false, false, true)) { wrapper.WaitForExit(); } File.Delete(listFile); // Now we start the YouTube work... var youtubeService = await GetYouTubeService(settings); var video = new Video { Snippet = new VideoSnippet { Title = FormatFromGd3(settings.YouTubeTitle, mergedGd3).TrimEnd(' ', '-'), CategoryId = "10", // Music Description = description.ToString() }, Status = new VideoStatus {PrivacyStatus = "public"} }; if (settings.YouTubeCategory != null) { bool retry = false; int tryCount = 0; do { try { var request = youtubeService.VideoCategories.List("snippet"); request.RegionCode = "US"; ++tryCount; var response = await request.ExecuteAsync(); video.Snippet.CategoryId = response.Items .Where(c => c.Snippet.Title.ToLowerInvariant() .Contains(settings.YouTubeCategory.ToLowerInvariant())) .Select(c => c.Id) .FirstOrDefault(); if (video.Snippet.CategoryId == null) { await Console.Error.WriteLineAsync( $"Warning: couldn't find category matching \"{settings.YouTubeCategory}\", defaulting to \"Music\""); } } catch (AggregateException ex) { retry = ex.InnerExceptions.OfType().Any() && tryCount < 10; await Console.Error.WriteLineAsync($"Exception talking to YouTube; retry = {retry}, tryCount = {tryCount}"); } } while (retry); } if (video.Snippet.Title.Length > 100) { video.Snippet.Title = video.Snippet.Title.Substring(0, 97) + "..."; } if (video.Snippet.Description.Length > 4500) { video.Snippet.Description = video.Snippet.Description.Substring(0, 4450) + "...\nDescription truncated to fit in YouTube limits"; } // We now escape some strings as the API doesn't do it internally... video.Snippet.Title = RemoveAngledBrackets(video.Snippet.Title); video.Snippet.Description = RemoveAngledBrackets(video.Snippet.Description); await UploadVideo(settings.OutputFile, youtubeService, video); } private static Gd3Tag MergeGd3Tags(IList tags) { return new Gd3Tag { System = new Gd3Tag.MultiLanguageTag { English = MergeTags(tags, t => t.System.English), Japanese = MergeTags(tags, t => t.System.Japanese) }, Game = new Gd3Tag.MultiLanguageTag { English = MergeTags(tags, t => t.Game.English), Japanese = MergeTags(tags, t => t.Game.Japanese) }, Title = new Gd3Tag.MultiLanguageTag { English = MergeTags(tags, t => t.Title.English), Japanese = MergeTags(tags, t => t.Title.Japanese) }, Composer = new Gd3Tag.MultiLanguageTag { English = MergeTags(tags, t => t.Composer.English), Japanese = MergeTags(tags, t => t.Composer.Japanese) }, Ripper = MergeTags(tags, t => t.Ripper), Date = MergeTags(tags, t => t.Date) }; } private static string MergeTags(IEnumerable tags, Func getter) { return string.Join("; ", tags.Select(getter) .SelectMany(s => s.Split(';', ',')) .Select(s => s.Trim()) .Where(s => s.Length > 0) .Distinct() .OrderBy(s => s)); } private static Gd3Tag GetGd3(string path) { // We guess a file path for the VGM var vgmPath = Path.ChangeExtension(path, "vgm"); if (!File.Exists(vgmPath)) { return null; } return new VgmFile(vgmPath).Gd3Tag; } private static TimeSpan GetVideoDuration(string path, Settings settings) { // This is a bit of a hack... var re = new Regex(" +Duration: (?[0-9:.]+)"); using var wrapper = new ProcessWrapper( settings.FfMpegPath, $"-i \"{path}\" -hide_banner", true); wrapper.WaitForExit(); var lines = wrapper.Lines().ToList(); var line = lines.FirstOrDefault(s => re.IsMatch(s)) ?? throw new Exception($"Failed to find duration for {path}. FFMPEG output:\n{string.Join("\n", lines)}"); return TimeSpan.Parse(re.Match(line).Groups["duration"].Value); } private static string RemoveAngledBrackets(string s) { return s.Replace("<", "").Replace(">", ""); } private static string FormatFromGd3(string pattern, Gd3Tag gd3) { return pattern .Replace("[title]", gd3.Title.English) .Replace("[system]", gd3.System.English) .Replace("[game]", gd3.Game.English) .Replace("[composer]", gd3.Composer.English); } } } ================================================ FILE: SidWizPlus/SidWizPlus.csproj ================================================  net48 Exe true latest true ================================================ FILE: SidWizPlus/TextInfo.cs ================================================ using System.Drawing; using System.Windows.Forms; namespace SidWizPlus { internal class TextInfo { public string Text { get; } public string FontName { get; } public float FontSize { get; } public FontStyle FontStyle { get; } public ContentAlignment Alignment { get; } public DockStyle ConstrainWaves { get; } public Color Color { get; } public TextInfo(string text, string fontName, float fontSize, ContentAlignment alignment, FontStyle fontStyle, DockStyle constrainWaves, Color color) { Text = text; Alignment = alignment; ConstrainWaves = constrainWaves; Color = color; FontName = fontName; FontSize = fontSize; FontStyle = fontStyle; } } } ================================================ FILE: appveyor.yml ================================================ version: '{build}' skip_tags: true image: Visual Studio 2022 configuration: Release environment: secret: secure: mFvP2wRhzLBcR2kQJuZw1pj5tpB0R17V4xSwO8VWuRLeLPQXn8AhyNgLjpnry/AoLdCQGMWxl94VlgdxfGlIl+QmhR+glo29vvPTQzyyKwCV4B5i/OvFngZ4PJX4225tlUr8lPEU+/JV82/FOscVwnXEEaOvp3NVegiCrrferZm0ufKd5Xy7L1noCAAPa3/r909r4rFcdA5cGQqLKb7t2S73hpM4zu7sj9ytxfgtg7ID/Hp1KXhkO9zS/OhCD3bTPIgqry4w1DIQ26U9yysSpFPfOePV3SOqNZWZjW2IcQ0ra6cwFL+6UQu8ZYFBWpL81mEM7+kCQYCh4XdXtpIEIVKSGG1qD7i4KWdziG4e/DBKDsIdNa0jxD0RJlU0sIVdIiPzf8mgMsbKgVWD/PmazGTkB2je9CdVOzQBMgAUkUs/6sthNTTEUCELdwc26TiHzGZYd5Tg+b99tjAp6jcoDAt2si85rEfFtk2q5ezbWQuUly41kOhd87C7/xEZRPSReM+VWt3MFCowK0UBpynxjj8lHp2fm8/PQOTRj6gJ3fQED8HbBfUrOLwvvEF/7U4QWs3atHhKcSWImNDk9QIfHg== before_build: - cmd: nuget restore build: parallel: true verbosity: minimal after_build: - cmd: | cd /d %APPVEYOR_BUILD_FOLDER%\SidWizPlus\bin\Release\net48 7z a -mx9 -r %APPVEYOR_BUILD_FOLDER%\sidwizplus.zip *.dll *.exe cd /d %APPVEYOR_BUILD_FOLDER%\SidWiz\bin\Release\net48 7z a -mx9 -r %APPVEYOR_BUILD_FOLDER%\sidwizplusgui.zip *.dll *.exe artifacts: - path: '*.zip' name: Binaries deploy: - provider: GitHub description: Appveyor automated build auth_token: secure: rKUfRHMGs7wVh/SYX9kJ67VqiPlztPwvnh3yjC3C2NX/Wpn9XsZrxIx0BXe9ZztT artifact: Binaries draft: false prerelease: false force_update: true