First working version of Rhubarb Lip Sync for Spine (C#, Windows Forms)

This commit is contained in:
Daniel Wolf 2017-11-10 22:09:21 +01:00
parent 7e7acb8068
commit df783f9afa
27 changed files with 2226 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

3
extras/rhubarb-for-spine/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin/
obj/
packages/

View File

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rhubarb-for-spine", "rhubarb-for-spine\rhubarb-for-spine.csproj", "{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace rhubarb_for_spine {
public class AnimationFileModel : ModelBase {
private readonly string animationFilePath;
private readonly SemaphoreSlim semaphore;
private readonly JObject json;
private string _mouthSlot;
private MouthNaming _mouthNaming;
private IReadOnlyCollection<MouthShape> _mouthShapes;
private string _mouthShapesDisplayString;
private BindingList<AudioFileModel> _audioFileModels;
public AnimationFileModel(string animationFilePath, SemaphoreSlim semaphore) {
this.animationFilePath = animationFilePath;
this.semaphore = semaphore;
json = SpineJson.ReadJson(animationFilePath);
SpineJson.ValidateJson(json, AnimationFileDirectory);
Slots = SpineJson.GetSlots(json);
MouthSlot = SpineJson.GuessMouthSlot(Slots);
var audioFileModels = SpineJson.GetAudioEvents(json)
.Select(CreateAudioFileModel)
.ToList();
AudioFileModels = new BindingList<AudioFileModel>(audioFileModels);
}
public IReadOnlyCollection<string> Slots { get; }
public string MouthSlot {
get { return _mouthSlot; }
set {
_mouthSlot = value;
MouthNaming = GetMouthNaming();
MouthShapes = GetMouthShapes();
OnPropertyChanged(nameof(MouthSlot));
}
}
public MouthNaming MouthNaming {
get { return _mouthNaming; }
private set {
_mouthNaming = value;
OnPropertyChanged(nameof(MouthNaming));
}
}
public IReadOnlyCollection<MouthShape> MouthShapes {
get { return _mouthShapes; }
private set {
_mouthShapes = value;
MouthShapesDisplayString = GetMouthShapesDisplayString();
OnPropertyChanged(nameof(MouthShapes));
}
}
public string MouthShapesDisplayString {
get { return _mouthShapesDisplayString; }
set {
_mouthShapesDisplayString = value;
SetError(nameof(MouthShapesDisplayString), GetMouthShapesDisplayStringError());
OnPropertyChanged(nameof(MouthShapesDisplayString));
}
}
public BindingList<AudioFileModel> AudioFileModels {
get { return _audioFileModels; }
set {
_audioFileModels = value;
if (!_audioFileModels.Any()) {
SetError(nameof(AudioFileModels), "The JSON file doesn't contain any audio events.");
}
OnPropertyChanged(nameof(AudioFileModels));
}
}
private string AnimationFileDirectory => Path.GetDirectoryName(animationFilePath);
private MouthNaming GetMouthNaming() {
IReadOnlyCollection<string> mouthNames = SpineJson.GetSlotAttachmentNames(json, _mouthSlot);
return MouthNaming.Guess(mouthNames);
}
private List<MouthShape> GetMouthShapes() {
IReadOnlyCollection<string> mouthNames = SpineJson.GetSlotAttachmentNames(json, _mouthSlot);
return rhubarb_for_spine.MouthShapes.All
.Where(shape => mouthNames.Contains(MouthNaming.GetName(shape)))
.ToList();
}
private string GetMouthShapesDisplayString() {
return MouthShapes
.Select(shape => shape.ToString())
.Join(", ");
}
private string GetMouthShapesDisplayStringError() {
var basicMouthShapes = rhubarb_for_spine.MouthShapes.Basic;
var missingBasicShapes = basicMouthShapes
.Where(shape => !MouthShapes.Contains(shape))
.ToList();
if (!missingBasicShapes.Any()) return null;
var result = new StringBuilder();
string missingString = string.Join(", ", missingBasicShapes);
result.AppendLine(missingBasicShapes.Count > 1
? $"Mouth shapes {missingString} are missing."
: $"Mouth shape {missingString} is missing.");
MouthShape first = basicMouthShapes.First();
MouthShape last = basicMouthShapes.Last();
result.Append($"At least the basic mouth shapes {first}-{last} need corresponding image attachments.");
return result.ToString();
}
private AudioFileModel CreateAudioFileModel(SpineJson.AudioEvent audioEvent) {
string audioDirectory = SpineJson.GetAudioDirectory(json, AnimationFileDirectory);
string filePath = Path.Combine(audioDirectory, audioEvent.RelativeAudioFilePath);
bool animatedPreviously = SpineJson.HasAnimation(json, GetAnimationName(audioEvent.Name));
var extendedMouthShapes = MouthShapes.Where(shape => !rhubarb_for_spine.MouthShapes.IsBasic(shape));
return new AudioFileModel(
audioEvent.Name, filePath, audioEvent.RelativeAudioFilePath, audioEvent.Dialog,
new HashSet<MouthShape>(extendedMouthShapes), animatedPreviously,
semaphore, cues => AcceptAnimationResult(cues, audioEvent));
}
private string GetAnimationName(string audioEventName) {
return $"say_{audioEventName}";
}
private void AcceptAnimationResult(
IReadOnlyCollection<MouthCue> mouthCues, SpineJson.AudioEvent audioEvent
) {
string animationName = GetAnimationName(audioEvent.Name);
SpineJson.CreateOrUpdateAnimation(
json, mouthCues, audioEvent.Name, animationName, MouthSlot, MouthNaming);
File.WriteAllText(animationFilePath, json.ToString(Formatting.Indented));
}
}
}

View File

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace rhubarb_for_spine {
public class AudioFileModel : ModelBase {
private readonly string filePath;
private readonly ISet<MouthShape> extendedMouthShapes;
private readonly SemaphoreSlim semaphore;
private readonly Action<IReadOnlyCollection<MouthCue>> reportResult;
private bool animatedPreviously;
private CancellationTokenSource cancellationTokenSource;
private AudioFileStatus _status;
private string _actionLabel;
private double? _animationProgress;
public AudioFileModel(
string name,
string filePath,
string displayFilePath,
string dialog,
ISet<MouthShape> extendedMouthShapes,
bool animatedPreviously,
SemaphoreSlim semaphore,
Action<IReadOnlyCollection<MouthCue>> reportResult
) {
this.reportResult = reportResult;
Name = name;
this.filePath = filePath;
this.extendedMouthShapes = extendedMouthShapes;
this.animatedPreviously = animatedPreviously;
this.semaphore = semaphore;
DisplayFilePath = displayFilePath;
Dialog = dialog;
Status = animatedPreviously ? AudioFileStatus.Done : AudioFileStatus.NotAnimated;
}
public string Name { get; }
public string DisplayFilePath { get; }
public string Dialog { get; }
public double? AnimationProgress {
get { return _animationProgress; }
private set {
_animationProgress = value;
OnPropertyChanged(nameof(AnimationProgress));
// Hack, so that a binding to Status will refresh
OnPropertyChanged(nameof(Status));
}
}
public AudioFileStatus Status {
get { return _status; }
private set {
_status = value;
ActionLabel = _status == AudioFileStatus.Scheduled || _status == AudioFileStatus.Animating
? "Cancel"
: "Animate";
OnPropertyChanged(nameof(Status));
}
}
public string ActionLabel {
get { return _actionLabel; }
private set {
_actionLabel = value;
OnPropertyChanged(nameof(ActionLabel));
}
}
public void PerformAction() {
switch (Status) {
case AudioFileStatus.NotAnimated:
case AudioFileStatus.Done:
StartAnimation();
break;
case AudioFileStatus.Scheduled:
case AudioFileStatus.Animating:
CancelAnimation();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private async void StartAnimation() {
cancellationTokenSource = new CancellationTokenSource();
Status = AudioFileStatus.Scheduled;
try {
await semaphore.WaitAsync(cancellationTokenSource.Token);
Status = AudioFileStatus.Animating;
try {
var progress = new Progress(value => AnimationProgress = value);
IReadOnlyCollection<MouthCue> mouthCues = await RhubarbCli.AnimateAsync(
filePath, Dialog, extendedMouthShapes, progress, cancellationTokenSource.Token);
animatedPreviously = true;
Status = AudioFileStatus.Done;
reportResult(mouthCues);
} finally {
AnimationProgress = null;
semaphore.Release();
cancellationTokenSource = null;
}
} catch (OperationCanceledException) {
Status = animatedPreviously ? AudioFileStatus.Done : AudioFileStatus.NotAnimated;
}
}
private void CancelAnimation() {
cancellationTokenSource.Cancel();
}
private class Progress : IProgress<double> {
private readonly Action<double> report;
private double value;
public Progress(Action<double> report) {
this.report = report;
}
public void Report(double progress) {
value = progress;
report(progress);
}
}
}
public enum AudioFileStatus {
NotAnimated,
Scheduled,
Animating,
Done
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace rhubarb_for_spine {
/// <summary>
/// A modification of the standard <see cref="ComboBox"/> in which a data binding
/// on the SelectedItem property with the update mode set to DataSourceUpdateMode.OnPropertyChanged
/// actually updates when a selection is made in the combobox.
/// Code taken from https://stackoverflow.com/a/8392100/52041
/// </summary>
public class BindableComboBox : ComboBox {
/// <inheritdoc />
protected override void OnSelectionChangeCommitted(EventArgs e) {
base.OnSelectionChangeCommitted(e);
var bindings = DataBindings
.Cast<Binding>()
.Where(binding => binding.PropertyName == nameof(SelectedItem)
&& binding.DataSourceUpdateMode == DataSourceUpdateMode.OnPropertyChanged);
foreach (Binding binding in bindings) {
// Force the binding to update from the new SelectedItem
binding.WriteValue();
// Force the Textbox to update from the binding
binding.ReadValue();
}
}
}
}

View File

@ -0,0 +1,366 @@
using System;
namespace rhubarb_for_spine {
partial class MainForm {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
// Some bindings cannot be added in InitializeComponent; see https://stackoverflow.com/a/47167781/52041.
mouthSlotComboBox.DataBindings.Add(new System.Windows.Forms.Binding("DataSource", animationFileBindingSource, "Slots", true));
mouthSlotComboBox.DataBindings.Add(new System.Windows.Forms.Binding("SelectedItem", animationFileBindingSource, "MouthSlot", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
audioEventsDataGridView.AutoGenerateColumns = false;
audioEventsDataGridView.DataBindings.Add(new System.Windows.Forms.Binding("DataSource", animationFileBindingSource, "AudioFileModels", true));
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.filePathLabel = new System.Windows.Forms.Label();
this.panel1 = new System.Windows.Forms.Panel();
this.filePathTextBox = new System.Windows.Forms.TextBox();
this.filePathBrowseButton = new System.Windows.Forms.Button();
this.mouthSlotLabel = new System.Windows.Forms.Label();
this.animationFileBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.mouthShapesLabel = new System.Windows.Forms.Label();
this.audioEventsLabel = new System.Windows.Forms.Label();
this.audioEventsDataGridView = new System.Windows.Forms.DataGridView();
this.mouthShapesResultLabel = new System.Windows.Forms.Label();
this.mouthNamingLabel = new System.Windows.Forms.Label();
this.mouthNamingResultLabel = new System.Windows.Forms.Label();
this.mainErrorProvider = new System.Windows.Forms.ErrorProvider(this.components);
this.animationFileErrorProvider = new System.Windows.Forms.ErrorProvider(this.components);
this.mainBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.mouthSlotComboBox = new rhubarb_for_spine.BindableComboBox();
this.eventColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.audioFileColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dialogColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.statusColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.actionColumn = new System.Windows.Forms.DataGridViewButtonColumn();
this.tableLayoutPanel1.SuspendLayout();
this.panel1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.animationFileBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.audioEventsDataGridView)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.mainErrorProvider)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.animationFileErrorProvider)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.mainBindingSource)).BeginInit();
this.SuspendLayout();
//
// tableLayoutPanel1
//
this.tableLayoutPanel1.AutoSize = true;
this.tableLayoutPanel1.ColumnCount = 2;
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.Controls.Add(this.filePathLabel, 0, 0);
this.tableLayoutPanel1.Controls.Add(this.panel1, 1, 0);
this.tableLayoutPanel1.Controls.Add(this.mouthSlotLabel, 0, 1);
this.tableLayoutPanel1.Controls.Add(this.mouthSlotComboBox, 1, 1);
this.tableLayoutPanel1.Controls.Add(this.mouthShapesLabel, 0, 3);
this.tableLayoutPanel1.Controls.Add(this.audioEventsLabel, 0, 4);
this.tableLayoutPanel1.Controls.Add(this.audioEventsDataGridView, 1, 4);
this.tableLayoutPanel1.Controls.Add(this.mouthShapesResultLabel, 1, 3);
this.tableLayoutPanel1.Controls.Add(this.mouthNamingLabel, 0, 2);
this.tableLayoutPanel1.Controls.Add(this.mouthNamingResultLabel, 1, 2);
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.Location = new System.Drawing.Point(5, 5);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(3);
this.tableLayoutPanel1.RowCount = 5;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(903, 612);
this.tableLayoutPanel1.TabIndex = 0;
//
// filePathLabel
//
this.filePathLabel.AutoSize = true;
this.filePathLabel.Location = new System.Drawing.Point(6, 3);
this.filePathLabel.Name = "filePathLabel";
this.filePathLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0);
this.filePathLabel.Size = new System.Drawing.Size(81, 19);
this.filePathLabel.TabIndex = 0;
this.filePathLabel.Text = "Spine JSON file";
//
// panel1
//
this.panel1.AutoSize = true;
this.panel1.Controls.Add(this.filePathTextBox);
this.panel1.Controls.Add(this.filePathBrowseButton);
this.panel1.Dock = System.Windows.Forms.DockStyle.Top;
this.panel1.Location = new System.Drawing.Point(93, 6);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(804, 23);
this.panel1.TabIndex = 1;
//
// filePathTextBox
//
this.filePathTextBox.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.mainBindingSource, "FilePath", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
this.mainErrorProvider.SetIconPadding(this.filePathTextBox, -20);
this.filePathTextBox.Location = new System.Drawing.Point(0, 0);
this.filePathTextBox.Name = "filePathTextBox";
this.filePathTextBox.Size = new System.Drawing.Size(769, 20);
this.filePathTextBox.TabIndex = 2;
//
// filePathBrowseButton
//
this.filePathBrowseButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filePathBrowseButton.Location = new System.Drawing.Point(775, 0);
this.filePathBrowseButton.Name = "filePathBrowseButton";
this.filePathBrowseButton.Size = new System.Drawing.Size(30, 20);
this.filePathBrowseButton.TabIndex = 3;
this.filePathBrowseButton.Text = "...";
this.filePathBrowseButton.UseVisualStyleBackColor = true;
this.filePathBrowseButton.Click += new System.EventHandler(this.filePathBrowseButton_Click);
//
// mouthSlotLabel
//
this.mouthSlotLabel.AutoSize = true;
this.mouthSlotLabel.Location = new System.Drawing.Point(6, 32);
this.mouthSlotLabel.Name = "mouthSlotLabel";
this.mouthSlotLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0);
this.mouthSlotLabel.Size = new System.Drawing.Size(56, 19);
this.mouthSlotLabel.TabIndex = 2;
this.mouthSlotLabel.Text = "Mouth slot";
//
// animationFileBindingSource
//
this.animationFileBindingSource.DataMember = "AnimationFileModel";
this.animationFileBindingSource.DataSource = this.mainBindingSource;
//
// mouthShapesLabel
//
this.mouthShapesLabel.AutoSize = true;
this.mouthShapesLabel.Location = new System.Drawing.Point(6, 79);
this.mouthShapesLabel.Name = "mouthShapesLabel";
this.mouthShapesLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0);
this.mouthShapesLabel.Size = new System.Drawing.Size(74, 19);
this.mouthShapesLabel.TabIndex = 4;
this.mouthShapesLabel.Text = "Mouth shapes";
//
// audioEventsLabel
//
this.audioEventsLabel.AutoSize = true;
this.audioEventsLabel.Location = new System.Drawing.Point(6, 98);
this.audioEventsLabel.Name = "audioEventsLabel";
this.audioEventsLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0);
this.audioEventsLabel.Size = new System.Drawing.Size(69, 19);
this.audioEventsLabel.TabIndex = 6;
this.audioEventsLabel.Text = "Audio events";
//
// audioEventsDataGridView
//
this.audioEventsDataGridView.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders;
this.audioEventsDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.audioEventsDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.eventColumn,
this.audioFileColumn,
this.dialogColumn,
this.statusColumn,
this.actionColumn});
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle2.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
this.audioEventsDataGridView.DefaultCellStyle = dataGridViewCellStyle2;
this.audioEventsDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
this.animationFileErrorProvider.SetIconPadding(this.audioEventsDataGridView, -20);
this.audioEventsDataGridView.Location = new System.Drawing.Point(93, 101);
this.audioEventsDataGridView.Name = "audioEventsDataGridView";
this.audioEventsDataGridView.ReadOnly = true;
this.audioEventsDataGridView.RowHeadersVisible = false;
this.audioEventsDataGridView.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.audioEventsDataGridView.Size = new System.Drawing.Size(804, 505);
this.audioEventsDataGridView.TabIndex = 7;
this.audioEventsDataGridView.CellClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.audioEventsDataGridView_CellClick);
this.audioEventsDataGridView.CellPainting += new System.Windows.Forms.DataGridViewCellPaintingEventHandler(this.audioEventsDataGridView_CellPainting);
//
// mouthShapesResultLabel
//
this.mouthShapesResultLabel.AutoSize = true;
this.mouthShapesResultLabel.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.animationFileBindingSource, "MouthShapesDisplayString", true));
this.mouthShapesResultLabel.Location = new System.Drawing.Point(93, 79);
this.mouthShapesResultLabel.Name = "mouthShapesResultLabel";
this.mouthShapesResultLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0);
this.mouthShapesResultLabel.Size = new System.Drawing.Size(16, 19);
this.mouthShapesResultLabel.TabIndex = 8;
this.mouthShapesResultLabel.Text = " ";
//
// mouthNamingLabel
//
this.mouthNamingLabel.AutoSize = true;
this.mouthNamingLabel.Location = new System.Drawing.Point(6, 59);
this.mouthNamingLabel.Name = "mouthNamingLabel";
this.mouthNamingLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0);
this.mouthNamingLabel.Size = new System.Drawing.Size(74, 19);
this.mouthNamingLabel.TabIndex = 9;
this.mouthNamingLabel.Text = "Mouth naming";
//
// mouthNamingResultLabel
//
this.mouthNamingResultLabel.AutoSize = true;
this.mouthNamingResultLabel.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.animationFileBindingSource, "MouthNaming.DisplayString", true));
this.mouthNamingResultLabel.Location = new System.Drawing.Point(93, 59);
this.mouthNamingResultLabel.Name = "mouthNamingResultLabel";
this.mouthNamingResultLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0);
this.mouthNamingResultLabel.Size = new System.Drawing.Size(16, 19);
this.mouthNamingResultLabel.TabIndex = 10;
this.mouthNamingResultLabel.Text = " ";
//
// mainErrorProvider
//
this.mainErrorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.NeverBlink;
this.mainErrorProvider.ContainerControl = this;
this.mainErrorProvider.DataSource = this.mainBindingSource;
//
// animationFileErrorProvider
//
this.animationFileErrorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.NeverBlink;
this.animationFileErrorProvider.ContainerControl = this;
this.animationFileErrorProvider.DataSource = this.animationFileBindingSource;
//
// mainBindingSource
//
this.mainBindingSource.DataSource = typeof(rhubarb_for_spine.MainModel);
//
// mouthSlotComboBox
//
this.mouthSlotComboBox.Dock = System.Windows.Forms.DockStyle.Top;
this.mouthSlotComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.mouthSlotComboBox.Location = new System.Drawing.Point(93, 35);
this.mouthSlotComboBox.Name = "mouthSlotComboBox";
this.mouthSlotComboBox.Size = new System.Drawing.Size(804, 21);
this.mouthSlotComboBox.TabIndex = 3;
//
// eventColumn
//
this.eventColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.eventColumn.DataPropertyName = "Name";
this.eventColumn.HeaderText = "Event";
this.eventColumn.Name = "eventColumn";
this.eventColumn.ReadOnly = true;
//
// audioFileColumn
//
this.audioFileColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.audioFileColumn.DataPropertyName = "DisplayFilePath";
this.audioFileColumn.HeaderText = "Audio file";
this.audioFileColumn.Name = "audioFileColumn";
this.audioFileColumn.ReadOnly = true;
//
// dialogColumn
//
this.dialogColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.dialogColumn.DataPropertyName = "Dialog";
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.dialogColumn.DefaultCellStyle = dataGridViewCellStyle1;
this.dialogColumn.FillWeight = 300F;
this.dialogColumn.HeaderText = "Dialog";
this.dialogColumn.Name = "dialogColumn";
this.dialogColumn.ReadOnly = true;
//
// statusColumn
//
this.statusColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.statusColumn.DataPropertyName = "Status";
this.statusColumn.HeaderText = "Status";
this.statusColumn.Name = "statusColumn";
this.statusColumn.ReadOnly = true;
//
// actionColumn
//
this.actionColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None;
this.actionColumn.DataPropertyName = "ActionLabel";
this.actionColumn.HeaderText = "";
this.actionColumn.Name = "actionColumn";
this.actionColumn.ReadOnly = true;
this.actionColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
this.actionColumn.Text = "";
this.actionColumn.Width = 80;
//
// MainForm
//
this.AllowDrop = true;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(913, 622);
this.Controls.Add(this.tableLayoutPanel1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "MainForm";
this.Padding = new System.Windows.Forms.Padding(5);
this.Text = "Rhubarb Lip Sync for Spine";
this.DragDrop += new System.Windows.Forms.DragEventHandler(this.MainForm_DragDrop);
this.DragEnter += new System.Windows.Forms.DragEventHandler(this.MainForm_DragEnter);
this.tableLayoutPanel1.ResumeLayout(false);
this.tableLayoutPanel1.PerformLayout();
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.animationFileBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.audioEventsDataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.mainErrorProvider)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.animationFileErrorProvider)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.mainBindingSource)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.Label filePathLabel;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.TextBox filePathTextBox;
private System.Windows.Forms.Button filePathBrowseButton;
private System.Windows.Forms.Label mouthSlotLabel;
private BindableComboBox mouthSlotComboBox;
private System.Windows.Forms.Label mouthShapesLabel;
private System.Windows.Forms.Label audioEventsLabel;
private System.Windows.Forms.DataGridView audioEventsDataGridView;
private System.Windows.Forms.BindingSource mainBindingSource;
private System.Windows.Forms.ErrorProvider mainErrorProvider;
private System.Windows.Forms.Label mouthShapesResultLabel;
private System.Windows.Forms.Label mouthNamingLabel;
private System.Windows.Forms.Label mouthNamingResultLabel;
private System.Windows.Forms.BindingSource animationFileBindingSource;
private System.Windows.Forms.ErrorProvider animationFileErrorProvider;
private System.Windows.Forms.DataGridViewTextBoxColumn eventColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn audioFileColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn dialogColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn;
private System.Windows.Forms.DataGridViewButtonColumn actionColumn;
}
}

