fix audio desync for realsies, stderr to log.txt
This commit is contained in:
@@ -1 +1 @@
|
|||||||
/home/anon/sda/Personal/Repos/anm2ed/build/compile_commands.json
|
/home/anon/sda/Personal/Repos/anm2ed/out/build/linux-debug/compile_commands.json
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
#include "animation_preview.hpp"
|
#include "animation_preview.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
@@ -82,6 +84,51 @@ namespace anm2ed::imgui
|
|||||||
directory.clear();
|
directory.clear();
|
||||||
frames.clear();
|
frames.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool render_audio_stream_generate(AudioStream& audioStream, std::map<int, anm2::Sound>& sounds,
|
||||||
|
const std::vector<int>& frameSoundIDs, int fps)
|
||||||
|
{
|
||||||
|
audioStream.stream.clear();
|
||||||
|
if (frameSoundIDs.empty() || fps <= 0) return true;
|
||||||
|
|
||||||
|
SDL_AudioSpec mixSpec = audioStream.spec;
|
||||||
|
mixSpec.format = SDL_AUDIO_F32;
|
||||||
|
auto* mixer = MIX_CreateMixer(&mixSpec);
|
||||||
|
if (!mixer) return false;
|
||||||
|
|
||||||
|
auto channels = std::max(mixSpec.channels, 1);
|
||||||
|
auto sampleRate = std::max(mixSpec.freq, 1);
|
||||||
|
auto framesPerStep = (double)sampleRate / (double)fps;
|
||||||
|
auto sampleFrameAccumulator = 0.0;
|
||||||
|
auto frameBuffer = std::vector<float>{};
|
||||||
|
|
||||||
|
for (auto soundID : frameSoundIDs)
|
||||||
|
{
|
||||||
|
if (soundID != -1 && sounds.contains(soundID)) sounds.at(soundID).audio.play(false, mixer);
|
||||||
|
|
||||||
|
sampleFrameAccumulator += framesPerStep;
|
||||||
|
auto sampleFramesToGenerate = (int)std::floor(sampleFrameAccumulator);
|
||||||
|
sampleFramesToGenerate = std::max(sampleFramesToGenerate, 1);
|
||||||
|
sampleFrameAccumulator -= (double)sampleFramesToGenerate;
|
||||||
|
|
||||||
|
frameBuffer.resize((std::size_t)sampleFramesToGenerate * (std::size_t)channels);
|
||||||
|
if (!MIX_Generate(mixer, frameBuffer.data(), (int)(frameBuffer.size() * sizeof(float))))
|
||||||
|
{
|
||||||
|
for (auto& [_, sound] : sounds)
|
||||||
|
sound.audio.track_detach(mixer);
|
||||||
|
MIX_DestroyMixer(mixer);
|
||||||
|
audioStream.stream.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
audioStream.stream.insert(audioStream.stream.end(), frameBuffer.begin(), frameBuffer.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [_, sound] : sounds)
|
||||||
|
sound.audio.track_detach(mixer);
|
||||||
|
MIX_DestroyMixer(mixer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
||||||
@@ -106,6 +153,8 @@ namespace anm2ed::imgui
|
|||||||
|
|
||||||
if (playback.time > end || playback.isFinished)
|
if (playback.time > end || playback.isFinished)
|
||||||
{
|
{
|
||||||
|
if (settings.timelineIsSound) audioStream.capture_end(mixer);
|
||||||
|
|
||||||
if (type == render::PNGS)
|
if (type == render::PNGS)
|
||||||
{
|
{
|
||||||
if (!renderTempFrames.empty())
|
if (!renderTempFrames.empty())
|
||||||
@@ -185,7 +234,20 @@ namespace anm2ed::imgui
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (animation_render(ffmpegPath, path, renderTempFrames, audioStream, (render::Type)type, anm2.info.fps))
|
if (settings.timelineIsSound && type != render::GIF)
|
||||||
|
{
|
||||||
|
if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, anm2.info.fps))
|
||||||
|
{
|
||||||
|
toasts.push(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED));
|
||||||
|
logger.error("Failed to generate deterministic render audio stream; exporting without audio.");
|
||||||
|
audioStream.stream.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
audioStream.stream.clear();
|
||||||
|
|
||||||
|
if (animation_render(ffmpegPath, path, renderTempFrames, renderTempFrameDurations, audioStream,
|
||||||
|
(render::Type)type, anm2.info.fps))
|
||||||
{
|
{
|
||||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString)));
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString)));
|
||||||
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH),
|
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH),
|
||||||
@@ -204,9 +266,15 @@ namespace anm2ed::imgui
|
|||||||
{
|
{
|
||||||
renderTempDirectory.clear();
|
renderTempDirectory.clear();
|
||||||
renderTempFrames.clear();
|
renderTempFrames.clear();
|
||||||
|
renderTempFrameDurations.clear();
|
||||||
|
renderFrameSoundIDs.clear();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||||
|
renderTempFrameDurations.clear();
|
||||||
|
renderFrameSoundIDs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (settings.renderIsRawAnimation)
|
if (settings.renderIsRawAnimation)
|
||||||
{
|
{
|
||||||
@@ -220,8 +288,6 @@ namespace anm2ed::imgui
|
|||||||
isCheckerPanInitialized = false;
|
isCheckerPanInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.timelineIsSound) audioStream.capture_end(mixer);
|
|
||||||
|
|
||||||
playback.isPlaying = false;
|
playback.isPlaying = false;
|
||||||
playback.isFinished = false;
|
playback.isFinished = false;
|
||||||
manager.isRecording = false;
|
manager.isRecording = false;
|
||||||
@@ -229,6 +295,28 @@ namespace anm2ed::imgui
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (settings.timelineIsSound && renderTempFrames.empty()) audioStream.capture_begin(mixer);
|
||||||
|
auto frameSoundID = -1;
|
||||||
|
if (settings.timelineIsSound && !anm2.content.sounds.empty())
|
||||||
|
{
|
||||||
|
if (auto animation = document.animation_get();
|
||||||
|
animation && animation->triggers.isVisible && (!settings.timelineIsOnlyShowLayers || manager.isRecording))
|
||||||
|
{
|
||||||
|
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible)
|
||||||
|
{
|
||||||
|
if (!trigger.soundIDs.empty())
|
||||||
|
{
|
||||||
|
auto soundIndex = trigger.soundIDs.size() > 1
|
||||||
|
? (size_t)math::random_in_range(0.0f, (float)trigger.soundIDs.size())
|
||||||
|
: (size_t)0;
|
||||||
|
soundIndex = std::min(soundIndex, trigger.soundIDs.size() - 1);
|
||||||
|
auto soundID = trigger.soundIDs[soundIndex];
|
||||||
|
if (anm2.content.sounds.contains(soundID)) frameSoundID = soundID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderFrameSoundIDs.push_back(frameSoundID);
|
||||||
|
|
||||||
bind();
|
bind();
|
||||||
auto pixels = pixels_get();
|
auto pixels = pixels_get();
|
||||||
@@ -237,6 +325,24 @@ namespace anm2ed::imgui
|
|||||||
if (Texture::write_pixels_png(framePath, size, pixels.data()))
|
if (Texture::write_pixels_png(framePath, size, pixels.data()))
|
||||||
{
|
{
|
||||||
renderTempFrames.push_back(framePath);
|
renderTempFrames.push_back(framePath);
|
||||||
|
auto nowCounter = SDL_GetPerformanceCounter();
|
||||||
|
auto counterFrequency = SDL_GetPerformanceFrequency();
|
||||||
|
auto fallbackDuration = 1.0 / (double)std::max(anm2.info.fps, 1);
|
||||||
|
|
||||||
|
if (renderTempFrames.size() == 1)
|
||||||
|
{
|
||||||
|
renderCaptureCounterPrev = nowCounter;
|
||||||
|
renderTempFrameDurations.push_back(fallbackDuration);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto elapsedCounter = nowCounter - renderCaptureCounterPrev;
|
||||||
|
auto frameDuration = counterFrequency > 0 ? (double)elapsedCounter / (double)counterFrequency : 0.0;
|
||||||
|
frameDuration = std::max(frameDuration, 1.0 / 1000.0);
|
||||||
|
renderTempFrameDurations.back() = frameDuration;
|
||||||
|
renderTempFrameDurations.push_back(frameDuration);
|
||||||
|
renderCaptureCounterPrev = nowCounter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -244,6 +350,8 @@ namespace anm2ed::imgui
|
|||||||
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
||||||
std::make_format_args(pathString)));
|
std::make_format_args(pathString)));
|
||||||
if (type != render::PNGS) render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
if (type != render::PNGS) render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||||
|
renderTempFrameDurations.clear();
|
||||||
|
renderFrameSoundIDs.clear();
|
||||||
playback.isPlaying = false;
|
playback.isPlaying = false;
|
||||||
playback.isFinished = false;
|
playback.isFinished = false;
|
||||||
manager.isRecording = false;
|
manager.isRecording = false;
|
||||||
@@ -258,7 +366,7 @@ namespace anm2ed::imgui
|
|||||||
auto& isSound = settings.timelineIsSound;
|
auto& isSound = settings.timelineIsSound;
|
||||||
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
|
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
|
||||||
|
|
||||||
if (!anm2.content.sounds.empty() && isSound)
|
if (!manager.isRecording && !anm2.content.sounds.empty() && isSound)
|
||||||
{
|
{
|
||||||
if (auto animation = document.animation_get();
|
if (auto animation = document.animation_get();
|
||||||
animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording))
|
animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording))
|
||||||
@@ -474,8 +582,6 @@ namespace anm2ed::imgui
|
|||||||
{
|
{
|
||||||
savedSettings = settings;
|
savedSettings = settings;
|
||||||
|
|
||||||
if (settings.timelineIsSound) audioStream.capture_begin(mixer);
|
|
||||||
|
|
||||||
if (settings.renderIsRawAnimation)
|
if (settings.renderIsRawAnimation)
|
||||||
{
|
{
|
||||||
settings.previewBackgroundColor = vec4();
|
settings.previewBackgroundColor = vec4();
|
||||||
@@ -502,6 +608,9 @@ namespace anm2ed::imgui
|
|||||||
manager.isRecordingStart = false;
|
manager.isRecordingStart = false;
|
||||||
manager.isRecording = true;
|
manager.isRecording = true;
|
||||||
renderTempFrames.clear();
|
renderTempFrames.clear();
|
||||||
|
renderTempFrameDurations.clear();
|
||||||
|
renderFrameSoundIDs.clear();
|
||||||
|
renderCaptureCounterPrev = 0;
|
||||||
if (settings.renderType == render::PNGS)
|
if (settings.renderType == render::PNGS)
|
||||||
{
|
{
|
||||||
renderTempDirectory = settings.renderPath;
|
renderTempDirectory = settings.renderPath;
|
||||||
@@ -1033,9 +1142,13 @@ namespace anm2ed::imgui
|
|||||||
{
|
{
|
||||||
renderTempDirectory.clear();
|
renderTempDirectory.clear();
|
||||||
renderTempFrames.clear();
|
renderTempFrames.clear();
|
||||||
|
renderTempFrameDurations.clear();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||||
|
renderTempFrameDurations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pan = savedPan;
|
pan = savedPan;
|
||||||
zoom = savedZoom;
|
zoom = savedZoom;
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ namespace anm2ed::imgui
|
|||||||
glm::vec2 moveOffset{};
|
glm::vec2 moveOffset{};
|
||||||
std::filesystem::path renderTempDirectory{};
|
std::filesystem::path renderTempDirectory{};
|
||||||
std::vector<std::filesystem::path> renderTempFrames{};
|
std::vector<std::filesystem::path> renderTempFrames{};
|
||||||
|
std::vector<double> renderTempFrameDurations{};
|
||||||
|
std::vector<int> renderFrameSoundIDs{};
|
||||||
|
Uint64 renderCaptureCounterPrev{};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AnimationPreview();
|
AnimationPreview();
|
||||||
|
|||||||
111
src/log.cpp
111
src/log.cpp
@@ -1,16 +1,26 @@
|
|||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <print>
|
#include <print>
|
||||||
|
|
||||||
|
#include "path_.hpp"
|
||||||
#include "sdl.hpp"
|
#include "sdl.hpp"
|
||||||
#include "time_.hpp"
|
#include "time_.hpp"
|
||||||
|
|
||||||
|
#if _WIN32
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <io.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace anm2ed::util;
|
using namespace anm2ed::util;
|
||||||
|
|
||||||
namespace anm2ed
|
namespace anm2ed
|
||||||
{
|
{
|
||||||
void Logger::write_raw(const std::string& message)
|
void Logger::write_raw(const std::string& message)
|
||||||
{
|
{
|
||||||
|
std::lock_guard lock(mutex);
|
||||||
std::println("{}", message);
|
std::println("{}", message);
|
||||||
if (file.is_open()) file << message << '\n' << std::flush;
|
if (file.is_open()) file << message << '\n' << std::flush;
|
||||||
}
|
}
|
||||||
@@ -26,7 +36,105 @@ namespace anm2ed
|
|||||||
void Logger::error(const std::string& message) { write(ERROR, message); }
|
void Logger::error(const std::string& message) { write(ERROR, message); }
|
||||||
void Logger::fatal(const std::string& message) { write(FATAL, message); }
|
void Logger::fatal(const std::string& message) { write(FATAL, message); }
|
||||||
void Logger::command(const std::string& message) { write(COMMAND, message); }
|
void Logger::command(const std::string& message) { write(COMMAND, message); }
|
||||||
void Logger::open(const std::filesystem::path& path) { file.open(path, std::ios::out | std::ios::app); }
|
|
||||||
|
void Logger::stderr_pump()
|
||||||
|
{
|
||||||
|
std::string pending{};
|
||||||
|
std::array<char, 512> buffer{};
|
||||||
|
while (isStderrRedirecting)
|
||||||
|
{
|
||||||
|
int readBytes{};
|
||||||
|
#if _WIN32
|
||||||
|
readBytes = _read(stderrPipeReadFd, buffer.data(), (unsigned int)buffer.size());
|
||||||
|
#else
|
||||||
|
readBytes = (int)read(stderrPipeReadFd, buffer.data(), buffer.size());
|
||||||
|
#endif
|
||||||
|
if (readBytes <= 0) break;
|
||||||
|
|
||||||
|
pending.append(buffer.data(), (std::size_t)readBytes);
|
||||||
|
|
||||||
|
std::size_t lineEnd{};
|
||||||
|
while ((lineEnd = pending.find('\n')) != std::string::npos)
|
||||||
|
{
|
||||||
|
auto line = pending.substr(0, lineEnd);
|
||||||
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||||
|
if (!line.empty()) write_raw(line);
|
||||||
|
pending.erase(0, lineEnd + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pending.empty()) write_raw(pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::stderr_redirect_start()
|
||||||
|
{
|
||||||
|
if (isStderrRedirecting) return;
|
||||||
|
|
||||||
|
int pipeFds[2]{-1, -1};
|
||||||
|
#if _WIN32
|
||||||
|
if (_pipe(pipeFds, 4096, _O_BINARY) != 0) return;
|
||||||
|
stderrOriginalFd = _dup(_fileno(stderr));
|
||||||
|
if (stderrOriginalFd < 0 || _dup2(pipeFds[1], _fileno(stderr)) != 0)
|
||||||
|
{
|
||||||
|
_close(pipeFds[0]);
|
||||||
|
_close(pipeFds[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_close(pipeFds[1]);
|
||||||
|
#else
|
||||||
|
if (pipe(pipeFds) != 0) return;
|
||||||
|
stderrOriginalFd = dup(fileno(stderr));
|
||||||
|
if (stderrOriginalFd < 0 || dup2(pipeFds[1], fileno(stderr)) < 0)
|
||||||
|
{
|
||||||
|
close(pipeFds[0]);
|
||||||
|
close(pipeFds[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close(pipeFds[1]);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::setvbuf(stderr, nullptr, _IONBF, 0);
|
||||||
|
|
||||||
|
stderrPipeReadFd = pipeFds[0];
|
||||||
|
isStderrRedirecting = true;
|
||||||
|
stderrThread = std::thread([this]() { stderr_pump(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::stderr_redirect_stop()
|
||||||
|
{
|
||||||
|
if (!isStderrRedirecting) return;
|
||||||
|
isStderrRedirecting = false;
|
||||||
|
|
||||||
|
if (stderrOriginalFd >= 0)
|
||||||
|
{
|
||||||
|
#if _WIN32
|
||||||
|
_dup2(stderrOriginalFd, _fileno(stderr));
|
||||||
|
_close(stderrOriginalFd);
|
||||||
|
#else
|
||||||
|
dup2(stderrOriginalFd, fileno(stderr));
|
||||||
|
close(stderrOriginalFd);
|
||||||
|
#endif
|
||||||
|
stderrOriginalFd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderrPipeReadFd >= 0)
|
||||||
|
{
|
||||||
|
#if _WIN32
|
||||||
|
_close(stderrPipeReadFd);
|
||||||
|
#else
|
||||||
|
close(stderrPipeReadFd);
|
||||||
|
#endif
|
||||||
|
stderrPipeReadFd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderrThread.joinable()) stderrThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::open(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
file.open(path, std::ios::out | std::ios::app);
|
||||||
|
stderr_redirect_start();
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path Logger::path() { return sdl::preferences_directory_get() / "log.txt"; }
|
std::filesystem::path Logger::path() { return sdl::preferences_directory_get() / "log.txt"; }
|
||||||
|
|
||||||
@@ -39,6 +147,7 @@ namespace anm2ed
|
|||||||
Logger::~Logger()
|
Logger::~Logger()
|
||||||
{
|
{
|
||||||
info("Exiting Anm2Ed");
|
info("Exiting Anm2Ed");
|
||||||
|
stderr_redirect_stop();
|
||||||
if (file.is_open()) file.close();
|
if (file.is_open()) file.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/log.hpp
12
src/log.hpp
@@ -4,6 +4,8 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
namespace anm2ed
|
namespace anm2ed
|
||||||
{
|
{
|
||||||
@@ -44,6 +46,16 @@ namespace anm2ed
|
|||||||
class Logger
|
class Logger
|
||||||
{
|
{
|
||||||
std::ofstream file{};
|
std::ofstream file{};
|
||||||
|
std::mutex mutex{};
|
||||||
|
std::thread stderrThread{};
|
||||||
|
bool isStderrRedirecting{};
|
||||||
|
int stderrPipeReadFd{-1};
|
||||||
|
int stderrPipeWriteFd{-1};
|
||||||
|
int stderrOriginalFd{-1};
|
||||||
|
|
||||||
|
void stderr_redirect_start();
|
||||||
|
void stderr_redirect_stop();
|
||||||
|
void stderr_pump();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static std::filesystem::path path();
|
static std::filesystem::path path();
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ namespace anm2ed
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool animation_render(const std::filesystem::path& ffmpegPath, const std::filesystem::path& path,
|
bool animation_render(const std::filesystem::path& ffmpegPath, const std::filesystem::path& path,
|
||||||
const std::vector<std::filesystem::path>& framePaths, AudioStream& audioStream,
|
const std::vector<std::filesystem::path>& framePaths,
|
||||||
render::Type type, int fps)
|
const std::vector<double>& frameDurations, AudioStream& audioStream, render::Type type, int fps)
|
||||||
{
|
{
|
||||||
if (framePaths.empty() || ffmpegPath.empty() || path.empty()) return false;
|
if (framePaths.empty() || ffmpegPath.empty() || path.empty()) return false;
|
||||||
|
(void)frameDurations;
|
||||||
fps = std::max(fps, 1);
|
fps = std::max(fps, 1);
|
||||||
|
|
||||||
auto pathString = path::to_utf8(path);
|
auto pathString = path::to_utf8(path);
|
||||||
@@ -83,8 +84,16 @@ namespace anm2ed
|
|||||||
audioFile.write(data, byteCount);
|
audioFile.write(data, byteCount);
|
||||||
audioFile.close();
|
audioFile.close();
|
||||||
|
|
||||||
audioInputArguments = std::format("-f f32le -ar {0} -ac {1} -i \"{2}\"", audioStream.spec.freq,
|
auto sampleRate = std::max(audioStream.spec.freq, 1);
|
||||||
audioStream.spec.channels, path::to_utf8(audioPath));
|
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)
|
switch (type)
|
||||||
{
|
{
|
||||||
@@ -97,6 +106,9 @@ namespace anm2ed
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!audioDurationFilter.empty())
|
||||||
|
audioOutputArguments = std::format("-af \"{}\" {}", audioDurationFilter, audioOutputArguments);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -114,10 +126,12 @@ namespace anm2ed
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto frameDuration = 1.0 / (double)fps;
|
auto defaultFrameDuration = 1.0 / (double)fps;
|
||||||
for (const auto& framePath : framePaths)
|
for (std::size_t index = 0; index < framePaths.size(); ++index)
|
||||||
{
|
{
|
||||||
|
auto framePath = framePaths[index];
|
||||||
auto framePathString = path::to_utf8(framePath);
|
auto framePathString = path::to_utf8(framePath);
|
||||||
|
auto frameDuration = defaultFrameDuration;
|
||||||
framesListFile << "file '" << ffmpeg_concat_escape(framePathString) << "'\n";
|
framesListFile << "file '" << ffmpeg_concat_escape(framePathString) << "'\n";
|
||||||
framesListFile << "duration " << std::format("{:.9f}", frameDuration) << "\n";
|
framesListFile << "duration " << std::format("{:.9f}", frameDuration) << "\n";
|
||||||
}
|
}
|
||||||
@@ -127,9 +141,9 @@ namespace anm2ed
|
|||||||
|
|
||||||
auto framesListPathString = path::to_utf8(framesListPath);
|
auto framesListPathString = path::to_utf8(framesListPath);
|
||||||
command = std::format("\"{0}\" -y -f concat -safe 0 -i \"{1}\"", ffmpegPathString, framesListPathString);
|
command = std::format("\"{0}\" -y -f concat -safe 0 -i \"{1}\"", ffmpegPathString, framesListPathString);
|
||||||
command += std::format(" -fps_mode cfr -r {}", fps);
|
|
||||||
|
|
||||||
if (!audioInputArguments.empty()) command += " " + audioInputArguments;
|
if (!audioInputArguments.empty()) command += " " + audioInputArguments;
|
||||||
|
command += std::format(" -fps_mode cfr -r {}", fps);
|
||||||
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,5 +37,6 @@ namespace anm2ed
|
|||||||
{
|
{
|
||||||
std::filesystem::path ffmpeg_log_path();
|
std::filesystem::path ffmpeg_log_path();
|
||||||
bool animation_render(const std::filesystem::path&, const std::filesystem::path&,
|
bool animation_render(const std::filesystem::path&, const std::filesystem::path&,
|
||||||
const std::vector<std::filesystem::path>&, AudioStream&, render::Type, int);
|
const std::vector<std::filesystem::path>&, const std::vector<double>&, AudioStream&,
|
||||||
|
render::Type, int);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,14 @@ namespace anm2ed::resource
|
|||||||
MIX_StopTrack(track, 0);
|
MIX_StopTrack(track, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Audio::track_detach(MIX_Mixer* mixer)
|
||||||
|
{
|
||||||
|
if (!track) return;
|
||||||
|
if (mixer && MIX_GetTrackMixer(track) != mixer) return;
|
||||||
|
MIX_DestroyTrack(track);
|
||||||
|
track = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); }
|
bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); }
|
||||||
|
|
||||||
Audio::Audio(const Audio& other)
|
Audio::Audio(const Audio& other)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace anm2ed::resource
|
|||||||
bool is_valid();
|
bool is_valid();
|
||||||
void play(bool loop = false, MIX_Mixer* = nullptr);
|
void play(bool loop = false, MIX_Mixer* = nullptr);
|
||||||
void stop(MIX_Mixer* = nullptr);
|
void stop(MIX_Mixer* = nullptr);
|
||||||
|
void track_detach(MIX_Mixer* = nullptr);
|
||||||
bool is_playing() const;
|
bool is_playing() const;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "audio_stream.hpp"
|
#include "audio_stream.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#if defined(__clang__) || defined(__GNUC__)
|
#if defined(__clang__) || defined(__GNUC__)
|
||||||
#pragma GCC diagnostic push
|
#pragma GCC diagnostic push
|
||||||
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
||||||
@@ -10,16 +12,46 @@ namespace anm2ed
|
|||||||
void AudioStream::callback(void* userData, MIX_Mixer* mixer, const SDL_AudioSpec* spec, float* pcm, int samples)
|
void AudioStream::callback(void* userData, MIX_Mixer* mixer, const SDL_AudioSpec* spec, float* pcm, int samples)
|
||||||
{
|
{
|
||||||
auto self = (AudioStream*)userData;
|
auto self = (AudioStream*)userData;
|
||||||
|
if (!self->isFirstCallbackCaptured)
|
||||||
|
{
|
||||||
|
self->firstCallbackCounter = SDL_GetPerformanceCounter();
|
||||||
|
self->isFirstCallbackCaptured = true;
|
||||||
|
}
|
||||||
|
self->callbackSamples = samples;
|
||||||
self->stream.insert(self->stream.end(), pcm, pcm + samples);
|
self->stream.insert(self->stream.end(), pcm, pcm + samples);
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioStream::AudioStream(MIX_Mixer* mixer) { MIX_GetMixerFormat(mixer, &spec); }
|
AudioStream::AudioStream(MIX_Mixer* mixer) { MIX_GetMixerFormat(mixer, &spec); }
|
||||||
|
|
||||||
void AudioStream::capture_begin(MIX_Mixer* mixer) { MIX_SetPostMixCallback(mixer, callback, this); }
|
void AudioStream::capture_begin(MIX_Mixer* mixer)
|
||||||
|
{
|
||||||
|
stream.clear();
|
||||||
|
callbackSamples = 0;
|
||||||
|
captureStartCounter = SDL_GetPerformanceCounter();
|
||||||
|
firstCallbackCounter = 0;
|
||||||
|
isFirstCallbackCaptured = false;
|
||||||
|
MIX_SetPostMixCallback(mixer, callback, this);
|
||||||
|
}
|
||||||
|
|
||||||
void AudioStream::capture_end(MIX_Mixer* mixer)
|
void AudioStream::capture_end(MIX_Mixer* mixer)
|
||||||
{
|
{
|
||||||
MIX_SetPostMixCallback(mixer, nullptr, this);
|
MIX_SetPostMixCallback(mixer, nullptr, this);
|
||||||
stream.clear();
|
stream.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double AudioStream::callback_latency_seconds_get() const
|
||||||
|
{
|
||||||
|
auto freq = std::max(spec.freq, 1);
|
||||||
|
auto channels = std::max(spec.channels, 1);
|
||||||
|
auto framesPerCallback = (double)callbackSamples / (double)channels;
|
||||||
|
return framesPerCallback / (double)freq;
|
||||||
|
}
|
||||||
|
|
||||||
|
double AudioStream::capture_start_delay_seconds_get() const
|
||||||
|
{
|
||||||
|
if (!isFirstCallbackCaptured || captureStartCounter == 0 || firstCallbackCounter < captureStartCounter) return 0.0;
|
||||||
|
auto frequency = SDL_GetPerformanceFrequency();
|
||||||
|
if (frequency == 0) return 0.0;
|
||||||
|
return (double)(firstCallbackCounter - captureStartCounter) / (double)frequency;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,15 @@ namespace anm2ed
|
|||||||
public:
|
public:
|
||||||
std::vector<float> stream{};
|
std::vector<float> stream{};
|
||||||
SDL_AudioSpec spec{};
|
SDL_AudioSpec spec{};
|
||||||
|
int callbackSamples{};
|
||||||
|
Uint64 captureStartCounter{};
|
||||||
|
Uint64 firstCallbackCounter{};
|
||||||
|
bool isFirstCallbackCaptured{};
|
||||||
|
|
||||||
AudioStream(MIX_Mixer*);
|
AudioStream(MIX_Mixer*);
|
||||||
void capture_begin(MIX_Mixer*);
|
void capture_begin(MIX_Mixer*);
|
||||||
void capture_end(MIX_Mixer*);
|
void capture_end(MIX_Mixer*);
|
||||||
|
double callback_latency_seconds_get() const;
|
||||||
|
double capture_start_delay_seconds_get() const;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -159,6 +159,31 @@ namespace anm2ed
|
|||||||
{
|
{
|
||||||
auto currentTick = SDL_GetTicks();
|
auto currentTick = SDL_GetTicks();
|
||||||
auto currentUpdate = SDL_GetTicks();
|
auto currentUpdate = SDL_GetTicks();
|
||||||
|
auto isRecording = manager.isRecording;
|
||||||
|
auto tickIntervalMs = (double)TICK_INTERVAL;
|
||||||
|
|
||||||
|
if (isRecording)
|
||||||
|
{
|
||||||
|
if (auto document = manager.get())
|
||||||
|
{
|
||||||
|
auto fps = std::max(document->anm2.info.fps, 1);
|
||||||
|
tickIntervalMs = std::max(1.0, 1000.0 / (double)fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecording != wasRecording)
|
||||||
|
{
|
||||||
|
// Drop any accumulated backlog when entering/leaving recording mode.
|
||||||
|
tickAccumulatorMs = 0.0;
|
||||||
|
previousTick = currentTick;
|
||||||
|
wasRecording = isRecording;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousTick == 0) previousTick = currentTick;
|
||||||
|
auto tickDeltaMs = currentTick - previousTick;
|
||||||
|
tickDeltaMs = std::min<Uint64>(tickDeltaMs, 250);
|
||||||
|
tickAccumulatorMs += (double)tickDeltaMs;
|
||||||
|
previousTick = currentTick;
|
||||||
|
|
||||||
if (currentUpdate - previousUpdate >= UPDATE_INTERVAL)
|
if (currentUpdate - previousUpdate >= UPDATE_INTERVAL)
|
||||||
{
|
{
|
||||||
@@ -167,10 +192,10 @@ namespace anm2ed
|
|||||||
previousUpdate = currentUpdate;
|
previousUpdate = currentUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTick - previousTick >= TICK_INTERVAL)
|
if (tickAccumulatorMs >= tickIntervalMs)
|
||||||
{
|
{
|
||||||
tick(settings);
|
tick(settings);
|
||||||
previousTick = currentTick;
|
tickAccumulatorMs -= tickIntervalMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Delay(1);
|
SDL_Delay(1);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace anm2ed
|
|||||||
|
|
||||||
uint64_t previousTick{};
|
uint64_t previousTick{};
|
||||||
uint64_t previousUpdate{};
|
uint64_t previousUpdate{};
|
||||||
|
double tickAccumulatorMs{};
|
||||||
|
bool wasRecording{};
|
||||||
|
|
||||||
State(SDL_Window*&, Settings& settings, std::vector<std::string>&);
|
State(SDL_Window*&, Settings& settings, std::vector<std::string>&);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user