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
================================================
latesttrue
================================================
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
================================================
net48truetruelatesttrue..\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-resx2.0System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e08917, 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.0latesttrue
================================================
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

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://www.youtube.com/watch?v=H-Ip9c0yjGk "Sonic 3 - Ice Cap Zone - Brad Buxer")
[](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/
[](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