View File

@ -0,0 +1,107 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace rhubarb_for_spine {
public partial class MainForm : Form {
private MainModel MainModel { get; }
public MainForm(MainModel mainModel) {
InitializeComponent();
MainModel = mainModel;
mainBindingSource.DataSource = mainModel;
}
private void filePathBrowseButton_Click(object sender, EventArgs e) {
var openFileDialog = new OpenFileDialog {
Filter = "JSON files (*.json)|*.json",
InitialDirectory = filePathTextBox.Text
};
if (openFileDialog.ShowDialog() == DialogResult.OK) {
filePathTextBox.Text = openFileDialog.FileName;
}
}
private void MainForm_DragEnter(object sender, DragEventArgs e) {
if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
e.Effect = DragDropEffects.Copy;
}
}
private void MainForm_DragDrop(object sender, DragEventArgs e) {
var files = (string[]) e.Data.GetData(DataFormats.FileDrop);
if (files.Any()) {
filePathTextBox.Text = files.First();
}
}
private void audioEventsDataGridView_CellClick(object sender, DataGridViewCellEventArgs e) {
if (e.ColumnIndex != actionColumn.Index) return;
AudioFileModel audioFileModel = GetAudioFileModel(e.RowIndex);
audioFileModel?.PerformAction();
}
private void audioEventsDataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) {
if (e.ColumnIndex != statusColumn.Index || e.RowIndex == -1) return;
e.PaintBackground(e.CellBounds, false);
AudioFileModel audioFileModel = GetAudioFileModel(e.RowIndex);
if (audioFileModel == null) return;
string text = string.Empty;
StringAlignment horizontalTextAlignment = StringAlignment.Near;
Color backgroundColor = Color.Transparent;
double? progress = null;
switch (audioFileModel.Status) {
case AudioFileStatus.NotAnimated:
break;
case AudioFileStatus.Scheduled:
text = "Waiting";
backgroundColor = SystemColors.Highlight.WithOpacity(0.2);
break;
case AudioFileStatus.Animating:
progress = audioFileModel.AnimationProgress ?? 0.0;
text = $"{(int) (progress * 100)}%";
horizontalTextAlignment = StringAlignment.Center;
break;
case AudioFileStatus.Done:
text = "Done";
break;
default:
throw new ArgumentOutOfRangeException();
}
// Draw background
var bounds = e.CellBounds;
e.Graphics.FillRectangle(new SolidBrush(backgroundColor), bounds);
// Draw progress bar
if (progress.HasValue) {
e.Graphics.FillRectangle(
SystemBrushes.Highlight,
bounds.X, bounds.Y, bounds.Width * (float) progress, bounds.Height);
}
// Draw text
var stringFormat = new StringFormat {
Alignment = horizontalTextAlignment,
LineAlignment = StringAlignment.Center
};
e.Graphics.DrawString(
text,
e.CellStyle.Font, new SolidBrush(e.CellStyle.ForeColor),
bounds, stringFormat);
e.Handled = true;
}
private AudioFileModel GetAudioFileModel(int rowIndex) {
return rowIndex < 0
? null
: MainModel.AnimationFileModel?.AudioFileModels[rowIndex];
}
}
}

