Compare commits

...

28 commits
main ... main

Author SHA1 Message Date
Mercurio 807945aed9 replaced WebClient with HttpClient, refactored ffmpeg and apploopback downloads to be async tasks, added regex rules for demo loop 2025-02-09 16:17:03 +01:00
Mercurio b5327f1574 Revert ffmpeg clip combination, fix application audio recording call for charts with spaces and special characters in them 2025-01-01 16:46:35 +01:00
Mercurio 1b8420f1ab Merge branch 'main' of https://git.mercurio.moe/Mercury/2dxAutoClip 2024-12-15 22:16:38 +01:00
Mercurio 1379ad664b Refactor clip merge function to use hw encoding and fixed-framerate rendering 2024-12-15 22:16:20 +01:00
Mercury. 9c5aac2033 Update readme-ita.md 2024-12-15 17:49:47 +01:00
Mercury. 2406cc4aa6 Update readme.md 2024-12-15 17:48:33 +01:00
Mercurio 9a3987e63f Added ApplicationLoopback module download and toggle in prop. 2024-12-15 17:17:37 +01:00
Mercurio 139b64dd1b Initial support for ApplicationLoopback audio recording (WASAPI only) 2024-12-15 15:42:33 +01:00
Mercurio d6010162fc Added support for cached DxDiag output 2024-10-07 22:47:43 +02:00
Mercurio 507e17ec14 Code Cleanup 2024-09-19 12:32:34 +02:00
Mercurio 98fa171645 change hardware detection to DxDiag 2024-09-10 21:22:32 +02:00
Mercury 4408023cd6 updated readmes 2024-09-10 17:08:26 +02:00
Mercury d4c662ca4c remove FFMPEG from artifacts, as it gets downloaded automatically on application startup 2024-09-10 16:58:32 +02:00
Mercury 944c6c90a0 add GPU autodetection for HW accelerated recording and encoding. 2024-09-10 16:43:10 +02:00
Mercury. cc70624b54 Update 2dxAutoClip/iidxAutoClip.cs
Disable asio recording.
2024-09-09 21:27:06 +02:00
Mercury. 82fb949125 revert 80a1e6d7bb
Temporarily revert and disable ASIO recording
2024-09-09 21:25:39 +02:00
Mercurio f62c51729f Properly instantiate ASIO recording 2024-09-09 21:12:20 +02:00
Mercurio 390dc02117 ASIO Recording 2024-09-09 21:00:34 +02:00
Mercury. 80a1e6d7bb Updated dotnet runtime links 2024-09-07 16:54:13 +02:00
Mercury. a46c3ed226 aggiornato readme italiano 2024-09-07 16:53:29 +02:00
Mercury cf44c4689f Aggiunto README italiano 2024-09-07 15:53:47 +02:00
Mercury. 1d3c4c7e16 Update readme.md 2024-09-07 15:52:03 +02:00
Mercury. b189e379dd Update readme.md 2024-09-07 14:05:58 +02:00
Mercurio b91ed611d0 Force DDAgrab framerate, add -shortest to combined output to reduce dropped frames at the end of the final clip 2024-09-07 13:50:34 +02:00
Mercurio 8f96bb6029 0.0.4 prep
- added custom settings loading from prop.txt
- added `-tune zerolatency` to FFMPEG capture
- reduced latency for WASAPI audio capture
- cleared up a bit of the ffmpeg spawning/killing logic
2024-09-06 15:02:18 +02:00
Mercury. c67c1ac504 Update readme.md 2024-09-06 14:58:33 +02:00
Mercury. c93c205572 Update 2dxAutoClip/artifacts/prop.txt 2024-09-06 14:54:08 +02:00
Mercury. a3a536316f Update prop to include more custom settings 2024-09-06 14:19:14 +02:00
12 changed files with 548 additions and 184 deletions

Binary file not shown.

View file

@ -1 +1,5 @@
path: E:\autorecording path: C:\Your\Path\Here
resolution: 1920x1080
framerate: 60
crf: 20
game_process_name: spice64

View file

