diff --git a/build-win.bat b/build-win.bat new file mode 100644 index 0000000..4c5317d --- /dev/null +++ b/build-win.bat @@ -0,0 +1,59 @@ +@echo off +rem ===================================================================== +rem build-win.bat - Build the project on Windows (MSYS2 UCRT64 + MinGW). +rem +rem Builds build\hatsune-miku-shoshitsu.exe and build\hatsune-miku-demo.exe +rem +rem Requirements (install once with MSYS2's pacman, using a proxy if needed): +rem pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gcc-libs ^ +rem mingw-w64-ucrt-x86_64-ffmpeg mingw-w64-ucrt-x86_64-cmake +rem +rem The FFmpeg dev libraries must be installed together with the gcc +rem toolchain (FFmpeg 9 needs newer gcc-libs than a stock older gcc). +rem ===================================================================== +setlocal EnableDelayedExpansion + +set "SCRIPT_DIR=%~dp0" +cd /d "%SCRIPT_DIR%" + +rem ---- locate the MSYS2 UCRT64 toolchain ---------------------------------- +rem (note: an inherited MSYS2_ROOT env var is respected; don't pre-set it here +rem to empty, or "if not defined" below would never match) +if not defined MSYS2_ROOT if exist "D:\SDKs\msys64\ucrt64\bin\g++.exe" set "MSYS2_ROOT=D:\SDKs\msys64" +if not defined MSYS2_ROOT if exist "C:\msys64\ucrt64\bin\g++.exe" set "MSYS2_ROOT=C:\msys64" +if not defined MSYS2_ROOT ( + echo [build-win] MSYS2 UCRT64 not found. + echo Set the MSYS2_ROOT environment variable, e.g.: + echo set MSYS2_ROOT=D:\SDK\msys64 + exit /b 1 +) +set "PATH=%MSYS2_ROOT%\ucrt64\bin;%PATH%" +echo [build-win] MSYS2 UCRT64 : %MSYS2_ROOT% + +rem ---- sanity checks -------------------------------------------------------- +where cmake.exe >nul 2>&1 || ( + echo [build-win] cmake not found. Install mingw-w64-ucrt-x86_64-cmake. + exit /b 1 +) +pkg-config --exists libavformat || ( + echo [build-win] FFmpeg dev libraries not found. + echo Install mingw-w64-ucrt-x86_64-ffmpeg. + exit /b 1 +) + +rem ---- configure + build ---------------------------------------------------- +cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release +if errorlevel 1 exit /b 1 + +cmake --build build -j +if errorlevel 1 exit /b 1 + +echo. +echo [build-win] Done. +echo %SCRIPT_DIR%build\hatsune-miku-shoshitsu.exe +echo %SCRIPT_DIR%build\hatsune-miku-demo.exe +echo. +echo Run with the MSYS2 UCRT64 bin dir on PATH so the FFmpeg DLLs resolve: +echo set PATH=%MSYS2_ROOT%\ucrt64\bin;%%PATH%% +echo build\hatsune-miku-shoshitsu.exe --video ^ --audio ^ --lyrics ^ +exit /b 0 diff --git a/modules/backgroundDisplay.cpp b/modules/backgroundDisplay.cpp index d070c63..8004441 100644 --- a/modules/backgroundDisplay.cpp +++ b/modules/backgroundDisplay.cpp @@ -157,27 +157,35 @@ struct RgbFrame { std::vector rgb; // width * height * 3, tightly packed RGB24 }; -// Bounded queue that always keeps the newest frame. pushLatest() never blocks -// (drops the oldest), so the producer can't deadlock against the consumer. -class BoundedQueue { +// Bounded FIFO of decoded frames. The producer BLOCKS while the queue is full +// (backpressure), so it can never run far ahead of the consumer; the consumer +// pops frames in decode order and plays them at the timeline pace. Without +// this, a decoder that is faster than real time would let the renderer keep +// showing the "latest" frame and the video would fast-forward / loop-jump. +class FrameQueue { public: - explicit BoundedQueue(size_t capacity) : capacity_(capacity) {} + explicit FrameQueue(size_t capacity) : capacity_(capacity) {} - void pushLatest(RgbFrame frame) { - std::lock_guard lock(mutex_); - while (queue_.size() >= capacity_) queue_.pop_front(); + // Producer: blocks while full. Returns false if the queue was closed + // (playback ended), telling the decoder to stop. + bool push(RgbFrame frame) { + std::unique_lock lock(mutex_); + cv_.wait(lock, [this] { return queue_.size() < capacity_ || closed_; }); + if (closed_) return false; queue_.push_back(std::move(frame)); - cv_.notify_one(); + cv_.notify_all(); + return true; } - // Wait up to `deadline` for the newest frame. Returns false if none - // arrived by then. Drains older frames (only the newest is kept). - bool popLatestUntil(std::chrono::steady_clock::time_point deadline, RgbFrame& out) { + // Consumer: wait up to `deadline` for the next frame (oldest first). + // Returns false if none arrived by then. + bool popUntil(std::chrono::steady_clock::time_point deadline, RgbFrame& out) { std::unique_lock lock(mutex_); - cv_.wait_until(lock, deadline, [this] { return !queue_.empty() || finished_; }); + cv_.wait_until(lock, deadline, [this] { return !queue_.empty() || finished_ || closed_; }); if (queue_.empty()) return false; - out = std::move(queue_.back()); - queue_.clear(); + out = std::move(queue_.front()); + queue_.pop_front(); + cv_.notify_all(); return true; } @@ -192,12 +200,21 @@ public: cv_.notify_all(); } + // Unblock a waiting producer when playback is ending (called by the + // consumer right before it joins the decoder thread). + void close() { + std::lock_guard lock(mutex_); + closed_ = true; + cv_.notify_all(); + } + private: mutable std::mutex mutex_; std::condition_variable cv_; std::deque queue_; size_t capacity_; bool finished_ = false; + bool closed_ = false; }; struct DecodeContext { @@ -218,8 +235,13 @@ struct DecodeContext { AVFrame* frame = nullptr; AVPacket* pkt = nullptr; + // Discard this many leading frames so playback starts at the timeline's + // current position (otherwise the video would lag the audio by the setup + // time spent probing the file and sizing the terminal). + int skipFrames = 0; + bool loop = false; - BoundedQueue* queue = nullptr; + FrameQueue* queue = nullptr; const std::atomic* stop = nullptr; std::atomic* finished = nullptr; }; @@ -292,14 +314,21 @@ int setupScaler(DecodeContext& dc, int dstW, int dstH) { return (dc.frame && dc.pkt) ? 0 : -1; } -void scaleAndPush(DecodeContext& dc) { +// Scale the current frame and push it onto the queue. Skips the first +// `skipFrames` frames (start-sync). Returns false when the queue was closed, +// telling the decoder to stop. +bool scaleAndPush(DecodeContext& dc) { + if (dc.skipFrames > 0) { + --dc.skipFrames; // discard without scaling + return true; + } sws_scale(dc.sws, dc.frame->data, dc.frame->linesize, 0, dc.frame->height, dc.dstData, dc.dstLinesize); RgbFrame frame; frame.width = dc.dstW; frame.height = dc.dstH; frame.rgb.assign(dc.dstBuf.begin(), dc.dstBuf.end()); - dc.queue->pushLatest(std::move(frame)); + return dc.queue->push(std::move(frame)); } void decoderThreadFn(DecodeContext& dc) { @@ -308,7 +337,10 @@ void decoderThreadFn(DecodeContext& dc) { if (r == AVERROR_EOF) { // Flush whatever is buffered in the decoder. avcodec_send_packet(dc.codec, nullptr); - while (avcodec_receive_frame(dc.codec, dc.frame) == 0) scaleAndPush(dc); + bool ok = true; + while (ok && avcodec_receive_frame(dc.codec, dc.frame) == 0) + ok = scaleAndPush(dc); + if (!ok) break; if (dc.loop && !(dc.stop && dc.stop->load())) { av_seek_frame(dc.fmt, -1, 0, AVSEEK_FLAG_BACKWARD); @@ -320,7 +352,10 @@ void decoderThreadFn(DecodeContext& dc) { if (r < 0) break; // non-EOF read error if (dc.pkt->stream_index == dc.videoIndex) { avcodec_send_packet(dc.codec, dc.pkt); - while (avcodec_receive_frame(dc.codec, dc.frame) == 0) scaleAndPush(dc); + bool ok = true; + while (ok && avcodec_receive_frame(dc.codec, dc.frame) == 0) + ok = scaleAndPush(dc); + if (!ok) break; } av_packet_unref(dc.pkt); } @@ -489,28 +524,33 @@ int playVideo(const std::string& path, Timeline* timeline, writeRaw("\x1b[?25l\x1b[2J\x1b[H"); std::atomic finished(false); - BoundedQueue queue(4); + FrameQueue queue(8); dc.queue = &queue; dc.stop = opts.stop; dc.finished = &finished; dc.loop = opts.loop; + // Start the playback at the timeline's current position so the video + // doesn't lag the audio by the probe/setup time. + dc.skipFrames = std::max(0, static_cast(timeline->seconds() * fps)); std::thread decoder(decoderThreadFn, std::ref(dc)); const double intervalSec = 1.0 / fps; std::string out; out.reserve(static_cast(gridW) * gridH * 24); - // Render one (latest) frame per time slot, paced by the shared timeline. - // `nextSlot` jumps past any missed slots, so a slow decode never causes a - // catch-up burst of redraws. - double nextSlot = 0.0; + // Render frames in decode order, one per time slot paced by the shared + // timeline. `nextSlot` starts at the current position (frames we skipped + // in the decoder) and re-anchors to the current time when the renderer + // falls behind, so it never bursts a catch-up redraw; the blocking queue + // keeps the decoder from running ahead of us. + double nextSlot = timeline->seconds(); while (!(opts.stop && opts.stop->load())) { RgbFrame frame; const auto deadline = timeline->startTime() + std::chrono::duration_cast( std::chrono::duration(nextSlot)); - const bool got = queue.popLatestUntil(deadline, frame); + const bool got = queue.popUntil(deadline, frame); // Wait until the timeline reaches this slot (unless stopped). while (timeline->seconds() < nextSlot) { @@ -530,7 +570,7 @@ int playVideo(const std::string& path, Timeline* timeline, nextSlot = std::max(nextSlot + intervalSec, now + intervalSec); } - queue.setFinished(); + queue.close(); // unblock a producer stuck on a full queue, then join decoder.join(); writeRaw("\x1b[0m\x1b[?25h"); freeDecode(dc); diff --git a/uninstall.cpp b/uninstall.cpp index 751bb5b..542fac8 100644 --- a/uninstall.cpp +++ b/uninstall.cpp @@ -57,7 +57,7 @@ static int runShow(const std::string& videoPath, const std::string& audioPath, bd::PlayVideoOptions vo; vo.loop = true; - vo.intensity = 0.6f; // 60% brightness + vo.intensity = 1f; // brightness settings vo.topRows = 1; // one row above the video reserved for the lyrics vo.outGrid = &videoGrid; vo.outOffsetX = &videoOffsetX;