View File

@ -0,0 +1,521 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="mainBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="mainErrorProvider.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>209, 17</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>67</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAASUpKEElKSkBJSkpASUpKQElKSkBJSkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAASUpKEElKSoBJSkrPSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKr0lKSmAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
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
Yf//lWH//5Vh//+VYf//lWH//5Vh//+VYf//glvS/5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//+M
Xuj/SUpK/0lKSt9JSkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKv0lKSv+M
Xuj/lWH//5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//90V7D/glvS/5Vh//+VYf//lWH//5Vh//+V
Yf//lWH//5Vh//9hUYP/SUpK/0lKSv9JSkqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ
SkpASUpK/0lKSv9XTmz/kGD0/5Vh//+VYf//lWH//5Vh//+VYf//lWH//3lYu/9hUYP/lWH//5Vh//+V
Yf//lWH//5Vh//+VYf//lWH//2ZTjv9JSkr/SUpK/0lKSv9JSkr/SUpKIAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAABJSkrfSUpK/0lKSv9JSkr/TktV/29Wpf+MXuj/lWH//5Vh//99Wsb/YVGD/0lKSv9h
UYP/jF7o/5Vh//+VYf//lWH//5Vh//+CW9L/XFB3/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKvwAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAElKSmBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/1NNYf9cUHf/XFB3/05LVf9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSq8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKQElKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKn0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkqPAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ
SkoQSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
SkrfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAABJSkpgSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqvSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKnwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKShBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK7wAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSmBJSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSjAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSp9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/2tsbP+Njo7/pKWl/8bGxv/S
0tL/u7u7/42Ojv/Gxsb/pKWl/42Ojv9rbGz/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSo8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAElKSu9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/VFVV/1RVVf/Gxsb/9PT0////////
////////////////////0tLS/6Slpf//////////////////////9PT0/7u7u/93d3f/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSt8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAASUpKMElKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/2BhYf+7u7v/9PT0/42Ojv//
////////////////////////////////////0tLS/6Slpf//////////////////////////////////
////mZmZ/3d3d/9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKj0lKSv9JSkr/SUpK/0lKSv9JSkr/pKWl////////
////0tLS/7u7u///////////////////////////////////////0tLS/6Slpf//////////////////
////////////////////0tLS/7u7u//S0tL/a2xs/0lKSv9JSkr/SUpK/0lKSv9JSkpwAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKz0lKSv9JSkr/SUpK/2tsbP/o
6Oj/////////////////pKWl/+jo6P//////////////////////////////////////0tLS/6Slpf//
/////////////////////////////////////////42Ojv///////////6Slpf9JSkr/SUpK/0lKSv9J
SkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9J
Skr/SUpK/9LS0v//////////////////////goOD///////S0tL/u7u7/6Slpf93d3f/d3d3/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/3d3d/+Njo7/r7Cw/93d3f///////////42Ojv////////////////+v
sLD/SUpK/0lKSv9JSkr/SUpKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ
SkpwSUpK/0lKSv9JSkr/a2xs/////////////////93d3f+ZmZn/YGFh/0lKSv9JSkr/SUpK/0lKSv9J
Skq/SUpKr0lKSoBJSkqASUpKgElKSoBJSkqASUpKgElKSp9JSkq/SUpK/0lKSv9gYWH/mZmZ/4KDg//o
6Oj/////////////////VFVV/0lKSv9JSkr/SUpKcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAABJSkrPSUpK/0lKSv9JSkr/u7u7/8bGxv+Njo7/VFVV/0lKSv9JSkr/SUpKz0lKSo9J
SkpgSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJ
SkqPSUpK30lKSv9gYWH/u7u7////////////mZmZ/0lKSv9JSkr/SUpK3wAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJSkr/SUpK/0lKSv9JSkr/VFVV/0lKSv9JSkr/SUpKr0lKSmBJ
SkogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAElKSlBJSkq/SUpK/2BhYf/Gxsb/3d3d/0lKSv9JSkr/SUpK/0lKSjAA
AAAAAAAAAAAAAAAAAAAAAAAAAElKSt9JSkpwAAAAAElKSp9JSkr/SUpK/0lKSv9JSkr/SUpKv0lKSmBJ
SkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKQElKSr9JSkr/d3d3/1RVVf9J
Skr/SUpK/0lKSp8AAAAAAAAAAAAAAAAAAAAAAAAAAElKSjBJSkrPSUpK30lKSv9JSkr/SUpK/0lKSp9J
SkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ
SkpQSUpK30lKSv9JSkr/SUpK/0lKSu9JSkoQAAAAAAAAAAAAAAAAAAAAAAAAAABJSkoQSUpKgElKSu9J
Skr/SUpK30lKSp9JSkqASUpKnwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAASUpKEElKSq9JSkr/SUpK/0lKSv9JSkpgAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAElKShBJSkpASUpKgElKSnBJSkpASUpKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpgSUpK70lKSv9JSkrfAAAAAAAAAABJ
SkpwSUpKrwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSiBJSkpASUpKcElKSu9J
Skr/SUpKr0lKSu9JSkrfSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSiBJ
SkqASUpKv0lKSr9JSkq/SUpKr0lKSmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////xVa////////FVr///////8VWv//////
/xVa///wP///FVr//4AP//8VWv//AgP//xVa//w/4P//FVr/+Ph4f/8VWv/xgAw//xVa//MAAj//FVr/
9gABn/8VWv/8AACf/xVa//gAAH//FVr/8AAAP/8VWv/gAAAf/xVa/+AAAB//FVr/wAAAD/8VWv/AAAAP
/xVa/4AAAAf/FVr/gAAAB/8VWv8AAAAD/xVa/wAAAAP/FVr+AAAAA/8VWv4AAAAB/xVa/gAAAAH/FVr8
AAAAAf8VWvwAAAAA/xVa/AAAAAD/FVr8AAAAAP8VWvgAAAAAfxVa+AAAAAB/FVr4AAAAAH8VWvAAAAAA
PxVa8AAAAAA/FVrwAD/8AD8VWuAD//+AHxVaIB///+AfFVoA////+A8VWoB////8DxVa4H////8MFVr/
/////gAVWv/////+AxVa////////FVr///////8VWv///////xVa////////FVr///////8VWigAAAAg
AAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpgSUpKr0lKSr9J
Skr/SUpKv0lKSo9JSkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpASUpK30lKSp9J
SkpgSUpKQElKSkBJSkpASUpKj0lKSt9JSkq/SUpKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKYElKSs9J
SkogAAAAAAAAAABJSkowSUpKQElKSiAAAAAAAAAAAElKSoBJSkrvSUpKQAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSlBJ
SkqfAAAAAElKSkBJSkqvSUpK/0lKSv9JSkr/SUpK/0lKSu9JSkqPSUpKEElKSjBJSkrfSUpKMAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAASUpKgAAAAABJSkqASUpK/1xQd/99Wsb/kGD0/5Vh//+MXuj/eVi7/1NNYf9JSkrvSUpKUElKSjBJ
SkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAASUpKn05LVf99Wsb/lWH//5Vh//+VYf//lWH//5Vh//+VYf//lWH//3RXsP9J
Skr/SUpKYElKSjBJSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAElKSnBOS1X/h13d/5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//+V
Yf//lWH//3lYu/9JSkrvSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogTktV/4dd3f+VYf//lWH//5Vh//+VYf//lWH//31axv+V
Yf//lWH//5Vh//+VYf//lWH//1NNYf9JSkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9OS1X/fVrG/5Vh//+VYf//lWH//5Vh//95
WLv/dFew/5Vh//+VYf//lWH//5Vh//9mU47/SUpK/0lKSv9JSkpwAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpASUpK/0lKSv9JSkr/YVGD/3lYu/90
V7D/XFB3/0lKSv9mU47/glvS/4Jb0v95WLv/XFB3/0lKSv9JSkr/SUpK/0lKSu9JSkoQAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSoAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSo9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAASUpK30lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkqvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAElKSjBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAASUpKj0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv93
d3f/VFVV/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSlAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkrPSUpK/0lKSv9JSkr/SUpK/0lKSv9UVVX/r7Cw/9LS0v//
//////////////+kpaX//////+jo6P+7u7v/jY6O/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKnwAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKIElKSv9JSkr/SUpK/0lKSv+Njo7/6Ojo/5mZmf//
/////////////////////////6Slpf//////////////////////xsbG/4KDg/9JSkr/SUpK/0lKSv9J
SkrvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpwSUpK/0lKSv9rbGz/3d3d///////S
0tL/xsbG///////////////////////S0tL/pKWl////////////////////////////pKWl/+jo6P9r
bGz/SUpK/0lKSv9JSkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSq9JSkr/SUpK/9LS0v//
/////////5mZmf+kpaX/jY6O/3d3d/9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv93d3f/mZmZ/93d3f+Z
mZn///////////9rbGz/SUpK/0lKSp8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9r
bGz/xsbG/42Ojv9UVVX/SUpK70lKSq9JSkqASUpKQElKSiAAAAAAAAAAAAAAAAAAAAAASUpKEElKSkBJ
SkqPSUpK30lKSv+vsLD/9PT0/6+wsP9JSkr/SUpK7wAAAAAAAAAAAAAAAAAAAABJSkpgAAAAAElKSoBJ
Skr/SUpK/0lKSv9JSkrfSUpKj0lKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAASUpKQElKSr9UVVX/pKWl/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAElKSmBJ
SkrfSUpK30lKSv9JSkq/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJSkrPSUpK/0lKSv9JSkqvAAAAAAAAAAAA
AAAAAAAAAElKSjBJSkqfSUpK30lKSs9JSkqASUpKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqASUpK/0lKSv9J
SkogAAAAAElKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSjBJ
SkqASUpK/0lKSr9JSkrfSUpKjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAASUpKIElKSnBJSkqASUpKcElKSiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////
//////Af///AB///jGP//yAB//9AAf//gAD//wAB//4AAf/+AAD//AAAf/wAAH/4AAB/+AAAP/gAAD/w
AAA/8AAAH/AAAB/gAAAf4AAAD+AAAA/AA8APQH/8BwP//weB///C////wP///8H///////////////8o
AAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ
SkqzSUpK/0lKSv9JSkr/SUpKswAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ
SkqzAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAElKSv9JSkr/SUpK/0lKSv9JSkr/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAElKSv+AU6b/lWH//5Vh//+VYf//gFOm/0lKSv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAElKSrOAU6b/lWH//5Vh//9/Uqb/lWH//5Vh//+AU6b/SUpKswAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAABJSkr/SUpK/4BTpv+AU6b/gFOm/4BTpv+AU6b/SUpK/0lKSv8AAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAABJSkqzSUpK/1lZWf9cXV3/WFlZ/1JTU/9cXV3/WFlZ/1tcXP9JSkr/SUpKswAAAAAA
AAAAAAAAAAAAAAAAAAAASUpK/0lKSv9mZ2f/amtr/29wcP90dXX/aGlp/21ubv9vcHD/SktL/0lKSv8A
AAAAAAAAAAAAAAAAAAAASUpKs0lKSv9JSkr/WFhY/1hYWP9KS0v/SktL/0pLS/9KS0v/SktL/1NUVP9J
Skr/AAAAAAAAAAAAAAAAAAAAAElKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J
Skr/SUpK/0lKSrMAAAAAAAAAAAAAAABJSkr/SUpK/0lKSv+jpKT///////////+jpKT///////////+j
pKT/SUpK/0lKSv9JSkr/AAAAAAAAAABJSkqzSUpK/0lKSv//////o6Sk////////////o6Sk////////
////o6Sk//////9JSkr/SUpK/wAAAABJSkqzSUpK/0lKSv//////o6Sk/6OkpP9JSkqzSUpK/0lKSv9J
Skr/SUpK/0lKSrOjpKT//////0lKSv9JSkqzSUpK/0lKSrNJSkr/o6Sk/0lKSv9JSkqzAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAASUpKs0lKSv+jpKT/SUpK/0lKSrMAAAAASUpK/0lKSv9JSkqzAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzSUpK/0lKSv8AAAAAAAAAAAAAAABJSkqzSUpK/0lKSrMA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzSUpK/0lKSrMAAAAAAAAAAPg/rEH336xB+D+sQfAfrEHg
D6xB4A+sQcAHrEHAB6xBgAesQYADrEGAA6xBAAKsQQAArEEH4KxBj/GsQcfjrEE=
</value>
</data>
<metadata name="animationFileBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>359, 17</value>
</metadata>
<metadata name="eventColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="audioFileColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="dialogColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="statusColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="actionColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="animationFileErrorProvider.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>563, 17</value>
</metadata>
</root>