@ -1,39 +1,123 @@
using System.Diagnostics; using System.Diagnostics;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using NAudio.CoreAudioApi; using NAudio.CoreAudioApi;
using NAudio.Wave; using NAudio.Wave;
using System.Net;
namespace _2dxAutoClip; namespace _2dxAutoClip;
class Program class Program
{ {
private static readonly string WebsocketAddress = "localhost"; private static readonly string WebsocketAddress = "localhost";
private static readonly int WebsocketPort = 10573;
private static Process? _ffmpegProcess; private static Process? _ffmpegProcess;
private static string _ffmpegFolderPath = GetFolderPath(); private static Process? _recorderprocess;
private static string _ffmpegFolderPath = GetDefaultVideosFolderPath();
private static WasapiCapture? _waveSource; private static WasapiCapture? _waveSource;
private static WaveFileWriter _writer = null!; private static WaveFileWriter _writer = null!;
private static string _audioFilePath = null!; private static string _audioFilePath = null!;
private static string _videoFilePath = null!; private static string _videoFilePath = null!;
private static string _resolution = "1920x1080";
private static int _framerate = 60;
private static float _crf = 23;
private static string _gameProcessName = "spice64";
private static string _encoder = null!;
private static bool _sysaudio;
private static async Task Main(string[] args) private static async Task Main(string[] args)
{ {
var spiceProcesses = Process.GetProcessesByName("spice64"); await DownloadFFmpeg();
LoadSettingsFromPropFile();
if (spiceProcesses.Length > 0) await FetchAppLB();
_encoder = GetHardwareEncoder();
var gameProcesses = Process.GetProcessesByName(_gameProcessName);
if (gameProcesses.Length > 0)
{ {
Console.WriteLine("Found spice64, Attempting connection to TickerHookWS..."); Console.WriteLine($"Found {_gameProcessName}, Attempting connection to TickerHookWS...");
await TryConnectWebSocket(); await TryConnectWebSocket();
} }
else else
{ {
Console.WriteLine("Unable to find Spice64. Are you sure Beatmania IIDX is running and TickerHook is enabled?"); Console.WriteLine($"Unable to find {_gameProcessName}. Is the game running and TickerHook enabled?");
}
}
private static async Task DownloadFFmpeg()
{
const string ffmpegExe = "ffmpeg.exe";
const string ffmpegUrl = "https://tfm2.mercurio.moe/ffmpeg.exe";
if (File.Exists(ffmpegExe))
{
Console.WriteLine("FFmpeg already exists.");
return;
}
try
{
Console.WriteLine("FFmpeg not found. Downloading...");
using (HttpClient client = new HttpClient())
{
using (HttpResponseMessage response = await client.GetAsync(ffmpegUrl))
{
response.EnsureSuccessStatusCode();
byte[] fileBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync(ffmpegExe, fileBytes);
} }
} }
Console.WriteLine("FFmpeg downloaded successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error downloading FFmpeg: {ex.Message}");
}
}
private static string GetFolderPath() private static async Task FetchAppLB()
{
const string applbBinary = "applb.exe";
const string applburl = "https://tfm2.mercurio.moe/applb.exe";
if (File.Exists(applbBinary))
{
Console.WriteLine("AppLB already exists.");
return;
}
try
{
Console.WriteLine("AppLB not found. Downloading...");
using (HttpClient client = new HttpClient())
{
var response = await client.GetAsync(applburl);
response.EnsureSuccessStatusCode();
await using var fileStream = new FileStream(applbBinary, FileMode.Create, FileAccess.Write, FileShare.None);
await using var httpStream = await response.Content.ReadAsStreamAsync();
await httpStream.CopyToAsync(fileStream);
}
Console.WriteLine("AppLB downloaded successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error downloading AppLB: {ex.Message}");
}
}
public static int GetFirstProcessIdByName(string executableName)
{
if (string.IsNullOrWhiteSpace(executableName))
{
throw new ArgumentException("Executable name cannot be null or empty.", nameof(executableName));
}
var processes = Process.GetProcessesByName(executableName);
Console.WriteLine($"Found spice PID: {processes.FirstOrDefault()?.Id}");
return processes.FirstOrDefault()?.Id ?? -1;
}
private static void LoadSettingsFromPropFile()
{ {
const string filePath = "prop.txt"; const string filePath = "prop.txt";
if (File.Exists(filePath)) if (File.Exists(filePath))
@ -47,84 +131,100 @@ class Program
var path = line["path:".Length..].Trim(); var path = line["path:".Length..].Trim();
if (Directory.Exists(path)) if (Directory.Exists(path))
{ {
return path; _ffmpegFolderPath = path;
} }
Console.WriteLine($"The path specified in {filePath} does not exist."); else
{
Console.WriteLine($"The path specified in {filePath} does not exist. Using default recording path.");
_ffmpegFolderPath = GetDefaultVideosFolderPath();
}
}
if (line.StartsWith("resolution:"))
{
_resolution = line["resolution:".Length..].Trim();
Console.WriteLine($"Custom Resolution: {_resolution}");
}
if (line.StartsWith("framerate:"))
{
_framerate = int.Parse(line["framerate:".Length..].Trim());
Console.WriteLine($"Custom framerate: {_framerate}");
}
if (line.StartsWith("crf:"))
{
_crf = float.Parse(line["crf:".Length..].Trim());
Console.WriteLine($"custom crf: {_crf}");
}
if (line.StartsWith("game_process_name:"))
{
_gameProcessName = line["game_process_name:".Length..].Trim();
Console.WriteLine($"custom process name: {_gameProcessName}");
}
if (line.StartsWith("record_system_audio:"))
{
_sysaudio = bool.Parse(line["record_system_audio:".Length..].Trim());
Console.WriteLine($"record system audio: {_sysaudio}");
} }
} }
} }
else else
{ {
Console.WriteLine($"The file {filePath} does not exist."); Console.WriteLine($"The file {filePath} does not exist. Using default values.");
_ffmpegFolderPath = GetDefaultVideosFolderPath();
} }
return GetDefaultVideosFolderPath();
} }
private static string GetDefaultVideosFolderPath() private static string GetDefaultVideosFolderPath()
{ {
var userName = Environment.UserName; var userName = Environment.UserName;
var defaultPath = Path.Combine($@"C:\Users\{userName}", "Videos"); return Path.Combine($@"C:\Users\{userName}", "Videos");
return defaultPath;
} }
private static async Task TryConnectWebSocket() private static async Task TryConnectWebSocket()
{ {
const int maxRetries = 5; const int maxRetries = 5;
int attempt = 0; var attempt = 0;
while (attempt < maxRetries) while (attempt < maxRetries)
{ {
Console.WriteLine($"Attempt {attempt + 1} of {maxRetries} to connect..."); Console.WriteLine($"Attempt {attempt + 1} of {maxRetries} to connect...");
var success = await ConnectWebSocket();
bool success = await ConnectWebSocket(); if (success) break;
if (success)
{
break; // Exit the loop if connection was successful
}
attempt++; attempt++;
if (attempt < maxRetries) if (attempt < maxRetries)
{ {
Console.WriteLine($"Retrying in 10 seconds... {maxRetries - attempt} attempts remaining."); Console.WriteLine($"Retrying in 10 seconds... {maxRetries - attempt} attempts remaining.");
await Task.Delay(10000); // Wait for 10 seconds before retrying await Task.Delay(10000);
} }
} }
if (attempt == maxRetries) if (attempt == maxRetries)
{ {
Console.WriteLine("Failed to connect after 5 attempts."); Console.WriteLine("Failed to connect after 5 attempts.");
} }
} }
private static async Task<bool> ConnectWebSocket() private static async Task<bool> ConnectWebSocket()
{ {
var tickerUri = new Uri($"ws://{WebsocketAddress}:{WebsocketPort}"); var tickerUri = new Uri($"ws://{WebsocketAddress}:10573");
var reconnecting = false; var reconnecting = false;
var lastMessage = string.Empty; var lastMessage = string.Empty;
var consecutiveMessageCount = 0; var consecutiveMessageCount = 0;
var isRecording = false; var isRecording = false;
var currentSongName = string.Empty; var currentSongName = string.Empty;
var shouldCheckForMusicSelect = false; var shouldCheckForMusicSelect = false;
using var clientWebSocket = new ClientWebSocket(); using var clientWebSocket = new ClientWebSocket();
try try
{ {
await clientWebSocket.ConnectAsync(tickerUri, CancellationToken.None); await clientWebSocket.ConnectAsync(tickerUri, CancellationToken.None);
Console.WriteLine("Connected to TickerHook WebSocket."); Console.WriteLine("[TickerHook] Connected to WebSocket.");
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Error connecting to TickerHook WebSocket: {ex.Message}"); Console.WriteLine($"[TickerHook] [ERROR] Unable to connect to WebSocket: {ex.Message}");
return false; // Indicate connection failure return false;
} }
var buffer = new byte[1024]; var buffer = new byte[1024];
while (clientWebSocket.State == WebSocketState.Open) while (clientWebSocket.State == WebSocketState.Open)
{ {
WebSocketReceiveResult result; WebSocketReceiveResult result;
@ -134,13 +234,19 @@ private static async Task<bool> ConnectWebSocket()
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Error receiving message: {ex.Message}"); Console.WriteLine($"[TickerHook] [ERROR] Unable to receive message: {ex.Message}");
reconnecting = true; reconnecting = true;
break; break;
} }
var message = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim().ToUpper(); var message = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim().ToUpper();
Console.WriteLine($"Received message: {message}"); Console.WriteLine($"[TickerHook] Received message: {message}");
if (Regex.IsMatch(message, "^WELCOME TO BEATMANIA.*") || message.Contains("DEMO PLAY"))
{
Console.WriteLine("[TickerHook] Ignoring excluded message.");
continue;
}
if (message == lastMessage && !message.Contains("SELECT FROM")) if (message == lastMessage && !message.Contains("SELECT FROM"))
{ {
@ -177,11 +283,19 @@ private static async Task<bool> ConnectWebSocket()
} }
return !reconnecting; return !reconnecting;
} }
private static void StartRecording(string songName) private static void StartRecording(string songName)
{
if (_sysaudio == true)
{ {
Task.Run(() => StartAudioRecording(songName)); Task.Run(() => StartAudioRecording(songName));
}
else
{
StartAudioProcessRecording(songName);
}
StartFfmpegRecording(songName); StartFfmpegRecording(songName);
} }
@ -191,8 +305,7 @@ private static async Task<bool> ConnectWebSocket()
{ {
var date = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); var date = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
_audioFilePath = $"{_ffmpegFolderPath}\\{songName}_{date}.wav"; _audioFilePath = $"{_ffmpegFolderPath}\\{songName}_{date}.wav";
var outputFolder = Path.GetDirectoryName(_audioFilePath)!; Directory.CreateDirectory(Path.GetDirectoryName(_audioFilePath)!);
Directory.CreateDirectory(outputFolder);
_waveSource = new WasapiLoopbackCapture(); _waveSource = new WasapiLoopbackCapture();
_writer = new WaveFileWriter(_audioFilePath, _waveSource.WaveFormat); _writer = new WaveFileWriter(_audioFilePath, _waveSource.WaveFormat);
_waveSource.DataAvailable += (sender, args) => _waveSource.DataAvailable += (sender, args) =>
@ -202,17 +315,60 @@ private static async Task<bool> ConnectWebSocket()
_waveSource.RecordingStopped += (sender, args) => _waveSource.RecordingStopped += (sender, args) =>
{ {
_writer.Dispose(); _writer.Dispose();
_writer = null!;
_waveSource.Dispose(); _waveSource.Dispose();
_waveSource = null;
}; };
_waveSource.StartRecording(); _waveSource.StartRecording();
Console.WriteLine("WASAPI Audio recording started."); Console.WriteLine("[WASAPI] Audio recording started.");
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Error starting audio recording: {ex.Message}"); Console.WriteLine($"[WASAPI] [ERROR] Unable to start audio recording:{ex.Message}");
}
}
private static void StartAudioProcessRecording(string songName)
{
try
{
var date = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
_audioFilePath = $"{_ffmpegFolderPath}\\{songName}_{date}.wav";
var procid = GetFirstProcessIdByName(_gameProcessName);
var args = $"{procid} includetree \"{_audioFilePath}\"";
_recorderprocess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "applb",
Arguments = args,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
_recorderprocess.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
Console.WriteLine($"[applb]: {e.Data}");
}
};
_recorderprocess.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
Console.WriteLine($"[applb] [ERROR]: {e.Data}");
}
};
_recorderprocess.Start();
_recorderprocess.BeginOutputReadLine();
_recorderprocess.BeginErrorReadLine();
}
catch (Exception ex)
{
Console.WriteLine($"[applb] [ERROR] Unable to start audio recording: {ex.Message}");
} }
} }
@ -222,64 +378,176 @@ private static async Task<bool> ConnectWebSocket()
{ {
var date = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); var date = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
_videoFilePath = $"{_ffmpegFolderPath}\\{songName}_{date}.mkv"; _videoFilePath = $"{_ffmpegFolderPath}\\{songName}_{date}.mkv";
var ffmpegArguments = $"-framerate {_framerate} " +
var ffmpegArguments = $"-framerate 60 " + $"-filter_complex \"ddagrab=framerate={_framerate},hwdownload,format=bgra\" " +
$"-filter_complex \"ddagrab=0,hwdownload,format=bgra\" " + $"{_encoder} -crf {_crf} -video_size {_resolution} -draw_mouse 0 -movflags +faststart -y \"{_videoFilePath}\"";
$"-c:v libx264 -movflags +faststart -crf 20 -y \"{_videoFilePath}\""; _ffmpegProcess = new Process
{
_ffmpegProcess = new Process(); StartInfo = new ProcessStartInfo
_ffmpegProcess.StartInfo.FileName = "ffmpeg"; {
_ffmpegProcess.StartInfo.Arguments = ffmpegArguments; FileName = "ffmpeg",
_ffmpegProcess.StartInfo.UseShellExecute = false; Arguments = ffmpegArguments,
_ffmpegProcess.StartInfo.RedirectStandardOutput = false; UseShellExecute = false,
_ffmpegProcess.StartInfo.RedirectStandardError = true; RedirectStandardError = true,
_ffmpegProcess.StartInfo.CreateNoWindow = true; CreateNoWindow = true
}
_ffmpegProcess.ErrorDataReceived += (_, args) => Console.WriteLine(args.Data); };
_ffmpegProcess.Start(); _ffmpegProcess.Start();
_ffmpegProcess.BeginErrorReadLine();
Console.WriteLine("[FFmpeg] recording started.");
}
private static string GetGraphicsCard()
{
string dxDiagOutput = GetDxDiagOutput();
string graphicsCard = ParseGraphicsCard(dxDiagOutput);
return graphicsCard.ToUpper();
}
private static string GetDxDiagOutput()
{
string dxDiagFilePath = "dxdiag_output.txt";
if (File.Exists(dxDiagFilePath))
{
DateTime fileCreationTime = File.GetLastWriteTime(dxDiagFilePath);
DateTime oneWeekAgo = DateTime.Now.AddDays(-7);
if (fileCreationTime > oneWeekAgo)
{
Console.WriteLine("[DxDiagHelper] Using cached dxdiag_output.txt.");
Console.WriteLine("[DxDiagHelper] Delete your cached dxdiag_output.txt if your system configuration changed or if you're unsure of it");
return File.ReadAllText(dxDiagFilePath);
}
else
{
Console.WriteLine("[DxDiagHelper] dxdiag_output.txt is older than a week, regenerating...");
}
}
else
{
Console.WriteLine("[DxDiagHelper] dxdiag_output.txt does not exist, generating...");
}
return RunDxDiag(dxDiagFilePath);
}
private static string RunDxDiag(string dxDiagFilePath)
{
Process dxDiagProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dxdiag",
Arguments = $"/t {dxDiagFilePath}",
UseShellExecute = false,
RedirectStandardOutput = false,
CreateNoWindow = true
}
};
Console.WriteLine("[DxDiagHelper] Gathering GPU information for Hardware Accelerated Video");
dxDiagProcess.Start();
dxDiagProcess.WaitForExit();
return File.ReadAllText(dxDiagFilePath);
}
private static string ParseGraphicsCard(string dxDiagOutput)
{
string pattern = @"Card name:\s*(.*)";
Match match = Regex.Match(dxDiagOutput, pattern);
if (match.Success)
{
return match.Groups[1].Value;
}
return "Unknown";
}
private static string GetHardwareEncoder()
{
var graphicsCard = GetGraphicsCard();
Console.WriteLine($"[DxDiagHelper] Using {graphicsCard} for hardware video acceleration");
var encoder = "-c:v libx264";
if (graphicsCard.Contains("NVIDIA"))
{
encoder = "-c:v h264_nvenc";
Console.WriteLine("[DxDiagHelper] Using NVIDIA hardware encoding (h264_nvenc).");
}
else if (graphicsCard.Contains("AMD"))
{
encoder = "-c:v h264_amf";
Console.WriteLine("[DxDiagHelper] Using AMD hardware encoding (h264_amf).");
}
else if (graphicsCard.Contains("INTEL"))
{
encoder = "-c:v h264_qsv";
Console.WriteLine("[DxDiagHelper] Using Intel hardware encoding (h264_qsv).");
}
else
{
Console.WriteLine("[DxDiagHelper] [WARN] No recognized hardware encoder found, using CPU (libx264).");
Console.WriteLine("[DxDiagHelper] [WARN] Cpu encoding might present some graphical glitches such as desync at the end of the video or really slow framerates");
}
return encoder;
} }
private static void StopRecording(string songName) private static void StopRecording(string songName)
{ {
StopFfmpegRecording(); if (_sysaudio == true)
{
StopAudioRecording(); StopAudioRecording();
} else {
TerminateProcessByName();
}
StopFfmpegRecording();
CombineAudioAndVideo(_videoFilePath, _audioFilePath, songName); CombineAudioAndVideo(_videoFilePath, _audioFilePath, songName);
} }
public static void TerminateProcessByName()
{
var executableName = "applb";
try
{
int processId = GetFirstProcessIdByName(executableName);
if (processId == -1)
{
Console.WriteLine($"No running process found for '{executableName}'.");
return;
}
var process = Process.GetProcessById(processId);
process.Kill();
Console.WriteLine($"Process '{executableName}' with ID {processId} terminated successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error terminating process '{executableName}': {ex.Message}");
}
}
private static void StopAudioRecording() private static void StopAudioRecording()
{ {
if (_waveSource == null) return; _waveSource?.StopRecording();
Console.WriteLine("[WASAPI] Audio recording stopped.");
_waveSource.StopRecording();
_waveSource.Dispose();
_waveSource = null;
if (_writer != null)
{
_writer.Dispose();
_writer = null!;
} }
Console.WriteLine("Audio recording stopped.");
}
private static void StopFfmpegRecording() private static void StopFfmpegRecording()
{ {
if (_ffmpegProcess != null && _ffmpegProcess.HasExited) return; if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
_ffmpegProcess?.Kill(); {
_ffmpegProcess?.WaitForExit(); _ffmpegProcess.Kill();
_ffmpegProcess?.Dispose(); _ffmpegProcess.WaitForExit();
_ffmpegProcess = null; _ffmpegProcess = null!;
Console.WriteLine("FFMPEG process stopped."); Console.WriteLine("[FFmpeg] recording stopped.");
}
} }
private static void CombineAudioAndVideo(string videoFilePath, string audioFilePath, string songName) private static void CombineAudioAndVideo(string videoFilePath, string audioFilePath, string songName)
{ {
var combinedOutputFilePath = $"{_ffmpegFolderPath}\\{songName}_combined_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.mp4"; var combinedOutputFilePath = $"{_ffmpegFolderPath}\\{songName}_combined_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.mp4";
var ffmpegArgs = var ffmpegArgs =
$"-y -i \"{videoFilePath}\" -i \"{audioFilePath}\" -c:v copy -c:a aac -strict experimental \"{combinedOutputFilePath}\""; $"-y -i \"{videoFilePath}\" -i \"{audioFilePath}\" -c:v copy -c:a aac -strict experimental -shortest \"{combinedOutputFilePath}\"";
var processInfo = new ProcessStartInfo var processInfo = new ProcessStartInfo
{ {
@ -294,7 +562,7 @@ private static async Task<bool> ConnectWebSocket()
using var process = Process.Start(processInfo); using var process = Process.Start(processInfo);
if (process == null) if (process == null)
{ {
Console.WriteLine("FFmpeg failed to start."); Console.WriteLine("[FFmpeg] [FATAL] Unable to start.");
return; return;
} }
@ -305,8 +573,8 @@ private static async Task<bool> ConnectWebSocket()
Console.WriteLine("FFmpeg Error: " + error); Console.WriteLine("FFmpeg Error: " + error);
Console.WriteLine(File.Exists(combinedOutputFilePath) Console.WriteLine(File.Exists(combinedOutputFilePath)
? "Audio and video have been successfully combined." ? "[FFmpeg] Audio and video have been successfully combined."
: "Failed to combine audio and video. Check the logs for errors."); : "[FFmpeg] [FATAL] Failed to combine audio and video. Check the logs for errors.");
File.Delete(videoFilePath); File.Delete(videoFilePath);
File.Delete(audioFilePath); File.Delete(audioFilePath);
} }

View file

@ -1,25 +1,32 @@
{ {
"format": 1, "format": 1,
"restore": { "restore": {
"D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj": {} "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj": {}
}, },
"projects": { "projects": {
"D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj": { "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj": {
"version": "1.0.0", "version": "1.0.0",
"restore": { "restore": {
"projectUniqueName": "D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj", "projectUniqueName": "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj",
"projectName": "2dxAutoClip", "projectName": "2dxAutoClip",
"projectPath": "D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj", "projectPath": "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj",
"packagesPath": "C:\\Users\\Mercury\\.nuget\\packages\\", "packagesPath": "C:\\Users\\Mercury\\.nuget\\packages\\",
"outputPath": "D:\\2dxAutoClip\\2dxAutoClip\\obj\\", "outputPath": "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\obj\\",
"projectStyle": "PackageReference", "projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
],
"configFilePaths": [ "configFilePaths": [
"C:\\Users\\Mercury\\AppData\\Roaming\\NuGet\\NuGet.Config" "C:\\Users\\Mercury\\AppData\\Roaming\\NuGet\\NuGet.Config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
], ],
"originalTargetFrameworks": [ "originalTargetFrameworks": [
"net8.0" "net8.0"
], ],
"sources": { "sources": {
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
"C:\\Program Files\\dotnet\\library-packs": {},
"https://api.nuget.org/v3/index.json": {} "https://api.nuget.org/v3/index.json": {}
}, },
"frameworks": { "frameworks": {
@ -37,7 +44,8 @@
"enableAudit": "true", "enableAudit": "true",
"auditLevel": "low", "auditLevel": "low",
"auditMode": "direct" "auditMode": "direct"
} },
"SdkAnalysisLevel": "9.0.100"
}, },
"frameworks": { "frameworks": {
"net8.0": { "net8.0": {
@ -68,7 +76,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Users\\Mercury\\.dotnet\\sdk\\8.0.303/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.101/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View file

@ -5,11 +5,12 @@
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool> <RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile> <ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Mercury\.nuget\packages\</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Mercury\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.10.1</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.12.2</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\Mercury\.nuget\packages\" /> <SourceRoot Include="C:\Users\Mercury\.nuget\packages\" />
<SourceRoot Include="C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -452,24 +452,32 @@
] ]
}, },
"packageFolders": { "packageFolders": {
"C:\\Users\\Mercury\\.nuget\\packages\\": {} "C:\\Users\\Mercury\\.nuget\\packages\\": {},
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
}, },
"project": { "project": {
"version": "1.0.0", "version": "1.0.0",
"restore": { "restore": {
"projectUniqueName": "D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj", "projectUniqueName": "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj",
"projectName": "2dxAutoClip", "projectName": "2dxAutoClip",
"projectPath": "D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj", "projectPath": "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj",
"packagesPath": "C:\\Users\\Mercury\\.nuget\\packages\\", "packagesPath": "C:\\Users\\Mercury\\.nuget\\packages\\",
"outputPath": "D:\\2dxAutoClip\\2dxAutoClip\\obj\\", "outputPath": "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\obj\\",
"projectStyle": "PackageReference", "projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
],
"configFilePaths": [ "configFilePaths": [
"C:\\Users\\Mercury\\AppData\\Roaming\\NuGet\\NuGet.Config" "C:\\Users\\Mercury\\AppData\\Roaming\\NuGet\\NuGet.Config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
], ],
"originalTargetFrameworks": [ "originalTargetFrameworks": [
"net8.0" "net8.0"
], ],
"sources": { "sources": {
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
"C:\\Program Files\\dotnet\\library-packs": {},
"https://api.nuget.org/v3/index.json": {} "https://api.nuget.org/v3/index.json": {}
}, },
"frameworks": { "frameworks": {
@ -487,7 +495,8 @@
"enableAudit": "true", "enableAudit": "true",
"auditLevel": "low", "auditLevel": "low",
"auditMode": "direct" "auditMode": "direct"
} },
"SdkAnalysisLevel": "9.0.100"
}, },
"frameworks": { "frameworks": {
"net8.0": { "net8.0": {
@ -518,7 +527,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Users\\Mercury\\.dotnet\\sdk\\8.0.303/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.101/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View file

@ -1,8 +1,8 @@
{ {
"version": 2, "version": 2,
"dgSpecHash": "71ChNRMXyyA=", "dgSpecHash": "Cu8IH0pDN8c=",
"success": true, "success": true,
"projectFilePath": "D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj", "projectFilePath": "E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj",
"expectedPackageFiles": [ "expectedPackageFiles": [
"C:\\Users\\Mercury\\.nuget\\packages\\microsoft.netcore.platforms\\3.1.0\\microsoft.netcore.platforms.3.1.0.nupkg.sha512", "C:\\Users\\Mercury\\.nuget\\packages\\microsoft.netcore.platforms\\3.1.0\\microsoft.netcore.platforms.3.1.0.nupkg.sha512",
"C:\\Users\\Mercury\\.nuget\\packages\\microsoft.win32.registry\\4.7.0\\microsoft.win32.registry.4.7.0.nupkg.sha512", "C:\\Users\\Mercury\\.nuget\\packages\\microsoft.win32.registry\\4.7.0\\microsoft.win32.registry.4.7.0.nupkg.sha512",

View file

@ -1 +1 @@
"restore":{"projectUniqueName":"D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj","projectName":"2dxAutoClip","projectPath":"D:\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj","outputPath":"D:\\2dxAutoClip\\2dxAutoClip\\obj\\","projectStyle":"PackageReference","originalTargetFrameworks":["net8.0"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0":{"targetAlias":"net8.0","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"}}"frameworks":{"net8.0":{"targetAlias":"net8.0","dependencies":{"NAudio":{"target":"Package","version":"[2.2.1, )"},"NAudio.Wasapi":{"target":"Package","version":"[2.2.1, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.NETCore.App":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"C:\\Users\\Mercury\\.dotnet\\sdk\\8.0.303/PortableRuntimeIdentifierGraph.json"}} "restore":{"projectUniqueName":"E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj","projectName":"2dxAutoClip","projectPath":"E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\2dxAutoClip.csproj","outputPath":"E:\\csharpcazzo\\2dxAutoClip\\2dxAutoClip\\obj\\","projectStyle":"PackageReference","fallbackFolders":["C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"],"originalTargetFrameworks":["net8.0"],"sources":{"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\":{},"C:\\Program Files\\dotnet\\library-packs":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0":{"targetAlias":"net8.0","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]}}"frameworks":{"net8.0":{"targetAlias":"net8.0","dependencies":{"NAudio":{"target":"Package","version":"[2.2.1, )"},"NAudio.Wasapi":{"target":"Package","version":"[2.2.1, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.NETCore.App":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\8.0.300/PortableRuntimeIdentifierGraph.json"}}

View file

@ -1 +1 @@
17255542395188510 17259084205730795

View file

@ -1 +1 @@
17255543032402240 17259084285638427

44
readme-ita.md Normal file
View file

@ -0,0 +1,44 @@
# 2dxAutoClip
Piccola applicazione portatile in C# (<1mb senza il binario di FFmpeg) creata per registrare e salvare clip delle tue sessioni di Beatmania IIDX. Scarica automaticamente FFMPEG al primo avvio. Sono disponibili dei binari precompilati aggiornati ogni 4-5 commit.
## Impostazioni Personalizzate:
Dalla versione 0.0.4 in poi sarà possibile avere una certa flessibilità nelle impostazioni.
Il nuovo file [prop](https://git.mercurio.moe/Mercury/2dxAutoClip/src/branch/main/2dxAutoClip/artifacts/prop.txt) consentirà all'utente di regolare finemente alcune impostazioni come la risoluzione di registrazione (Consigliamo registrare a 720p tutti le versioni precedenti a Resident), il Constant Rate Factor (CRF) che determina la qualità e la velocità della registrazione, anche se suggeriamo di mantenerlo al valore predefinito, e il framerate del video sorgente; anche in questo caso, suggeriamo di mantenerlo a 60 o 120, poiché l'output finale forzerà il framerate a 60. Questa versione consente anche all'utente di passare alla registrazione con `spice`, `inject` e `launcher` invece di `spice64` per registrare versioni più vecchie o giochi avviati da BemaniTools.
## Requisiti:
- **ALMENO Windows 10 build 20348**
- Un'installazione abbastanza recente di beatmania iidx con [TickerHook](https://github.com/Radioo/TickerHook) in esecuzione. Tutti i crediti vanno all'autore originale
- Circa 2MB per estrarre la versione precompilata dell'applicazione e 100MB una volta completato il download dei file di supporto come FFMPEG e AppLbCap
- Runtime [.net 8 (64 bit)](https://dotnet.microsoft.com/it-it/download/dotnet/thank-you/runtime-8.0.8-windows-x64-installer) per l'esecuzione, SDK [.net 8 (64 bit)](https://dotnet.microsoft.com/it-it/download/dotnet/thank-you/sdk-8.0.401-windows-x64-installer) se si desidera compilarlo autonomamente
- FFMPEG aggiunto al PATH di sistema (opzionale. Il programma scaricherà una versione precompilata di FFMPEG se non ne trova una)
- ApplicationLoopbackCapture verrà scaricato a runtime. In alternativa, puoi scaricarlo da [qui](https://git.mercurio.moe/Mercury/2dxAutoClip-AppLbHelper) e compilarlo autonomamente (richiede VS 2019+)
## Istruzioni:
Le istruzioni per l'esecuzione sono piuttosto semplici:
- Aggiungi TickerHook alla tua applicazione bootstrap
- Avvia 2dx
- Avvia il programma di clipping
- Enjoy! :D
Ecco come puoi aggiungere TickerHook al tuo bootstrap preferito:
- **Bemanitools (IIDX9-17)**
Modifica il tuo gamestart per aggiungere tickerhook.dll subito dopo iidxhook*.dll:
`inject iidxhook1.dll TickerHook.dll bm2dx.exe --config iidxhook-09.conf %*`
- **Bemanitools (IIDX18-30)**
Modifica il tuo gamestart.bat per aggiungere tickerhook.dll dopo iidxhook*.dll:
`launcher -K iidxhook6.dll bm2dx.dll --config iidxhook-20.conf %*`
- **Spicetools**
Il metodo più semplice è aggiungere "tickerhook.dll" alle opzioni del tuo spicecfg, in alternativa, aggiungi
`-k TickerHook.dll` al tuo file start.bat, assicurandoti che la dll di Ticker sia nella cartella del gioco.
## Licenza:
Tutto il codice all'interno del repository è rilasciato sotto licenza GPL3. La (https://github.com/FFmpeg/FFmpeg/blob/master/LICENSE.md) di FFMPEG è disponibile qui.

View file

@ -1,16 +1,46 @@
# 2dxAutoClip # 2dxAutoClip
**Readme italiano disponibile** [qui](https://git.mercurio.moe/Mercury/2dxAutoClip/src/branch/main/readme-ita.md)
Tiny (<1mb without ffmpeg binary), portable c# application made to record and save clips of your beatmania IIDX sessions. Ships with a prebuilt version of [FFMPEG](https://github.com/FFmpeg/FFmpeg/tree/master) for easier usage. Tiny (<1mb without ffmpeg binary), portable c# application made to record and save clips of your beatmania IIDX sessions. Downloads FFMPEG on startup.
Prebuilt binaries are available as releases, and they get updated every few commits. Prebuilt binaries are available as releases, and they get updated every few commits.
## Custom Recording Path: ## Custom Settings:
To set a custom recording path create (or edit) the prop.txt file with your new recording path. check release 0.0.2 for an example. Version 0.0.4 and onwards will allow for some flexibility in the settings.
the new [prop](https://git.mercurio.moe/Mercury/2dxAutoClip/src/branch/main/2dxAutoClip/artifacts/prop.txt) file will allwo the user to finely tune some settings such as recording resolution (older styles might be better off recorded at 720p or lower), the Constant Rate Factor (CRF) that dictates the quality and speed of the recording, although we suggest keeping it at default, and the framerate of the source video, again, we suggest keeping it at either 60 or 120 since the final export will force it back to 60.
This version also allows the user to switch to recording `spice` instead of `spice64` for the aforementioned older styles
## Requirements: ## Requirements:
- **AT LEAST Windows 10 build 20348**
- A fairly modern beatmania iidx install running [TickerHook](https://github.com/Radioo/TickerHook). All credit goes to original author - A fairly modern beatmania iidx install running [TickerHook](https://github.com/Radioo/TickerHook). All credit goes to original author
- About 150mb to extract the precompiled version of the application - About 2mb to extract the precompiled version of the application, and 100MB once it finishes downloading helper files such as FFMPEG and AppLbCap
- .net 8 if you'd rather build the app on your own - [.net 8 runtime(64 bit)](https://dotnet.microsoft.com/it-it/download/dotnet/thank-you/runtime-8.0.8-windows-x64-installer) for running, [.net8 SDK(64 bit)](https://dotnet.microsoft.com/it-it/download/dotnet/thank-you/sdk-8.0.401-windows-x64-installer) if you want to build it yourself
- FFMPEG added to path (optional. the program will download a prebuilt version of ffmpeg if it can't find one)
- ApplicationLoopbackCapture will be downloaded at runtime. you can alternatively pull it from [here](https://git.mercurio.moe/Mercury/2dxAutoClip-AppLbHelper) and build it yourself (requires VS 2019+)
## Instructions:
Instructions for running boil down pretty much to:
- add tickerhook to your boostrap application
- launch 2dx
- launch clipping program
- profit :D
Here's how you can add your tickerhook to your bootstrap of choice:
- **Bemanitools (IIDX9-17)**
modify your gamestart to add tickerhook.dll right after iidxhook*.dll:
`inject iidxhook1.dll TickerHook.dll bm2dx.exe --config iidxhook-09.conf %*`
- **Bemanitools (IIDX18-30)**
Edit your gamestart to add tickerhook.dll after iidxhook*.dll
`launcher -K iidxhook6.dll bm2dx.dll --config iidxhook-20.conf %*`
- **Spicetools**
Easiest method is to add "tickerhook.dll" to your spicecfg options, alternatively, add
`-k TickerHook.dll` to your start.bat file, making sure the ticker dll is in the game folder
## Licensing: ## Licensing: