forum

lyrics! - C# Based Storyboard Lyrics Generator

posted
Total Posts
1
Topic Starter
tjysunset
Wrote a storyboard generator utilizing existing lrc files (see specs at https://en.wikipedia.org/wiki/LRC_(file_format)), which style inspired by https://osu.ppy.sh/beatmapsets/310499. Thought it would be useful to somebody since this might prevent reinventing of the wheel.

I'm not going to post compiled binaries here because the code is literally a mess - but I hope it's self-explanatory in some way. For compiling, create a WPF project then reference System.Windows.Forms (for its dang system dialog) and MoreLINQ (https://www.nuget.org/packages/morelinq/).

Licensed under WTFPL (http://www.wtfpl.net/, or, public domain), and any kind of credit & attribution will NOT be appreciated. No customer support provided, neither free nor paid. :-P

MainWindow.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using MoreLinq;

namespace lyrics_
{
public partial class MainWindow
{
public static readonly Regex Lrc =
new Regex(@"\[(?<Timestamp>[0-9]{2,}:[0-9]{2}\.[0-9]{2})\](?<Content>.+?)(\n|\r|$)");

public static readonly Regex BeatmapFilename =
new Regex(@"^(?<Name>.+? \(.+?\)) \[.+?\]\.osu$");

public static readonly Random Random = new Random(42);

public const string Whitespace = "  \t\r\n";

public const string Subdirectory = @"lyrics!";

public const double Spacing = 6; // in pixels

public const ulong FadeInLength = 200; // in milliseconds

public const int OriginX = 280; // in pixels

public const int OriginY = 320; // in pixels

public const int XRange = 8; // in pixels

public const int YRange = 12; // in pixels

public const double RotationRange = .4; // in radians

#region DP

public static readonly DependencyProperty WorkingDirectoryProperty = DependencyProperty.Register(
"WorkingDirectory", typeof(string), typeof(MainWindow), new PropertyMetadata(default(string)));

public string WorkingDirectory
{
get => (string) GetValue(WorkingDirectoryProperty);
set => SetValue(WorkingDirectoryProperty, value);
}

public static readonly DependencyProperty FontProperty = DependencyProperty.Register(
"Font", typeof(string), typeof(MainWindow), new PropertyMetadata(default(string)));

public string Font
{
get => (string) GetValue(FontProperty);
set => SetValue(FontProperty, value);
}

public static readonly DependencyProperty LyricsProperty = DependencyProperty.Register(
"Lyrics", typeof(string), typeof(MainWindow), new PropertyMetadata(default(string)));

public string Lyrics
{
get => (string) GetValue(LyricsProperty);
set => SetValue(LyricsProperty, value);
}

#endregion

public MainWindow()
{
InitializeComponent();
}

private void Browse(object sender, RoutedEventArgs e)
{
using (var dialog =
new System.Windows.Forms.FolderBrowserDialog {Description = @"Select your beatmap directory..."})
{
WorkingDirectory = dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK
? dialog.SelectedPath
: WorkingDirectory;
}
}

private void Generate(object sender, RoutedEventArgs e)
{
var lyrics = Lrc
.Matches(Lyrics)
.Cast<Match>()
.Select(x =>
((ulong) DateTime.ParseExact(x.Groups["Timestamp"].Value, @"mm:ss.ff",
CultureInfo.InvariantCulture)
.TimeOfDay.TotalMilliseconds, x.Groups["Content"].Value))
.ToArray();

try
{
Directory.Delete(Path.Combine(WorkingDirectory, Subdirectory));
}
catch
{
// ignored
}
finally
{
Directory.CreateDirectory(Path.Combine(WorkingDirectory, Subdirectory));
}

// generate character sprites
var alphabet = lyrics
.SelectMany(x => x.Item2)
.Distinct()
.Select(x =>
{
var render = new TextBlock
{
Text = x.ToString(),
FontFamily = new FontFamily(Font),
FontSize = 36
};

render.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
render.Arrange(new Rect(render.DesiredSize));

if (Whitespace.Contains(x)) return new KeyValuePair<char, double>(x, render.RenderSize.Width);

var bitmap = new RenderTargetBitmap(
(int) Math.Round(render.RenderSize.Width), (int) Math.Round(render.RenderSize.Height),
96, 96, PixelFormats.Pbgra32);
bitmap.Render(render);

var encoder = new PngBitmapEncoder();
var outputFrame = BitmapFrame.Create(bitmap);
encoder.Frames.Add(outputFrame);

using (var file =
File.OpenWrite(Path.Combine(WorkingDirectory, Subdirectory, $@"{(uint) x:X}.png")))
{
encoder.Save(file);
}

return new KeyValuePair<char, double>(x, render.RenderSize.Width / 2);
})
.ToDictionary();

var storyboard = "[Events]\n" +
lyrics.Pairwise((x, successor) =>
x.Item2.Zip(
x.Item2.Select(y => alphabet[y]).PreScan((y, z) => y + z + Spacing, 0)
.Select(y =>
(y, Random.Next(-XRange, XRange), Random.Next(-YRange, YRange),
Random.NextDouble() * RotationRange * 2 - RotationRange)),
(character, transform) =>
!Whitespace.Contains(character)
? $"Sprite,Foreground,Centre,\"{Subdirectory}/{(uint) character:X}.png\",{OriginX + transform.Item1},{OriginY}\n" +
$" S,0,{x.Item1 - FadeInLength},,0.5\n" +
$" F,0,{x.Item1 - FadeInLength},{x.Item1},0,1\n" +
$" F,2,{x.Item1},{successor.Item1},1,0\n" +
$" M,0,{x.Item1 - FadeInLength},{successor.Item1},{OriginX + transform.Item1 - transform.Item2},{OriginY - transform.Item3},{OriginX + transform.Item1 + transform.Item2},{OriginY + transform.Item3}\n" +
$" R,0,{x.Item1 - FadeInLength},{successor.Item1},{-transform.Item4},{transform.Item4}"
: "").ToDelimitedString("\n")
)
.ToDelimitedString("\n") + "\n//Storyboard Sound Samples\n";
File.WriteAllText(
Path.Combine(WorkingDirectory,
$@"{
BeatmapFilename
.Match(Path.GetFileName(Directory.GetFiles(WorkingDirectory)
.First(x => BeatmapFilename.IsMatch(Path.GetFileName(x))))).Groups["Name"].Value
}.osb"), storyboard);
MessageBox.Show("Done!", "Done");
}
}
}

MainWindow.xaml

<Window x:Name="Window" x:Class="lyrics_.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:lyrics_"
mc:Ignorable="d"
Title="lyrics!" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Beatmap directory" TextAlignment="Right" />
<TextBox Text="{Binding ElementName=Window, Path=WorkingDirectory}" Grid.Column="1" />
<StackPanel Orientation="Horizontal" Grid.Column="2" Grid.RowSpan="1">
<Button Content="_Browse..." Click="Browse" />
<Button Content="_Generate! (BACKUP FIRST!)" Click="Generate" />
</StackPanel>
<TextBlock Grid.Row="1" Text="Font" TextAlignment="Right" />
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding ElementName=Window, Path=Font}" />
<TextBlock Grid.Row="2" Text="Lrc" TextAlignment="Right" />
<TextBox Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="1" Text="{Binding ElementName=Window, Path=Lyrics}"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Auto" Margin="0,17.4,-0.6,-0.2"
Grid.RowSpan="2" />
</Grid>
</Window>
Please sign in to reply.

New reply