From 419bbcff26947909f7855bf89096a307db98a8a0 Mon Sep 17 00:00:00 2001 From: Sakurai Ryouta Date: Mon, 12 Jan 2026 11:32:18 +0900 Subject: [PATCH] first commit --- .gitignore | 5 + Encoder.csproj | 10 ++ Encoder.sln | 24 +++++ Lib/DetectWorker.cs | 60 +++++++++++ Lib/EncodeWorker.cs | 44 ++++++++ LimitedConcurrencyLevelTaskScheduler.cs | 130 ++++++++++++++++++++++++ Program.cs | 52 ++++++++++ 7 files changed, 325 insertions(+) create mode 100644 .gitignore create mode 100644 Encoder.csproj create mode 100644 Encoder.sln create mode 100644 Lib/DetectWorker.cs create mode 100644 Lib/EncodeWorker.cs create mode 100644 LimitedConcurrencyLevelTaskScheduler.cs create mode 100644 Program.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7971b0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin +obj +input +output + diff --git a/Encoder.csproj b/Encoder.csproj new file mode 100644 index 0000000..2150e37 --- /dev/null +++ b/Encoder.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/Encoder.sln b/Encoder.sln new file mode 100644 index 0000000..09c2ee2 --- /dev/null +++ b/Encoder.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Encoder", "Encoder.csproj", "{39BA8241-C077-61B7-0115-1F5EA996EC8E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {39BA8241-C077-61B7-0115-1F5EA996EC8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39BA8241-C077-61B7-0115-1F5EA996EC8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39BA8241-C077-61B7-0115-1F5EA996EC8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39BA8241-C077-61B7-0115-1F5EA996EC8E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E16575A-2202-4007-8A5E-CC8C9361B113} + EndGlobalSection +EndGlobal diff --git a/Lib/DetectWorker.cs b/Lib/DetectWorker.cs new file mode 100644 index 0000000..d919bb7 --- /dev/null +++ b/Lib/DetectWorker.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; + +namespace Encoder.Lib; + +public class DetectWorker +{ + private readonly ProcessStartInfo startInfo; + public string? MaxVolume { get; private set; } + public string? MeanVolume { get; private set; } + + public DetectWorker(string workingDirectory, string inputFile, string encodeParam) + { + var arguments = $"-y -i \"{inputFile}\" {encodeParam} -f null -"; + this.startInfo = new ProcessStartInfo() + { + FileName = "ffmpeg", + WorkingDirectory = workingDirectory, + Arguments = arguments, + }; + this.MeanVolume = null; + this.MaxVolume = null; + } + + public async ValueTask Run() + { + try + { + Console.WriteLine($"{this.startInfo.FileName} {this.startInfo.Arguments}"); + using Process process = new Process(); + process.StartInfo = this.startInfo; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + + var result = process.Start(); + + if (!result) + { + throw new FileNotFoundException(); + } + + var str = await process.StandardError.ReadToEndAsync().ConfigureAwait(false); + await process.WaitForExitAsync().ConfigureAwait(false); + + // Console.WriteLine(str); + var maxVolumeLine = str.Split('\n').FirstOrDefault(line => line.Contains("max_volume")); + this.MaxVolume = maxVolumeLine?.Split(':').Skip(1).FirstOrDefault()?.Trim().Replace(" ", ""); + var meanVolumeLine = str.Split('\n').FirstOrDefault(line => line.Contains("mean_volume")); + this.MeanVolume = meanVolumeLine?.Split(':').Skip(1).FirstOrDefault()?.Trim().Replace(" ", ""); + + return process.ExitCode; + } + catch (Exception err) + { + Console.WriteLine(err); + return -1; + } + } +} diff --git a/Lib/EncodeWorker.cs b/Lib/EncodeWorker.cs new file mode 100644 index 0000000..64e2bdd --- /dev/null +++ b/Lib/EncodeWorker.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; + +namespace Encoder.Lib; + +public class EncodeWorker +{ + private readonly ProcessStartInfo startInfo; + + public EncodeWorker(string workingDirectory, string inputFile, string outputFile, string encodeParam) + { + var arguments = $"-y -i \"{inputFile}\" {encodeParam} \"{outputFile}\""; + this.startInfo = new ProcessStartInfo() + { + FileName = "ffmpeg", + WorkingDirectory = workingDirectory, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + } + + public async ValueTask Run() + { + try + { + Console.WriteLine($"{this.startInfo.FileName} {this.startInfo.Arguments}"); + using Process? process = Process.Start(this.startInfo); + if (process is null) + { + throw new FileNotFoundException(); + } + + await process.StandardError.ReadToEndAsync().ConfigureAwait(false); + await process.WaitForExitAsync().ConfigureAwait(false); + return process.ExitCode; + } + catch + { + return -1; + } + } +} diff --git a/LimitedConcurrencyLevelTaskScheduler.cs b/LimitedConcurrencyLevelTaskScheduler.cs new file mode 100644 index 0000000..818ea97 --- /dev/null +++ b/LimitedConcurrencyLevelTaskScheduler.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Encoder +{ + public class LimitedConcurrencyLevelTaskScheduler + : TaskScheduler + { + + public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) + { + if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism), "maxDegreeOfParallelism must be greater than zero."); + _maxDegreeOfParallelism = maxDegreeOfParallelism; + } + + + + public override int MaximumConcurrencyLevel => _maxDegreeOfParallelism; + + private readonly int _maxDegreeOfParallelism; + private int _delegatesQueuedOrRunning; + + + private readonly LinkedList _tasks = new(); + private readonly object _tasksLock = new(); + + protected override IEnumerable? GetScheduledTasks() + { + //API仕様通り、ロックが取れたらタスク一覧を返し、ロックが取れないならNotSupportedExceptionを投げる。 + bool lockTaken = false; + try + { + Monitor.TryEnter(_tasksLock, ref lockTaken); + if (lockTaken) + { + return _tasks.ToArray(); + } + else + { + throw new NotSupportedException(); + } + } + finally + { + if (lockTaken) Monitor.Exit(_tasksLock); + } + + } + + protected override void QueueTask(Task task) + { + //Taskを追加した上で、同時実行スレッド数を超えていなければNotifyThreadPoolOfPendingWorkへ進む。 + //NotifyThreadPoolOfPendingWorkは、Taskが空になるまで実行を続けるある種のループ構造となるので、これが何個同時に走るかをコントロールすることで、実質的にスレッドプールの同時処理上限をコントロールできる。 + lock (_tasksLock) + { + _tasks.AddLast(task); + if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) + { + _delegatesQueuedOrRunning++; + NotifyThreadPoolOfPendingWork(); + } + } + + } + + private void NotifyThreadPoolOfPendingWork() + { + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + Task? item; + lock (_tasksLock) + { + //タスクが空になったら_tasks.Firstはnull + item = _tasks.First?.Value; + if (item is null) + { + //空になったらこの一連のNotifyThreadPoolOfPendingWorkのループを終了し、ループの数をデクリメントする。 + _delegatesQueuedOrRunning--; + return; + } + _tasks.RemoveFirst(); + } + + base.TryExecuteTask(item); + NotifyThreadPoolOfPendingWork(); + }, null); + } + + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + //インライン実行については、現在回しているループの数が上限以下であれば、空きがあるということで実行する。そうではない場合はfalseで断る。 + if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) + { + if (taskWasPreviouslyQueued) + { + if (TryDequeue(task)) + { + return base.TryExecuteTask(task); + } + else + { + return false; + } + } + else + { + return base.TryExecuteTask(task); + } + } + else + { + return false; + } + } + + protected override bool TryDequeue(Task task) + { + lock (_tasksLock) + { + return _tasks.Remove(task); + } + } + + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c8ce379 --- /dev/null +++ b/Program.cs @@ -0,0 +1,52 @@ +using Encoder.Lib; + +namespace Encoder; + +public static class Program +{ + public static void Main(string[] args) + { + var inputDirectory = new DirectoryInfo("input"); + var outputDirectory = new DirectoryInfo("output"); + + if (!inputDirectory.Exists) + { + inputDirectory.Create(); + } + + if (!outputDirectory.Exists) + { + outputDirectory.Create(); + } + + var coreCount = (int) Math.Max(Environment.ProcessorCount / 2, 1); + var limitedTaskScheduler = new LimitedConcurrencyLevelTaskScheduler(coreCount); + var limitedTaskFactory = new TaskFactory(limitedTaskScheduler); + var tasks = new List(); + + foreach (var file in inputDirectory.EnumerateFiles()) + { + var task = limitedTaskFactory.StartNew(async () => { + // detect volumes. + var detectWorker = new DetectWorker("./", file.FullName, "-vn -af volumedetect"); + await detectWorker.Run().ConfigureAwait(false); + if (string.IsNullOrEmpty(detectWorker.MaxVolume) || string.IsNullOrWhiteSpace(detectWorker.MeanVolume)) + { + return; + } + + var maxVolumeF = float.Parse(detectWorker.MaxVolume.Substring(0, detectWorker.MaxVolume.Length - 2)); + var meanVolumeF = float.Parse(detectWorker.MeanVolume.Substring(0, detectWorker.MeanVolume.Length - 2)); + var gainVolume = $"{Math.Round(meanVolumeF * 10 / 20 * -1)}dB"; + var outputFile = Path.ChangeExtension(Path.Combine(outputDirectory.FullName, file.Name), "m4a"); + + // encoding + EncodeWorker worker = new EncodeWorker("./", file.FullName, outputFile, $"-vn -ac 2 -c:a aac -b:a 256k -ar 48000 -af volume={gainVolume} -f mp4"); + await worker.Run().ConfigureAwait(false); + }); + tasks.Add(task.Unwrap()); + } + + Task.WhenAll(tasks.ToArray()).ConfigureAwait(false).GetAwaiter().GetResult(); + } +}