View File

@ -0,0 +1,50 @@
using System;
using System.IO;
using System.Threading;
namespace rhubarb_for_spine {
public class MainModel : ModelBase {
// For the time being, we're allowing only one file to be animated at a time.
// Rhubarb tries to use all processor cores, so there isn't much to be gained by allowing
// multiple parallel jobs. On the other hand, too many parallel Rhubarb instances may
// seriously slow down the system.
readonly SemaphoreSlim semaphore = new SemaphoreSlim(1);
private string _filePath;
private AnimationFileModel _animationFileModel;
public MainModel(string filePath = null) {
FilePath = filePath;
}
public string FilePath {
get { return _filePath; }
set {
_filePath = value;
AnimationFileModel = null;
if (string.IsNullOrEmpty(_filePath)) {
SetError(nameof(FilePath), "No input file specified.");
} else if (!File.Exists(_filePath)) {
SetError(nameof(FilePath), "File does not exist.");
} else {
try {
AnimationFileModel = new AnimationFileModel(_filePath, semaphore);
SetError(nameof(FilePath), null);
} catch (Exception e) {
SetError(nameof(FilePath), e.Message);
}
}
OnPropertyChanged(nameof(FilePath));
}
}
public AnimationFileModel AnimationFileModel {
get { return _animationFileModel; }
set {
_animationFileModel = value;
OnPropertyChanged(nameof(AnimationFileModel));
}
}
}
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.ComponentModel;
namespace rhubarb_for_spine {
public class ModelBase : INotifyPropertyChanged, IDataErrorInfo {
private readonly Dictionary<string, string> errors = new Dictionary<string, string>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetError(string propertyName, string error) {
errors[propertyName] = error;
}
string IDataErrorInfo.this[string propertyName] =>
errors.ContainsKey(propertyName) ? errors[propertyName] : null;
string IDataErrorInfo.Error => null;
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace rhubarb_for_spine {
public class MouthCue {
public MouthCue(TimeSpan time, MouthShape mouthShape) {
Time = time;
MouthShape = mouthShape;
}
public TimeSpan Time { get; }
public MouthShape MouthShape { get; }
public override string ToString() {
return $"{Time}: {MouthShape}";
}
}
}

View File

@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Linq;
namespace rhubarb_for_spine {
public class MouthNaming {
public MouthNaming(string prefix, string suffix, MouthShapeCasing mouthShapeCasing) {
Prefix = prefix;
Suffix = suffix;
MouthShapeCasing = mouthShapeCasing;
}
public string Prefix { get; }
public string Suffix { get; }
public MouthShapeCasing MouthShapeCasing { get; }
public static MouthNaming Guess(IReadOnlyCollection<string> mouthNames) {
string firstMouthName = mouthNames.First();
if (mouthNames.Count == 1) {
return firstMouthName == string.Empty
? new MouthNaming(string.Empty, string.Empty, MouthShapeCasing.Lower)
: new MouthNaming(
firstMouthName.Substring(0, firstMouthName.Length - 1),
string.Empty,
GuessMouthShapeCasing(firstMouthName.Last()));
}
string commonPrefix = mouthNames.GetCommonPrefix();
string commonSuffix = mouthNames.GetCommonSuffix();
var mouthShapeCasing = firstMouthName.Length > commonPrefix.Length
? GuessMouthShapeCasing(firstMouthName[commonPrefix.Length])
: MouthShapeCasing.Lower;
return new MouthNaming(commonPrefix, commonSuffix, mouthShapeCasing);
}
public string DisplayString {
get {
string casing = MouthShapeCasing == MouthShapeCasing.Upper
? "<UPPER-CASE SHAPE NAME>"
: "<lower-case shape name>";
return $"\"{Prefix}{casing}{Suffix}\"";
}
}
public string GetName(MouthShape mouthShape) {
string name = MouthShapeCasing == MouthShapeCasing.Upper
? mouthShape.ToString()
: mouthShape.ToString().ToLowerInvariant();
return $"{Prefix}{name}{Suffix}";
}
private static MouthShapeCasing GuessMouthShapeCasing(char mouthShape) {
return char.IsUpper(mouthShape) ? MouthShapeCasing.Upper : MouthShapeCasing.Lower;
}
}
public enum MouthShapeCasing {
Upper,
Lower
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace rhubarb_for_spine {
public enum MouthShape {
A, B, C, D, E, F, G, H, X
}
public static class MouthShapes {
public static IReadOnlyCollection<MouthShape> All =>
Enum.GetValues(typeof(MouthShape))
.Cast<MouthShape>()
.ToList();
public const int BasicShapesCount = 6;
public static bool IsBasic(MouthShape mouthShape) =>
(int) mouthShape < BasicShapesCount;
public static IReadOnlyCollection<MouthShape> Basic =>
All.Take(BasicShapesCount).ToList();
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Nito.AsyncEx;
namespace rhubarb_for_spine {
public static class ProcessTools {
public static async Task<int> RunProcessAsync(
string processFilePath,
string processArgs,
Action<string> receiveStdout,
Action<string> receiveStderr,
CancellationToken cancellationToken
) {
var startInfo = new ProcessStartInfo {
FileName = processFilePath,
Arguments = processArgs,
UseShellExecute = false, // Necessary to redirect streams
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
using (var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }) {
// Process all events in the original call's context
var pendingActions = new AsyncProducerConsumerQueue<Action>();
int? exitCode = null;
// ReSharper disable once AccessToDisposedClosure
process.Exited += (sender, args) =>
pendingActions.Enqueue(() => exitCode = process.ExitCode);
process.OutputDataReceived += (sender, args) => {
if (args.Data != null) {
pendingActions.Enqueue(() => receiveStdout(args.Data));
}
};
process.ErrorDataReceived += (sender, args) => {
if (args.Data != null) {
pendingActions.Enqueue(() => receiveStderr(args.Data));
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
cancellationToken.Register(() => pendingActions.Enqueue(() => {
try {
// ReSharper disable once AccessToDisposedClosure
process.Kill();
} catch (Exception e) {
Debug.WriteLine($"Error terminating process: {e}");
}
// ReSharper disable once AccessToDisposedClosure
process.WaitForExit();
throw new OperationCanceledException();
}));
while (exitCode == null) {
Action action = await pendingActions.DequeueAsync(cancellationToken);
action();
}
return exitCode.Value;
}
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace rhubarb_for_spine {
static class Program {
[STAThread]
static void Main(string[] args) {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
string filePath = args.FirstOrDefault();
MainModel mainModel = new MainModel(filePath);
Application.Run(new MainForm(mainModel));
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("rhubarb-for-spine")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("rhubarb-for-spine")]
[assembly: AssemblyCopyright("Copyright © 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c5ed6f8a-6141-4bae-be24-77dee23e495f")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="MainModel" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>rhubarb_for_spine.MainModel, rhubarb-for-spine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace rhubarb_for_spine.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace rhubarb_for_spine {
public static class RhubarbCli {
public static async Task<IReadOnlyCollection<MouthCue>> AnimateAsync(
string audioFilePath, string dialog, ISet<MouthShape> extendedMouthShapes,
IProgress<double> progress, CancellationToken cancellationToken
) {
if (cancellationToken.IsCancellationRequested) {
throw new OperationCanceledException();
}
if (!File.Exists(audioFilePath)) {
throw new ArgumentException($"File '{audioFilePath}' does not exist.");
}
using (var dialogFile = dialog != null ? new TemporaryTextFile(dialog) : null) {
string rhubarbExePath = GetRhubarbExePath();
string args = CreateArgs(audioFilePath, extendedMouthShapes, dialogFile?.FilePath);
bool success = false;
string errorMessage = null;
string resultString = "";
await ProcessTools.RunProcessAsync(
rhubarbExePath, args,
outString => resultString += outString,
errString => {
dynamic json = JObject.Parse(errString);
switch ((string) json.type) {
case "progress":
progress.Report((double) json.value);
break;
case "success":
success = true;
break;
case "failure":
errorMessage = json.reason;
break;
}
},
cancellationToken);
if (errorMessage != null) {
throw new ApplicationException(errorMessage);
}
if (success) {
progress.Report(1.0);
return ParseRhubarbResult(resultString);
}
throw new ApplicationException("Rhubarb did not return a result.");
}
}
private static string CreateArgs(
string audioFilePath, ISet<MouthShape> extendedMouthShapes, string dialogFilePath
) {
string extendedShapesString =
string.Join("", extendedMouthShapes.Select(shape => shape.ToString()));
string args = "--machineReadable"
+ " --exportFormat json"
+ $" --extendedShapes \"{extendedShapesString}\""
+ $" \"{audioFilePath}\"";
if (dialogFilePath != null) {
args = $"--dialogFile \"{dialogFilePath}\" " + args;
}
return args;
}
private static IReadOnlyCollection<MouthCue> ParseRhubarbResult(string jsonString) {
dynamic json = JObject.Parse(jsonString);
JArray mouthCues = json.mouthCues;
return mouthCues
.Cast<dynamic>()
.Select(mouthCue => {
TimeSpan time = TimeSpan.FromSeconds((double) mouthCue.start);
MouthShape mouthShape = (MouthShape) Enum.Parse(typeof(MouthShape), (string) mouthCue.value);
return new MouthCue(time, mouthShape);
})
.ToList();
}
private static string GetRhubarbExePath() {
bool onUnix = Environment.OSVersion.Platform == PlatformID.Unix
|| Environment.OSVersion.Platform == PlatformID.MacOSX;
string exeName = "rhubarb" + (onUnix ? "" : ".exe");
string guiExeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string currentDirectory = guiExeDirectory;
while (currentDirectory != Path.GetPathRoot(guiExeDirectory)) {
string candidate = Path.Combine(currentDirectory, exeName);
if (File.Exists(candidate)) {
return candidate;
}
currentDirectory = Path.GetDirectoryName(currentDirectory);
}
throw new ApplicationException(
$"Could not find Rhubarb Lip Sync executable '{exeName}'."
+ $" Expected to find it in '{guiExeDirectory}' or any directory above.");
}
private class TemporaryTextFile : IDisposable {
public string FilePath { get; }
public TemporaryTextFile(string text) {
FilePath = Path.GetTempFileName();
File.WriteAllText(FilePath, text);
}
public void Dispose() {
File.Delete(FilePath);
}
}
}
}

View File

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace rhubarb_for_spine {
public static class SpineJson {
public static JObject ReadJson(string filePath) {
string jsonString = File.ReadAllText(filePath);
try {
return JObject.Parse(jsonString);
} catch (Exception) {
throw new ApplicationException("Wrong file format. This is not a valid JSON file.");
}
}
public static void ValidateJson(JObject json, string animationFileDirectory) {
// This method doesn't validate the entire JSON.
// It merely checks that there are no obvious problems.
dynamic skeleton = ((dynamic) json).skeleton;
if (skeleton == null) {
throw new ApplicationException("JSON file is corrupted.");
}
if (skeleton.images == null) {
throw new ApplicationException(
"JSON file is incomplete. Make sure you checked 'Nonessential data' when exporting.");
}
if (((dynamic) json).skins["default"] == null) {
throw new ApplicationException("JSON file has no default skin.");
}
GetImagesDirectory(json, animationFileDirectory);
GetAudioDirectory(json, animationFileDirectory);
}
public static string GetImagesDirectory(JObject json, string animationFileDirectory) {
string result = Path.GetFullPath(Path.Combine(animationFileDirectory, (string) ((dynamic) json).skeleton.images));
if (!Directory.Exists(result)) {
throw new ApplicationException(
"Could not find images directory relative to the JSON file."
+ " Make sure the JSON file is in the same directory as the original Spine file.");
}
return result;
}
public static string GetAudioDirectory(JObject json, string animationFileDirectory) {
string result = Path.GetFullPath(Path.Combine(animationFileDirectory, (string) ((dynamic) json).skeleton.audio));
if (!Directory.Exists(result)) {
throw new ApplicationException(
"Could not find audio directory relative to the JSON file."
+ " Make sure the JSON file is in the same directory as the original Spine file.");
}
return result;
}
public static double GetFrameRate(JObject json) {
return (double?) ((dynamic) json).skeleton.fps ?? 30.0;
}
public static IReadOnlyCollection<string> GetSlots(JObject json) {
return ((JArray) ((dynamic) json).slots)
.Cast<dynamic>()
.Select(slot => (string) slot.name)
.ToList();
}
public static string GuessMouthSlot(IReadOnlyCollection<string> slots) {
return slots.FirstOrDefault(slot => slot.Contains("mouth", StringComparison.InvariantCultureIgnoreCase))
?? slots.FirstOrDefault();
}
public class AudioEvent {
public AudioEvent(string name, string relativeAudioFilePath, string dialog) {
Name = name;
RelativeAudioFilePath = relativeAudioFilePath;
Dialog = dialog;
}
public string Name { get; }
public string RelativeAudioFilePath { get; }
public string Dialog { get; }
}
public static IReadOnlyCollection<AudioEvent> GetAudioEvents(JObject json) {
return ((IEnumerable<KeyValuePair<string, JToken>>) ((dynamic) json).events)
.Select(pair => {
string name = pair.Key;
dynamic value = pair.Value;
string relativeAudioFilePath = value.audio;
string dialog = value["string"];
return new AudioEvent(name, relativeAudioFilePath, dialog);
})
.Where(audioEvent => audioEvent.RelativeAudioFilePath != null)
.ToList();
}
public static IReadOnlyCollection<string> GetSlotAttachmentNames(JObject json, string slotName) {
return ((JObject) ((dynamic) json).skins["default"][slotName])
.Properties()
.Select(property => property.Name)
.ToList();
}
public static bool HasAnimation(JObject json, string animationName) {
JObject animations = ((dynamic) json).animations;
if (animations == null) return false;
return animations.Properties().Any(property => property.Name == animationName);
}
public static void CreateOrUpdateAnimation(
JObject json, IReadOnlyCollection<MouthCue> animationResult,
string eventName, string animationName, string mouthSlot, MouthNaming mouthNaming
) {
dynamic dynamicJson = json;
dynamic animations = dynamicJson.animations;
if (animations == null) {
animations = dynamicJson.animations = new JObject();
}
// Round times to full frame. Always round down.
// If events coincide, prefer the later one.
double frameRate = GetFrameRate(json);
var keyframes = new Dictionary<int, MouthShape>();
foreach (MouthCue mouthCue in animationResult) {
int frameNumber = (int) (mouthCue.Time.TotalSeconds * frameRate);
keyframes[frameNumber] = mouthCue.MouthShape;
}
animations[animationName] = new JObject {
["slots"] = new JObject {
[mouthSlot] = new JObject {
["attachment"] = new JArray(
keyframes
.OrderBy(pair => pair.Key)
.Select(pair => new JObject {
["time"] = pair.Key / frameRate,
["name"] = mouthNaming.GetName(pair.Value)
})
.Cast<object>()
.ToArray()
)
}
},
["events"] = new JArray {
new JObject {
["time"] = 0.0,
["name"] = eventName,
["string"] = ""
}
}
};
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
namespace rhubarb_for_spine {
public static class Tools {
public static string GetCommonPrefix(this IReadOnlyCollection<string> strings) {
return strings.Any()
? strings.First().Substring(0, GetCommonPrefixLength(strings))
: string.Empty;
}
public static int GetCommonPrefixLength(this IReadOnlyCollection<string> strings) {
if (!strings.Any()) return 0;
string first = strings.First();
int result = first.Length;
foreach (string s in strings) {
for (int i = 0; i < Math.Min(result, s.Length); i++) {
if (s[i] != first[i]) {
result = i;
break;
}
}
}
return result;
}
public static string GetCommonSuffix(this IReadOnlyCollection<string> strings) {
if (!strings.Any()) return string.Empty;
int commonSuffixLength = GetCommonSuffixLength(strings);
string first = strings.First();
return first.Substring(first.Length - commonSuffixLength);
}
public static int GetCommonSuffixLength(this IReadOnlyCollection<string> strings) {
if (!strings.Any()) return 0;
string first = strings.First();
int result = first.Length;
foreach (string s in strings) {
for (int i = 0; i < Math.Min(result, s.Length); i++) {
if (s[s.Length - 1 - i] != first[first.Length - 1 - i]) {
result = i;
break;
}
}
}
return result;
}
public static bool Contains(this string s, string value, StringComparison stringComparison) {
return s.IndexOf(value, stringComparison) >= 0;
}
public static string Join(this IEnumerable<string> values, string separator) {
return string.Join(separator, values);
}
public static Color WithOpacity(this Color color, double opacity) {
return Color.FromArgb((int) (opacity * 255), color);
}
}
}

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/></startup></configuration>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Bcl" version="1.1.10" targetFramework="net46" />
<package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net46" />
<package id="Microsoft.Bcl.Build" version="1.0.21" targetFramework="net46" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" />
<package id="Nito.AsyncEx" version="4.0.1" targetFramework="net46" />
</packages>

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>rhubarb_for_spine</RootNamespace>
<AssemblyName>rhubarb-for-spine</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>rhubarb.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nito.AsyncEx, Version=4.0.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nito.AsyncEx.Concurrent, Version=4.0.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nito.AsyncEx.Enlightenment, Version=4.0.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="AnimationFileModel.cs" />
<Compile Include="AudioFileModel.cs" />
<Compile Include="BindableComboBox.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="MainForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</Compile>
<Compile Include="MainModel.cs" />
<Compile Include="ModelBase.cs" />
<Compile Include="MouthCue.cs" />
<Compile Include="MouthNaming.cs" />
<Compile Include="MouthShape.cs" />
<Compile Include="ProcessTools.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RhubarbCli.cs" />
<Compile Include="SpineJson.cs" />
<Compile Include="Tools.cs" />
<EmbeddedResource Include="MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
<None Include="app.config" />
<None Include="packages.config" />
<None Include="Properties\DataSources\MainModel.datasource" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Include="rhubarb.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets" Condition="Exists('..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB