first commit
This commit is contained in:
commit
419bbcff26
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
bin
|
||||||
|
obj
|
||||||
|
input
|
||||||
|
output
|
||||||
|
|
||||||
10
Encoder.csproj
Normal file
10
Encoder.csproj
Normal 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
24
Encoder.sln
Normal 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
60
Lib/DetectWorker.cs
Normal 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
44
Lib/EncodeWorker.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
LimitedConcurrencyLevelTaskScheduler.cs
Normal file
130
LimitedConcurrencyLevelTaskScheduler.cs
Normal 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
52
Program.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user