first commit

This commit is contained in:
Sakurai Ryota 2026-01-12 11:32:18 +09:00
commit 419bbcff26
7 changed files with 325 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin
obj
input
output

10
Encoder.csproj Normal file
View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

24
Encoder.sln Normal file
View File

@ -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

60
Lib/DetectWorker.cs Normal file
View File

@ -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<int> 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;
}
}
}

44
Lib/EncodeWorker.cs Normal file
View File

@ -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<int> 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;
}
}
}

View File

@ -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<Task> _tasks = new();
private readonly object _tasksLock = new();
protected override IEnumerable<Task>? 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);
}
}
}
}

52
Program.cs Normal file
View File

@ -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<Task>();
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();
}
}