示例#1
0
        private static void Main(string[] args)
        {
            Parser.Default.ParseArguments <Options>(args)
            .WithParsed(opts => {
                if (!File.Exists(opts.AppConfig))
                {
                    Console.Error.WriteLine($"Configuration file not found: '{opts.AppConfig}'");
                    Environment.Exit(1);
                }

                var appConfig = JsonConvert.DeserializeObject <AppConfiguration>(File.ReadAllText(opts.AppConfig));
                var animator  = new Animator(appConfig);
                var outputDir = Directory.CreateDirectory(opts.OutputDir);

                if (File.Exists(opts.SongConfigFile))
                {
                    var songConfig = SongConfiguration.LoadFromFile(opts.SongConfigFile);
                    Console.WriteLine($"Processing single song config: '{opts.SongConfigFile}'");
                    ProcessSong(ProgressReporterFactory(songConfig.OutputFilename), animator, songConfig, outputDir);
                    Console.WriteLine("Finished.");
                    Environment.Exit(0);
                }
                else
                {
                    if (!Directory.Exists(opts.SongConfigDir))
                    {
                        Console.Error.WriteLine($"Song config directory '{opts.SongConfigDir}' does not exist.");
                        Environment.Exit(1);
                    }

                    var configFiles = Directory.GetFiles(opts.SongConfigDir, "*.txt");

                    if (!configFiles.Any())
                    {
                        Console.Error.WriteLine($"No .txt lyric files found in directory '{opts.SongConfigDir}'");
                        Environment.Exit(1);
                    }

                    Console.WriteLine($"Processing {configFiles.Count()} configuration files from '{opts.SongConfigDir}'...");

                    foreach (var configFile in configFiles)
                    {
                        var songConfig = SongConfiguration.LoadFromFile(configFile);
                        ProcessSong(ProgressReporterFactory(songConfig.OutputFilename), animator, songConfig, outputDir);
                        Console.Write("\n");
                    }
                }
            });
示例#2
0
        public void Animate(Action <float> reportProgress, SongConfiguration config, string ffmpegExePath, DirectoryInfo outputDirectory, string pngOutputPath = null)
        {
            var desiredReadingY = height * 3 / 4;

            using (var titleTypeface = SKTypeface.FromFamilyName(appConfig.TitleFont.Family, SKFontStyleWeight.Light, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright))
                using (var lyricTypeface = SKTypeface.FromFamilyName(appConfig.LyricsFont.Family, SKFontStyleWeight.Light, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright))
                    using (var verseTypeface = SKTypeface.FromFamilyName(appConfig.VerseFont.Family, SKFontStyleWeight.Light, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright))
                    {
                        var timingRegex      = new Regex(@"^(?:\[(\d{1,2}:\d{1,2}:\d{1,2})\])?\s*(?:\{([^}]+)\})?\s*(.*)$");
                        var unprocessedLines = File.ReadAllLines(config.LyricsFilePath);

                        var speedChangeEasingFrames = appConfig.FramesPerSecond * 4;

                        var processedLines = new List <(TimeSpan arrivalTime, TimeSpan nextArrivalTime, IEnumerable <string> lines)>();
                        var verseLabels    = new List <VerseLabel>();

                        var      segmentLines   = new List <string>();
                        TimeSpan?segmentArrival = null;

                        foreach (var line in unprocessedLines)
                        {
                            var match            = timingRegex.Match(line);
                            var nextArrivalGroup = match.Groups[1];

                            if (nextArrivalGroup.Success)
                            {
                                var nextArrivalTime = TimeSpan.Parse(nextArrivalGroup.Value);

                                if (segmentLines.Any())
                                {
                                    processedLines.Add((segmentArrival.Value, nextArrivalTime, segmentLines));
                                    segmentLines = new List <string>();
                                }

                                segmentArrival = nextArrivalTime;

                                // TODO: Make it clearer/explicit that verse text requires a paired segment timing
                                if (match.Groups[2].Success)
                                {
                                    var arrivalFrame = (int)(segmentArrival.Value.TotalSeconds * appConfig.FramesPerSecond);

                                    if (verseLabels.Any())
                                    {
                                        verseLabels.Last().HiddenFrame = arrivalFrame - (int)(verseLabelHideBeforeVerseEnd.TotalSeconds * appConfig.FramesPerSecond);
                                    }

                                    verseLabels.Add(new VerseLabel
                                    {
                                        ArrivalFrame = arrivalFrame,
                                        Text         = match.Groups[2].Value
                                    });
                                }
                            }

                            var lyricText = match.Groups[3].Value;

                            segmentLines.Add(lyricText);
                        }

                        if (verseLabels.Any())
                        {
                            verseLabels.Last().HiddenFrame = (int)(processedLines.Last().nextArrivalTime.TotalSeconds *appConfig.FramesPerSecond);
                        }

                        float?previousPixelsPerFrame = null;

                        var speedChanges = new List <(int arrivalFrame, float fromPixelsPerFrame, float toPixelsPerFrame)>();

                        var lyrics = new List <Lyric>();

                        foreach (var timingSegment in processedLines)
                        {
                            // Calculate the speed of this segment, defined by (distance / duration)
                            var segmentHeight = CalculateTextHeight(
                                lyricTypeface,
                                appConfig.LyricsFont.Size,
                                timingSegment.lines,
                                appConfig.LyricsFont.Size + appConfig.LyricsFont.LineMargin,
                                width - sideMargin * 2
                                );

                            var segmentDuration = timingSegment.nextArrivalTime - timingSegment.arrivalTime;
                            var pixelsPerFrame  = (float)(segmentHeight / segmentDuration.TotalSeconds / appConfig.FramesPerSecond);
                            var arrivalFrame    = (int)(timingSegment.arrivalTime.TotalSeconds * appConfig.FramesPerSecond);

                            speedChanges.Add((
                                                 arrivalFrame,
                                                 previousPixelsPerFrame.GetValueOrDefault(pixelsPerFrame),
                                                 pixelsPerFrame
                                                 ));

                            var segmentDurationFrames = segmentDuration.TotalSeconds * appConfig.FramesPerSecond;

                            previousPixelsPerFrame = pixelsPerFrame;

                            lyrics.Add(new Lyric
                            {
                                VisibleFrame = (int)(arrivalFrame - (appConfig.OutputDimensions.Height - desiredReadingY) / pixelsPerFrame - 100),
                                HiddenFrame  = (int)(arrivalFrame + segmentDurationFrames + appConfig.OutputDimensions.Height / pixelsPerFrame),
                                ArrivalFrame = arrivalFrame,
                                Lines        = timingSegment.lines,
                                Height       = segmentHeight
                            });
                        }

                        var currentPixelsPerFrame = speedChanges.First().fromPixelsPerFrame;

                        var   currentSpeedChangeIndex = 1;
                        var   speedChangeStartFrame   = speedChanges.Count > 1 ? speedChanges[1].arrivalFrame - speedChangeEasingFrames / 2 : new int?();
                        float?accelerationPerFrame    = null;

                        var endTransitionDissolveDurationFrames = (int)(endTransitionDuration.TotalSeconds * appConfig.FramesPerSecond);

                        // TODO: Can we calculate total frames required from the song end time automatically?
                        // Or do we need an explicit "end of audio" timestamp in the lyrics file?
                        var duration = TimeSpan.Parse(config.Duration);

                        var totalFramesRequired = duration.TotalSeconds * appConfig.FramesPerSecond;
                        var outputFilePath      = Path.Combine(outputDirectory.FullName, config.OutputFilename);

                        File.Delete(outputFilePath);

                        var ffmpegProcess = StartFfmpeg(appConfig, config.AudioFilePath, outputFilePath);

                        var info = new SKImageInfo(width, height);
                        using (var surface = SKSurface.Create(info))
                        {
                            var canvas = surface.Canvas;

                            for (var frame = 0; frame <= totalFramesRequired; frame++)
                            {
                                reportProgress(frame / (float)totalFramesRequired);

                                canvas.Clear(SKColors.Black);

                                if (speedChangeStartFrame.HasValue)
                                {
                                    if (frame == speedChangeStartFrame.Value + speedChangeEasingFrames)
                                    {
                                        currentSpeedChangeIndex++;
                                        speedChangeStartFrame = speedChanges.Count > currentSpeedChangeIndex
                                    ? speedChanges[currentSpeedChangeIndex].arrivalFrame - speedChangeEasingFrames / 2
                                    : new int?();
                                        accelerationPerFrame = null;
                                    }
                                    else if (frame >= speedChangeStartFrame)
                                    {
                                        accelerationPerFrame   = accelerationPerFrame ?? (speedChanges[currentSpeedChangeIndex].toPixelsPerFrame - currentPixelsPerFrame) / speedChangeEasingFrames;
                                        currentPixelsPerFrame += accelerationPerFrame.Value;
                                    }
                                }

                                for (var i = 0; i < lyrics.Count; i++)
                                {
                                    var lyric = lyrics[i];

                                    if (frame < lyric.VisibleFrame)
                                    {
                                        continue;
                                    }

                                    if (frame == lyric.VisibleFrame)
                                    {
                                        lyric.Y = i > 0
                                    ? lyrics[i - 1].Y + lyrics[i - 1].Height
                                    : desiredReadingY + (lyric.ArrivalFrame - lyric.VisibleFrame) * currentPixelsPerFrame;
                                    }
                                    else
                                    {
                                        lyric.Y -= currentPixelsPerFrame;
                                    }

                                    DrawLyric(
                                        canvas,
                                        lyricTypeface,
                                        appConfig.LyricsFont.Size,
                                        appConfig.LyricsFont.Size + appConfig.LyricsFont.LineMargin,
                                        lyric.Lines,
                                        x: sideMargin,
                                        y: lyric.Y
                                        );
                                }

                                DrawGradientOverlays(canvas);

                                var framesToArrivalPointAtCurrentSpeed = (int)((appConfig.OutputDimensions.Height - desiredReadingY) / currentPixelsPerFrame);

                                for (var i = 0; i < verseLabels.Count; i++)
                                {
                                    var verseLabel = verseLabels[i];
                                    var opacity    = 1f;

                                    if (frame < verseLabel.ArrivalFrame - framesToArrivalPointAtCurrentSpeed)
                                    {
                                        continue;
                                    }

                                    if (frame == verseLabel.ArrivalFrame - framesToArrivalPointAtCurrentSpeed)
                                    {
                                        verseLabel.Y = appConfig.OutputDimensions.Height;
                                    }
                                    else if (frame > verseLabel.ArrivalFrame - framesToArrivalPointAtCurrentSpeed &&
                                             verseLabel.Y > appConfig.OutputDimensions.HeaderHeight + appConfig.OutputDimensions.GradientHeight)
                                    {
                                        verseLabel.Y -= currentPixelsPerFrame;
                                    }
                                    else if (frame >= verseLabel.HiddenFrame)
                                    {
                                        opacity = Math.Max(0, 1 - (frame - verseLabel.HiddenFrame) / (float)DissolveAnimationDurationFrames);
                                    }

                                    DrawVerseLabel(
                                        canvas,
                                        verseTypeface,
                                        appConfig.VerseFont.Size,
                                        SKColor.Parse(appConfig.VerseFont.HexColor),
                                        verseLabel.Text,
                                        width - sideMargin,
                                        verseLabel.Y,
                                        opacity
                                        );
                                }

                                DrawTitleAndFooterBars(canvas, titleTypeface, appConfig.TitleFont.Size, SKColor.Parse(appConfig.TitleFont.HexColor), config.SongTitle);

                                if (totalFramesRequired - frame <= endTransitionDissolveDurationFrames)
                                {
                                    var alpha = (1 - (totalFramesRequired - frame) / endTransitionDissolveDurationFrames) * 255;
                                    using (var paint = new SKPaint
                                    {
                                        Color = SKColors.Black.WithAlpha((byte)alpha)
                                    })
                                    {
                                        canvas.DrawRect(new SKRect(0, 0, width, height), paint);
                                    }
                                }

                                using (var image = surface.Snapshot())
                                    using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
                                        using (var ms = new MemoryStream(data.ToArray())
                                        {
                                            Position = 0
                                        })
                                        {
                                            ms.WriteTo(ffmpegProcess.StandardInput.BaseStream);
                                            ms.Flush();
                                            ffmpegProcess.StandardInput.BaseStream.Flush();

                                            if (pngOutputPath != null)
                                            {
                                                using (var fs = new FileStream(Path.Combine(pngOutputPath, $"{frame:D5}.png"), FileMode.Create, FileAccess.Write))
                                                {
                                                    ms.Position = 0;
                                                    ms.WriteTo(fs);
                                                    ms.Flush();

                                                    fs.Close();
                                                }
                                            }

                                            ms.Close();
                                        }
                            }
                        }

                        ffmpegProcess.StandardInput.BaseStream.Close();
                        ffmpegProcess.WaitForExit();
                    }
        }