211 lines
6.8 KiB
C++
211 lines
6.8 KiB
C++
#include "render.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <functional>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#include "log.hpp"
|
|
#include "path_.hpp"
|
|
#include "process_.hpp"
|
|
#include "sdl.hpp"
|
|
#include "string_.hpp"
|
|
|
|
using namespace anm2ed::util;
|
|
|
|
namespace anm2ed
|
|
{
|
|
namespace
|
|
{
|
|
std::string ffmpeg_concat_escape(const std::string& value)
|
|
{
|
|
std::string escaped{};
|
|
escaped.reserve(value.size());
|
|
for (auto character : value)
|
|
{
|
|
if (character == '\'') escaped += "'\\''";
|
|
else
|
|
escaped += character;
|
|
}
|
|
return escaped;
|
|
}
|
|
}
|
|
|
|
bool animation_render(const std::filesystem::path& ffmpegPath, const std::filesystem::path& path,
|
|
const std::vector<std::filesystem::path>& framePaths,
|
|
const std::vector<double>& frameDurations, AudioStream& audioStream, render::Type type, int fps)
|
|
{
|
|
if (framePaths.empty() || ffmpegPath.empty() || path.empty()) return false;
|
|
(void)frameDurations;
|
|
fps = std::max(fps, 1);
|
|
|
|
auto pathString = path::to_utf8(path);
|
|
auto ffmpegPathString = path::to_utf8(ffmpegPath);
|
|
auto loggerPath = Logger::path();
|
|
auto loggerPathString = path::to_utf8(loggerPath);
|
|
#if _WIN32
|
|
auto ffmpegTempPath = loggerPath.parent_path() / "ffmpeg_log.temp.txt";
|
|
auto ffmpegTempPathString = path::to_utf8(ffmpegTempPath);
|
|
std::error_code ffmpegTempError;
|
|
std::filesystem::remove(ffmpegTempPath, ffmpegTempError);
|
|
#endif
|
|
|
|
std::filesystem::path audioPath{};
|
|
std::string audioInputArguments{};
|
|
std::string audioOutputArguments{"-an"};
|
|
std::string command{};
|
|
auto temporaryDirectory = framePaths.front().parent_path();
|
|
if (temporaryDirectory.empty()) temporaryDirectory = std::filesystem::temp_directory_path();
|
|
|
|
auto audio_remove = [&]()
|
|
{
|
|
if (!audioPath.empty())
|
|
{
|
|
std::error_code ec;
|
|
std::filesystem::remove(audioPath, ec);
|
|
}
|
|
};
|
|
|
|
if (type != render::GIF && !audioStream.stream.empty() && audioStream.spec.freq > 0 &&
|
|
audioStream.spec.channels > 0)
|
|
{
|
|
auto tempFilenameUtf8 = std::format("anm2ed_audio_{}_{}.f32", std::hash<std::string>{}(pathString), SDL_GetTicks());
|
|
audioPath = temporaryDirectory / path::from_utf8(tempFilenameUtf8);
|
|
|
|
std::ofstream audioFile(audioPath, std::ios::binary);
|
|
|
|
if (audioFile)
|
|
{
|
|
auto data = (const char*)audioStream.stream.data();
|
|
auto byteCount = audioStream.stream.size() * sizeof(float);
|
|
audioFile.write(data, byteCount);
|
|
audioFile.close();
|
|
|
|
auto sampleRate = std::max(audioStream.spec.freq, 1);
|
|
auto channels = std::max(audioStream.spec.channels, 1);
|
|
auto audioDurationFilter = std::string{};
|
|
auto frameCount = (double)std::max((int)framePaths.size(), 1);
|
|
auto expectedDurationSeconds = frameCount / (double)fps;
|
|
if (expectedDurationSeconds > 0.0)
|
|
audioDurationFilter += std::format("apad,atrim=duration={:.9f}", expectedDurationSeconds);
|
|
|
|
audioInputArguments =
|
|
std::format("-f f32le -ar {0} -ac {1} -i \"{2}\"", sampleRate, channels, path::to_utf8(audioPath));
|
|
|
|
switch (type)
|
|
{
|
|
case render::WEBM:
|
|
audioOutputArguments = "-c:a libopus -b:a 160k -shortest";
|
|
break;
|
|
case render::MP4:
|
|
audioOutputArguments = "-c:a aac -b:a 192k -shortest";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!audioDurationFilter.empty())
|
|
audioOutputArguments = std::format("-af \"{}\" {}", audioDurationFilter, audioOutputArguments);
|
|
}
|
|
else
|
|
{
|
|
logger.warning("Failed to open temporary audio file; exporting video without audio.");
|
|
audio_remove();
|
|
}
|
|
}
|
|
|
|
auto framesListPath = temporaryDirectory / path::from_utf8(std::format(
|
|
"anm2ed_frames_{}_{}.txt", std::hash<std::string>{}(pathString), SDL_GetTicks())) ;
|
|
std::ofstream framesListFile(framesListPath);
|
|
if (!framesListFile)
|
|
{
|
|
audio_remove();
|
|
return false;
|
|
}
|
|
|
|
auto defaultFrameDuration = 1.0 / (double)fps;
|
|
for (std::size_t index = 0; index < framePaths.size(); ++index)
|
|
{
|
|
auto framePath = framePaths[index];
|
|
auto framePathString = path::to_utf8(framePath);
|
|
auto frameDuration = defaultFrameDuration;
|
|
framesListFile << "file '" << ffmpeg_concat_escape(framePathString) << "'\n";
|
|
framesListFile << "duration " << std::format("{:.9f}", frameDuration) << "\n";
|
|
}
|
|
auto lastFramePathString = path::to_utf8(framePaths.back());
|
|
framesListFile << "file '" << ffmpeg_concat_escape(lastFramePathString) << "'\n";
|
|
framesListFile.close();
|
|
|
|
auto framesListPathString = path::to_utf8(framesListPath);
|
|
command = std::format("\"{0}\" -y -f concat -safe 0 -i \"{1}\"", ffmpegPathString, framesListPathString);
|
|
|
|
if (!audioInputArguments.empty()) command += " " + audioInputArguments;
|
|
command += std::format(" -fps_mode cfr -r {}", fps);
|
|
|
|
switch (type)
|
|
{
|
|
case render::GIF:
|
|
command +=
|
|
" -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full[p];[s1][p]paletteuse=dither=floyd_steinberg\""
|
|
" -loop 0";
|
|
command += std::format(" \"{}\"", pathString);
|
|
break;
|
|
case render::WEBM:
|
|
command += " -c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 -auto-alt-ref 0";
|
|
if (!audioOutputArguments.empty()) command += " " + audioOutputArguments;
|
|
command += std::format(" \"{}\"", pathString);
|
|
break;
|
|
case render::MP4:
|
|
command += " -vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" -c:v libx265 -crf 20 -preset slow"
|
|
" -tag:v hvc1 -movflags +faststart";
|
|
if (!audioOutputArguments.empty()) command += " " + audioOutputArguments;
|
|
command += std::format(" \"{}\"", pathString);
|
|
break;
|
|
default:
|
|
{
|
|
std::error_code ec;
|
|
std::filesystem::remove(framesListPath, ec);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#if _WIN32
|
|
command = string::quote(command);
|
|
#endif
|
|
|
|
logger.command(command);
|
|
|
|
#if _WIN32
|
|
Process process(command.c_str(), "wb");
|
|
#else
|
|
Process process(command.c_str(), "w");
|
|
#endif
|
|
|
|
if (!process.get())
|
|
{
|
|
std::error_code ec;
|
|
std::filesystem::remove(framesListPath, ec);
|
|
audio_remove();
|
|
return false;
|
|
}
|
|
|
|
auto ffmpegExitCode = process.close();
|
|
if (ffmpegExitCode != 0)
|
|
{
|
|
logger.error(std::format("FFmpeg exited with code {} while exporting {}", ffmpegExitCode, pathString));
|
|
std::error_code ec;
|
|
std::filesystem::remove(framesListPath, ec);
|
|
audio_remove();
|
|
return false;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::remove(framesListPath, ec);
|
|
audio_remove();
|
|
return true;
|
|
}
|
|
}
|