From 73e48f52fad19986bb7a342e276e371708fd60c8 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 2 Oct 2024 17:15:28 +0200 Subject: [PATCH] animation --- src/app/Clock.h | 73 ++++++++++++++++++++++++++++++++++++++++++++++ src/app/Message.h | 14 +++++++++ src/app/Mixer.h | 41 ++++++++++++++++++++++++++ src/app/Renderer.h | 32 +++++++++++++------- src/app/Stream.h | 31 ++++++++++---------- src/main.cpp | 20 ++++++------- 6 files changed, 174 insertions(+), 37 deletions(-) create mode 100644 src/app/Clock.h create mode 100644 src/app/Mixer.h diff --git a/src/app/Clock.h b/src/app/Clock.h new file mode 100644 index 0000000..81b092f --- /dev/null +++ b/src/app/Clock.h @@ -0,0 +1,73 @@ +#ifndef TEST_APP_CLOCK_H_ +#define TEST_APP_CLOCK_H_ + +extern "C" { +#include +#include +} + +namespace app { + +class Clock { + +public: + Clock() + : timebase{1, 1000}, counter(0) { + } + explicit Clock(AVRational timebase) + : timebase(timebase), counter(0) { + } + +public: + void Advance() { + ++counter; + } + + void Advance(int n) { + counter += n; + } + + Clock Difference(const Clock &other) const { + Clock result(*this); + int64_t rescaled = av_rescale_q(other.counter, other.timebase, timebase); + result.counter -= rescaled; + return result; + } + + double Interpolate(double from, double to, int64_t from_ms, int64_t to_ms) const { + double t = double(GetMS() - from_ms) / double(to_ms - from_ms); + return from + t * (to - from); + } + + double InterpolateClamp(double from, double to, int64_t from_ms, int64_t to_ms) const { + int64_t ms = GetMS(); + if (ms < from_ms) return from; + if (ms > to_ms) return to; + return Interpolate(from, to, from_ms, to_ms); + } + + void Reset() { + counter = 0; + } + + Clock Snapshot() const { + return *this; + } + + int64_t GetCounter() const { + return counter; + } + + int64_t GetMS() const { + return av_rescale_q(counter, timebase, AVRational{1, 1000}); + } + +private: + AVRational timebase; + int64_t counter; + +}; + +} + +#endif diff --git a/src/app/Message.h b/src/app/Message.h index 30fbc81..a84d8a7 100644 --- a/src/app/Message.h +++ b/src/app/Message.h @@ -3,6 +3,7 @@ #include +#include "Clock.h" #include "../cairo/Context.h" #include "../gfx/ColorRGB.h" #include "../gfx/Position.h" @@ -46,6 +47,14 @@ public: channel_layout.SetWidth(inner_width); } + void SetBirth(const Clock &clock) { + birth = clock; + } + + const Clock &GetBirth() const { + return birth; + } + void SetText(const std::string &t) { text_layout.SetText(t); } @@ -58,6 +67,10 @@ public: pos = p; } + const gfx::Position &GetPosition() const { + return pos; + } + double GetHeight() const { return size.h; } @@ -93,6 +106,7 @@ private: gfx::ColorRGB text_color; gfx::ColorRGB channel_color; + Clock birth; gfx::Position pos; gfx::Size size; gfx::Spacing padding; diff --git a/src/app/Mixer.h b/src/app/Mixer.h new file mode 100644 index 0000000..dbe0508 --- /dev/null +++ b/src/app/Mixer.h @@ -0,0 +1,41 @@ +#ifndef TEST_APP_MIXER_H_ +#define TEST_APP_MIXER_H_ + +#include + +#include "Clock.h" + +namespace app { + +class Mixer { + +public: + Mixer(const Clock *clock, int16_t *plane, int channels, int frame_size) + : clock(clock), plane(plane), channels(channels), frame_size(frame_size) { + } + ~Mixer() { + } + + Mixer(const Mixer &) = delete; + Mixer &operator =(const Mixer &) = delete; + +public: + void RenderAudioFrame() { + for (int i = 0; i < frame_size; ++i) { + for (int j = 0; j < channels; ++j) { + plane[i * channels + j] = 0; + } + } + } + +private: + const Clock *clock; + int16_t *plane; + int channels; + int frame_size; + +}; + +} + +#endif diff --git a/src/app/Renderer.h b/src/app/Renderer.h index a74c7c8..b7b2e7e 100644 --- a/src/app/Renderer.h +++ b/src/app/Renderer.h @@ -8,6 +8,7 @@ extern "C" { #include "cairo.h" } +#include "Clock.h" #include "Message.h" #include "../cairo/Context.h" #include "../cairo/Surface.h" @@ -17,8 +18,9 @@ namespace app { class Renderer { public: - Renderer(uint8_t *plane, int linesize, int width, int height) - : text_font("DejaVu Sans 32px") + Renderer(const Clock *clock, uint8_t *plane, int linesize, int width, int height) + : clock(clock) + , text_font("DejaVu Sans 32px") , channel_font("DejaVu Sans 24px") , surface(plane, linesize, CAIRO_FORMAT_ARGB32, width, height) , ctx(surface.CreateContext()) @@ -33,7 +35,21 @@ public: Renderer &operator =(const Renderer &) = delete; public: - void RenderVideoFrame(int64_t num, int64_t time_ms) { + void Update() { + gfx::Position pos({ 50, 50 }); + for (Message &msg : msgs) { + double distance = msg.GetHeight() + 10.0; + Clock lifetime = clock->Difference(msg.GetBirth()); + pos.y -= lifetime.InterpolateClamp(distance + 50, 0.0, 0, 600); + msg.SetPosition(pos); + pos.y = std::max(pos.y + distance, 50.0); + } + if (msgs.size() > 1 && msgs.back().GetPosition().y > height) { + msgs.pop_back(); + } + } + + void RenderVideoFrame() { ctx.SetSourceRGB(0, 0, 0); ctx.Paint(); @@ -52,18 +68,12 @@ public: msg.SetWidth(width / 2.0); msg.SetText(text); msg.SetChannel(channel); + msg.SetBirth(clock->Snapshot()); msg.Update(); - if (msgs.size() > 3) { - msgs.pop_back(); - } - gfx::Position pos({ 50, 50 }); - for (Message &msg : msgs) { - msg.SetPosition(pos); - pos.y += msg.GetHeight() + 10.0; - } } private: + const Clock *clock; pango::Font text_font; pango::Font channel_font; cairo::Surface surface; diff --git a/src/app/Stream.h b/src/app/Stream.h index 7ad9fde..4f53425 100644 --- a/src/app/Stream.h +++ b/src/app/Stream.h @@ -12,6 +12,7 @@ extern "C" { #include } +#include "Clock.h" #include "../ffmpeg/Encoder.h" #include "../ffmpeg/Network.h" #include "../ffmpeg/OutputContext.h" @@ -34,6 +35,7 @@ public: audio_encoder.InferSampleFormat(); audio_encoder.SetBitRate(160 * 1000); audio_encoder.Open(); + audio_clock = Clock(audio_encoder.GetTimeBase()); video_encoder.SetSize(width, height); video_encoder.InferPixelFormat(); @@ -46,6 +48,7 @@ public: video_encoder.SetGlobalHeader(); } video_encoder.Open(); + video_clock = Clock(video_encoder.GetTimeBase()); video_stream = output.CreateVideoStream(video_encoder); audio_stream = output.CreateAudioStream(audio_encoder); @@ -85,8 +88,8 @@ public: output.Open(); output.WriteHeader(); start = std::chrono::high_resolution_clock::now(); - video_frame_counter = 0; - audio_frame_counter = 0; + video_clock.Reset(); + audio_clock.Reset(); } void Finish() { @@ -107,12 +110,12 @@ public: output.WriteTrailer(); } - int64_t GetVideoFrameCounter() const { - return video_frame_counter; + const Clock &GetAudioClock() const { + return audio_clock; } - int64_t AudioElapsedMS() const { - return av_rescale_q(audio_frame_counter, audio_encoder.GetTimeBase(), AVRational{1, 1000}); + const Clock &GetVideoClock() const { + return video_clock; } int64_t TimeElapsedMS() const { @@ -120,24 +123,20 @@ public: return std::chrono::duration_cast(now - start).count(); } - int64_t VideoElapsedMS() const { - return av_rescale_q(video_frame_counter, video_encoder.GetTimeBase(), AVRational{1, 1000}); - } - void PrepareAudioFrame() { audio_input_frame.MakeWritable(); } void PushAudioFrame() { resampler.Convert(audio_encoder, audio_input_frame, audio_output_frame); - audio_output_frame.SetPresentationTimestamp(audio_frame_counter); + audio_output_frame.SetPresentationTimestamp(audio_clock.GetCounter()); audio_encoder.SendFrame(audio_output_frame); while (audio_encoder.ReceivePacket(audio_packet)) { audio_packet.Rescale(audio_encoder, audio_stream); audio_packet.SetStream(audio_stream); output.WriteInterleavedPacket(audio_packet); } - audio_frame_counter += audio_encoder.GetFrameSize(); + audio_clock.Advance(audio_encoder.GetFrameSize()); } void PrepareVideoFrame() { @@ -147,9 +146,9 @@ public: void PushVideoFrame() { scaler.ScaleFrame(video_input_frame, video_output_frame); - video_output_frame.SetPresentationTimestamp(video_frame_counter); + video_output_frame.SetPresentationTimestamp(video_clock.GetCounter()); video_encoder.SendFrame(video_output_frame); - ++video_frame_counter; + video_clock.Advance(); while (video_encoder.ReceivePacket(video_packet)) { video_packet.Rescale(video_encoder, video_stream); video_packet.SetStream(video_stream); @@ -174,8 +173,8 @@ private: ffmpeg::Packet audio_packet; std::chrono::time_point start; - int64_t video_frame_counter; - int64_t audio_frame_counter; + Clock audio_clock; + Clock video_clock; }; diff --git a/src/main.cpp b/src/main.cpp index 8626285..b06aa40 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,7 @@ extern "C" { #include } +#include "app/Mixer.h" #include "app/Renderer.h" #include "app/Stream.h" #include "uv/Loop.h" @@ -70,8 +71,10 @@ int main(int argc, char**argv) { int16_t *audio_plane = stream.GetAudioPlane(); const int audio_channels = stream.GetAudioChannels(); + const int audio_frame_size = stream.GetAudioFrameSize(); - app::Renderer renderer(plane, linesize, WIDTH, HEIGHT); + app::Renderer renderer(&stream.GetVideoClock(), plane, linesize, WIDTH, HEIGHT); + app::Mixer mixer(&stream.GetAudioClock(), audio_plane, audio_channels, audio_frame_size); wsconn.Subscribe("ChatBotLog", &ws_handler, &renderer); @@ -82,27 +85,24 @@ int main(int argc, char**argv) { while (running) { loop.TryStep(); - const int64_t target = stream.VideoElapsedMS(); + const int64_t target = stream.GetVideoClock().GetMS(); const int64_t elapsed = stream.TimeElapsedMS(); const int64_t difference = target - elapsed; stream.PrepareVideoFrame(); - if (stream.GetVideoFrameCounter() > 0 && difference < 0) { + if (target > 0 && difference < 0) { std::cout << (difference / -1000.0) << "s behind schedule, dropping frame" << std::endl; } else { - renderer.RenderVideoFrame(stream.GetVideoFrameCounter(), target); + renderer.Update(); + renderer.RenderVideoFrame(); } stream.PushVideoFrame(); - while (stream.AudioElapsedMS() < target) { + while (stream.GetAudioClock().GetMS() < target) { stream.PrepareAudioFrame(); - for (int i = 0; i < stream.GetAudioFrameSize(); ++i) { - for (int j = 0; j < audio_channels; ++j) { - audio_plane[i * audio_channels + j] = 0; - } - } + mixer.RenderAudioFrame(); stream.PushAudioFrame(); } -- 2.39.2