recording: initial audio recording

This commit is contained in:
Megamouse 2022-11-06 11:19:24 +01:00
parent ebf48800e6
commit 70c35642a4
13 changed files with 1036 additions and 474 deletions

View file

@ -33,8 +33,7 @@ void audio_resampler::put_samples(const f32* buf, u32 sample_cnt)
std::pair<f32* /* buffer */, u32 /* samples */> audio_resampler::get_samples(u32 sample_cnt) std::pair<f32* /* buffer */, u32 /* samples */> audio_resampler::get_samples(u32 sample_cnt)
{ {
f32 *const buf = resampler.bufBegin(); return std::make_pair(resampler.bufBegin(), resampler.receiveSamples(sample_cnt));
return std::make_pair(buf, resampler.receiveSamples(sample_cnt));
} }
u32 audio_resampler::samples_available() const u32 audio_resampler::samples_available() const

View file

@ -5,6 +5,7 @@
#include "Emu/Cell/lv2/sys_process.h" #include "Emu/Cell/lv2/sys_process.h"
#include "Emu/Cell/lv2/sys_event.h" #include "Emu/Cell/lv2/sys_event.h"
#include "cellAudio.h" #include "cellAudio.h"
#include "util/video_provider.h"
#include <cmath> #include <cmath>
@ -69,7 +70,7 @@ void cell_audio_config::reset(bool backend_changed)
const AudioFreq freq = AudioFreq::FREQ_48K; const AudioFreq freq = AudioFreq::FREQ_48K;
const AudioSampleSize sample_size = raw.convert_to_s16 ? AudioSampleSize::S16 : AudioSampleSize::FLOAT; const AudioSampleSize sample_size = raw.convert_to_s16 ? AudioSampleSize::S16 : AudioSampleSize::FLOAT;
const auto [req_ch_cnt, downmix] = AudioBackend::get_channel_count_and_downmixer(0); // CELL_AUDIO_OUT_PRIMARY const auto& [req_ch_cnt, downmix] = AudioBackend::get_channel_count_and_downmixer(0); // CELL_AUDIO_OUT_PRIMARY
f64 cb_frame_len = 0.0; f64 cb_frame_len = 0.0;
u32 ch_cnt = 2; u32 ch_cnt = 2;
@ -276,16 +277,23 @@ void audio_ringbuffer::process_resampled_data()
{ {
if (!cfg.time_stretching_enabled) return; if (!cfg.time_stretching_enabled) return;
const auto [buffer, samples] = resampler.get_samples(static_cast<u32>(cb_ringbuf.get_free_size() / (cfg.audio_sample_size * static_cast<u32>(cfg.backend_ch_cnt)))); const auto& [buffer, samples] = resampler.get_samples(static_cast<u32>(cb_ringbuf.get_free_size() / (cfg.audio_sample_size * static_cast<u32>(cfg.backend_ch_cnt))));
commit_data(buffer, samples); commit_data(buffer, samples);
} }
void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt) void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
{ {
sample_cnt *= cfg.audio_channels; const u32 sample_cnt_in = sample_cnt * cfg.audio_channels;
const u32 sample_cnt_out = sample_cnt * static_cast<u32>(cfg.backend_ch_cnt);
// Dump audio if enabled // Dump audio if enabled
m_dump.WriteData(buf, sample_cnt * static_cast<u32>(AudioSampleSize::FLOAT)); m_dump.WriteData(buf, sample_cnt_in * static_cast<u32>(AudioSampleSize::FLOAT));
// Record audio if enabled
if (utils::video_provider& provider = g_fxo->get<utils::video_provider>(); provider.can_consume_sample())
{
provider.present_samples(reinterpret_cast<u8*>(buf), sample_cnt, static_cast<u32>(cfg.audio_channels));
}
if (cfg.backend_ch_cnt < AudioChannelCnt{cfg.audio_channels}) if (cfg.backend_ch_cnt < AudioChannelCnt{cfg.audio_channels})
{ {
@ -293,11 +301,11 @@ void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
{ {
if (cfg.backend_ch_cnt == AudioChannelCnt::SURROUND_5_1) if (cfg.backend_ch_cnt == AudioChannelCnt::SURROUND_5_1)
{ {
AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::SURROUND_5_1>(sample_cnt, buf, buf); AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::SURROUND_5_1>(sample_cnt_in, buf, buf);
} }
else if (cfg.backend_ch_cnt == AudioChannelCnt::STEREO) else if (cfg.backend_ch_cnt == AudioChannelCnt::STEREO)
{ {
AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::STEREO>(sample_cnt, buf, buf); AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::STEREO>(sample_cnt_in, buf, buf);
} }
else else
{ {
@ -308,7 +316,7 @@ void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
{ {
if (cfg.backend_ch_cnt == AudioChannelCnt::STEREO) if (cfg.backend_ch_cnt == AudioChannelCnt::STEREO)
{ {
AudioBackend::downmix<AudioChannelCnt::SURROUND_5_1, AudioChannelCnt::STEREO>(sample_cnt, buf, buf); AudioBackend::downmix<AudioChannelCnt::SURROUND_5_1, AudioChannelCnt::STEREO>(sample_cnt_in, buf, buf);
} }
else else
{ {
@ -321,8 +329,6 @@ void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
} }
} }
const u32 sample_cnt_out = sample_cnt / cfg.audio_channels * static_cast<u32>(cfg.backend_ch_cnt);
if (cfg.backend->get_convert_to_s16()) if (cfg.backend->get_convert_to_s16())
{ {
AudioBackend::convert_to_s16(sample_cnt_out, buf, buf); AudioBackend::convert_to_s16(sample_cnt_out, buf, buf);

View file

@ -140,34 +140,26 @@ struct rec_param
constexpr u32 rec_framerate = 30; // Always 30 fps constexpr u32 rec_framerate = 30; // Always 30 fps
class rec_image_sink : public utils::image_sink class rec_video_sink : public utils::video_sink
{ {
public: public:
rec_image_sink() : utils::image_sink() rec_video_sink() : utils::video_sink()
{ {
m_framerate = rec_framerate; m_framerate = rec_framerate;
m_sample_rate = 44100; // TODO
} }
void stop(bool flush = true) override void stop(bool flush = true) override
{ {
cellRec.notice("Stopping image sink. flush=%d", flush); cellRec.notice("Stopping video sink. flush=%d", flush);
std::lock_guard lock(m_mtx); std::lock_guard lock(m_mtx);
m_flush = flush; m_flush = flush;
m_frames_to_encode.clear(); m_frames_to_encode.clear();
m_samples_to_encode.clear();
has_error = false; has_error = false;
} }
void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms) override
{
std::lock_guard lock(m_mtx);
if (m_flush)
return;
m_frames_to_encode.emplace_back(timestamp_ms, pitch, width, height, pixel_format, std::move(frame));
}
encoder_frame get_frame() encoder_frame get_frame()
{ {
std::lock_guard lock(m_mtx); std::lock_guard lock(m_mtx);
@ -196,7 +188,7 @@ struct rec_info
vm::bptr<u8> video_input_buffer{}; // Used by the game to inject a frame right before it would render a frame to the screen. vm::bptr<u8> video_input_buffer{}; // Used by the game to inject a frame right before it would render a frame to the screen.
vm::bptr<u8> audio_input_buffer{}; // Used by the game to inject audio: 2-channel interleaved (left-right) * 256 samples * sizeof(f32) at 48000 kHz vm::bptr<u8> audio_input_buffer{}; // Used by the game to inject audio: 2-channel interleaved (left-right) * 256 samples * sizeof(f32) at 48000 kHz
std::vector<utils::image_sink::encoder_frame> video_ringbuffer; std::vector<utils::video_sink::encoder_frame> video_ringbuffer;
std::vector<u8> audio_ringbuffer; std::vector<u8> audio_ringbuffer;
usz video_ring_pos = 0; usz video_ring_pos = 0;
usz video_ring_frame_count = 0; usz video_ring_frame_count = 0;
@ -209,9 +201,9 @@ struct rec_info
return pos; return pos;
} }
std::shared_ptr<rec_image_sink> image_sink; std::shared_ptr<rec_video_sink> video_sink;
std::shared_ptr<utils::video_encoder> encoder; std::shared_ptr<utils::video_encoder> encoder;
std::unique_ptr<named_thread<std::function<void()>>> image_provider_thread; std::unique_ptr<named_thread<std::function<void()>>> video_provider_thread;
atomic_t<bool> paused = false; atomic_t<bool> paused = false;
s64 last_pts = -1; s64 last_pts = -1;
@ -240,9 +232,9 @@ struct rec_info
void set_video_params(s32 video_format); void set_video_params(s32 video_format);
void set_audio_params(s32 audio_format); void set_audio_params(s32 audio_format);
void start_image_provider(); void start_video_provider();
void pause_image_provider(); void pause_video_provider();
void stop_image_provider(bool flush); void stop_video_provider(bool flush);
}; };
void rec_info::set_video_params(s32 video_format) void rec_info::set_video_params(s32 video_format)
@ -507,29 +499,29 @@ void rec_info::set_audio_params(s32 audio_format)
cellRec.notice("set_audio_params: audio_format=0x%x, audio_codec_id=%d, sample_rate=%d, audio_bps=%d", audio_format, audio_codec_id, sample_rate, audio_bps); cellRec.notice("set_audio_params: audio_format=0x%x, audio_codec_id=%d, sample_rate=%d, audio_bps=%d", audio_format, audio_codec_id, sample_rate, audio_bps);
} }
void rec_info::start_image_provider() void rec_info::start_video_provider()
{ {
const bool was_paused = paused.exchange(false); const bool was_paused = paused.exchange(false);
utils::video_provider& video_provider = g_fxo->get<utils::video_provider>(); utils::video_provider& video_provider = g_fxo->get<utils::video_provider>();
if (image_provider_thread && was_paused) if (video_provider_thread && was_paused)
{ {
// Resume // Resume
const u64 pause_time_end = get_system_time(); const u64 pause_time_end = get_system_time();
ensure(pause_time_end > pause_time_start); ensure(pause_time_end > pause_time_start);
pause_time_total += (pause_time_end - pause_time_start); pause_time_total += (pause_time_end - pause_time_start);
video_provider.set_pause_time(pause_time_total / 1000); video_provider.set_pause_time(pause_time_total / 1000);
cellRec.notice("Resuming image provider."); cellRec.notice("Resuming video provider.");
return; return;
} }
cellRec.notice("Starting image provider."); cellRec.notice("Starting video provider.");
recording_time_start = get_system_time(); recording_time_start = get_system_time();
pause_time_total = 0; pause_time_total = 0;
video_provider.set_pause_time(0); video_provider.set_pause_time(0);
image_provider_thread = std::make_unique<named_thread<std::function<void()>>>("cellRec Image Provider", [this]() video_provider_thread = std::make_unique<named_thread<std::function<void()>>>("cellRec video provider", [this]()
{ {
const bool use_internal_audio = param.audio_input == CELL_REC_PARAM_AUDIO_INPUT_DISABLE || param.audio_input_mix_vol < 100; const bool use_internal_audio = param.audio_input == CELL_REC_PARAM_AUDIO_INPUT_DISABLE || param.audio_input_mix_vol < 100;
const bool use_external_audio = param.audio_input != CELL_REC_PARAM_AUDIO_INPUT_DISABLE && param.audio_input_mix_vol > 0; const bool use_external_audio = param.audio_input != CELL_REC_PARAM_AUDIO_INPUT_DISABLE && param.audio_input_mix_vol > 0;
@ -537,7 +529,7 @@ void rec_info::start_image_provider()
const bool use_ring_buffer = param.ring_sec > 0; const bool use_ring_buffer = param.ring_sec > 0;
const usz frame_size = input_format.pitch * input_format.height; const usz frame_size = input_format.pitch * input_format.height;
cellRec.notice("image_provider_thread: use_ring_buffer=%d, video_ringbuffer_size=%d, audio_ringbuffer_size=%d, ring_sec=%d, frame_size=%d, use_external_video=%d, use_external_audio=%d, use_internal_audio=%d", use_ring_buffer, video_ringbuffer.size(), audio_ringbuffer.size(), param.ring_sec, frame_size, use_external_video, use_external_audio, use_internal_audio); cellRec.notice("video_provider_thread: use_ring_buffer=%d, video_ringbuffer_size=%d, audio_ringbuffer_size=%d, ring_sec=%d, frame_size=%d, use_external_video=%d, use_external_audio=%d, use_internal_audio=%d", use_ring_buffer, video_ringbuffer.size(), audio_ringbuffer.size(), param.ring_sec, frame_size, use_external_video, use_external_audio, use_internal_audio);
while (thread_ctrl::state() != thread_state::aborting && encoder) while (thread_ctrl::state() != thread_state::aborting && encoder)
{ {
@ -575,7 +567,7 @@ void rec_info::start_image_provider()
{ {
if (use_ring_buffer) if (use_ring_buffer)
{ {
utils::image_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()]; utils::video_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()];
frame_data.pts = pts; frame_data.pts = pts;
frame_data.width = input_format.width; frame_data.width = input_format.width;
frame_data.height = input_format.height; frame_data.height = input_format.height;
@ -595,14 +587,14 @@ void rec_info::start_image_provider()
last_pts = pts; last_pts = pts;
} }
} }
else if (use_ring_buffer && image_sink) else if (use_ring_buffer && video_sink)
{ {
utils::image_sink::encoder_frame frame = image_sink->get_frame(); utils::video_sink::encoder_frame frame = video_sink->get_frame();
if (const s64 pts = encoder->get_pts(frame.timestamp_ms); pts > last_pts && frame.data.size() > 0) if (const s64 pts = encoder->get_pts(frame.timestamp_ms); pts > last_pts && frame.data.size() > 0)
{ {
ensure(frame.data.size() == frame_size); ensure(frame.data.size() == frame_size);
utils::image_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()]; utils::video_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()];
frame_data = std::move(frame); frame_data = std::move(frame);
frame_data.pts = pts; frame_data.pts = pts;
last_pts = pts; last_pts = pts;
@ -635,34 +627,34 @@ void rec_info::start_image_provider()
} }
// Update recording time // Update recording time
recording_time_total = encoder->get_timestamp_ms(encoder->last_pts()); recording_time_total = encoder->get_timestamp_ms(encoder->last_video_pts());
thread_ctrl::wait_for(100); thread_ctrl::wait_for(100);
} }
}); });
} }
void rec_info::pause_image_provider() void rec_info::pause_video_provider()
{ {
cellRec.notice("Pausing image provider."); cellRec.notice("Pausing image provider.");
if (image_provider_thread) if (video_provider_thread)
{ {
paused = true; paused = true;
pause_time_start = get_system_time(); pause_time_start = get_system_time();
} }
} }
void rec_info::stop_image_provider(bool flush) void rec_info::stop_video_provider(bool flush)
{ {
cellRec.notice("Stopping image provider."); cellRec.notice("Stopping video provider.");
if (image_provider_thread) if (video_provider_thread)
{ {
auto& thread = *image_provider_thread; auto& thread = *video_provider_thread;
thread = thread_state::aborting; thread = thread_state::aborting;
thread(); thread();
image_provider_thread.reset(); video_provider_thread.reset();
} }
if (flush && param.ring_sec > 0 && !video_ringbuffer.empty()) if (flush && param.ring_sec > 0 && !video_ringbuffer.empty())
@ -680,7 +672,7 @@ void rec_info::stop_image_provider(bool flush)
for (usz i = 0; i < frame_count; i++) for (usz i = 0; i < frame_count; i++)
{ {
const usz pos = (start_offset + i) % video_ringbuffer.size(); const usz pos = (start_offset + i) % video_ringbuffer.size();
utils::image_sink::encoder_frame& frame_data = video_ringbuffer[pos]; utils::video_sink::encoder_frame& frame_data = video_ringbuffer[pos];
encoder->add_frame(frame_data.data, frame_data.pitch, frame_data.width, frame_data.height, frame_data.av_pixel_format, encoder->get_timestamp_ms(frame_data.pts - start_pts)); encoder->add_frame(frame_data.data, frame_data.pitch, frame_data.width, frame_data.height, frame_data.av_pixel_format, encoder->get_timestamp_ms(frame_data.pts - start_pts));
// TODO: add audio data to encoder // TODO: add audio data to encoder
@ -1073,7 +1065,7 @@ error_code cellRecOpen(vm::cptr<char> pDirName, vm::cptr<char> pFileName, vm::cp
rec.audio_ringbuffer.resize(audio_ring_buffer_size); rec.audio_ringbuffer.resize(audio_ring_buffer_size);
rec.audio_ring_step = audio_size_per_sample; rec.audio_ring_step = audio_size_per_sample;
rec.video_ringbuffer.resize(video_ring_buffer_size, {}); rec.video_ringbuffer.resize(video_ring_buffer_size, {});
rec.image_sink = std::make_shared<rec_image_sink>(); rec.video_sink = std::make_shared<rec_video_sink>();
} }
rec.encoder = std::make_shared<utils::video_encoder>(); rec.encoder = std::make_shared<utils::video_encoder>();
@ -1082,6 +1074,7 @@ error_code cellRecOpen(vm::cptr<char> pDirName, vm::cptr<char> pFileName, vm::cp
rec.encoder->set_video_bitrate(rec.video_bps); rec.encoder->set_video_bitrate(rec.video_bps);
rec.encoder->set_video_codec(rec.video_codec_id); rec.encoder->set_video_codec(rec.video_codec_id);
rec.encoder->set_sample_rate(rec.sample_rate); rec.encoder->set_sample_rate(rec.sample_rate);
rec.encoder->set_audio_channels(rec.channels);
rec.encoder->set_audio_bitrate(rec.audio_bps); rec.encoder->set_audio_bitrate(rec.audio_bps);
rec.encoder->set_audio_codec(rec.audio_codec_id); rec.encoder->set_audio_codec(rec.audio_codec_id);
rec.encoder->set_output_format(rec.output_format); rec.encoder->set_output_format(rec.output_format);
@ -1114,12 +1107,12 @@ error_code cellRecClose(s32 isDiscard)
if (isDiscard) if (isDiscard)
{ {
// No need to flush // No need to flush
rec.stop_image_provider(false); rec.stop_video_provider(false);
rec.encoder->stop(false); rec.encoder->stop(false);
if (rec.image_sink) if (rec.video_sink)
{ {
rec.image_sink->stop(false); rec.video_sink->stop(false);
} }
if (fs::is_file(rec.param.filename)) if (fs::is_file(rec.param.filename))
@ -1135,18 +1128,18 @@ error_code cellRecClose(s32 isDiscard)
else else
{ {
// Flush to make sure we encode all remaining frames // Flush to make sure we encode all remaining frames
rec.stop_image_provider(true); rec.stop_video_provider(true);
rec.encoder->stop(true); rec.encoder->stop(true);
rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_pts()); rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_video_pts());
if (rec.image_sink) if (rec.video_sink)
{ {
rec.image_sink->stop(true); rec.video_sink->stop(true);
} }
const s64 start_pts = rec.encoder->get_pts(rec.param.scene_metadata.start_time); const s64 start_pts = rec.encoder->get_pts(rec.param.scene_metadata.start_time);
const s64 end_pts = rec.encoder->get_pts(rec.param.scene_metadata.end_time); const s64 end_pts = rec.encoder->get_pts(rec.param.scene_metadata.end_time);
const s64 last_pts = rec.encoder->last_pts(); const s64 last_pts = rec.encoder->last_video_pts();
is_valid_range = start_pts >= 0 && end_pts <= last_pts; is_valid_range = start_pts >= 0 && end_pts <= last_pts;
} }
@ -1157,7 +1150,7 @@ error_code cellRecClose(s32 isDiscard)
g_fxo->need<utils::video_provider>(); g_fxo->need<utils::video_provider>();
utils::video_provider& video_provider = g_fxo->get<utils::video_provider>(); utils::video_provider& video_provider = g_fxo->get<utils::video_provider>();
// Release the image sink if it was used // Release the video sink if it was used
if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE) if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE)
{ {
const recording_mode old_mode = g_recording_mode.exchange(recording_mode::stopped); const recording_mode old_mode = g_recording_mode.exchange(recording_mode::stopped);
@ -1167,15 +1160,15 @@ error_code cellRecClose(s32 isDiscard)
cellRec.error("cellRecClose: Unexpected recording mode %s found while stopping video capture.", old_mode); cellRec.error("cellRecClose: Unexpected recording mode %s found while stopping video capture.", old_mode);
} }
if (!video_provider.set_image_sink(nullptr, recording_mode::cell)) if (!video_provider.set_video_sink(nullptr, recording_mode::cell))
{ {
cellRec.error("cellRecClose failed to release image sink"); cellRec.error("cellRecClose failed to release video sink");
} }
} }
rec.param = {}; rec.param = {};
rec.encoder.reset(); rec.encoder.reset();
rec.image_sink.reset(); rec.video_sink.reset();
rec.audio_ringbuffer.clear(); rec.audio_ringbuffer.clear();
rec.video_ringbuffer.clear(); rec.video_ringbuffer.clear();
rec.state = rec_state::closed; rec.state = rec_state::closed;
@ -1207,7 +1200,7 @@ error_code cellRecStop()
sysutil_register_cb([&rec](ppu_thread& ppu) -> s32 sysutil_register_cb([&rec](ppu_thread& ppu) -> s32
{ {
// Disable image sink if it was used // Disable video sink if it was used
if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE) if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE)
{ {
const recording_mode old_mode = g_recording_mode.exchange(recording_mode::stopped); const recording_mode old_mode = g_recording_mode.exchange(recording_mode::stopped);
@ -1219,12 +1212,12 @@ error_code cellRecStop()
} }
// cellRecStop actually just pauses the recording // cellRecStop actually just pauses the recording
rec.pause_image_provider(); rec.pause_video_provider();
ensure(!!rec.encoder); ensure(!!rec.encoder);
rec.encoder->pause(true); rec.encoder->pause(true);
rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_pts()); rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_video_pts());
rec.state = rec_state::stopped; rec.state = rec_state::stopped;
rec.cb(ppu, CELL_REC_STATUS_STOP, CELL_OK, rec.cbUserData); rec.cb(ppu, CELL_REC_STATUS_STOP, CELL_OK, rec.cbUserData);
@ -1254,15 +1247,15 @@ error_code cellRecStart()
g_fxo->need<utils::video_provider>(); g_fxo->need<utils::video_provider>();
utils::video_provider& video_provider = g_fxo->get<utils::video_provider>(); utils::video_provider& video_provider = g_fxo->get<utils::video_provider>();
// Setup an image sink if it is needed // Setup an video sink if it is needed
if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE) if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE)
{ {
if (rec.param.ring_sec <= 0) if (rec.param.ring_sec <= 0)
{ {
// Regular recording // Regular recording
if (!video_provider.set_image_sink(rec.encoder, recording_mode::cell)) if (!video_provider.set_video_sink(rec.encoder, recording_mode::cell))
{ {
cellRec.error("Failed to set image sink"); cellRec.error("Failed to set video sink");
rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FATAL, rec.cbUserData); rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FATAL, rec.cbUserData);
return CELL_OK; return CELL_OK;
} }
@ -1270,9 +1263,9 @@ error_code cellRecStart()
else else
{ {
// Ringbuffer recording // Ringbuffer recording
if (!video_provider.set_image_sink(rec.image_sink, recording_mode::cell)) if (!video_provider.set_video_sink(rec.video_sink, recording_mode::cell))
{ {
cellRec.error("Failed to set image sink"); cellRec.error("Failed to set video sink");
rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FATAL, rec.cbUserData); rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FATAL, rec.cbUserData);
return CELL_OK; return CELL_OK;
} }
@ -1287,7 +1280,7 @@ error_code cellRecStart()
g_recording_mode = recording_mode::stopped; g_recording_mode = recording_mode::stopped;
} }
rec.start_image_provider(); rec.start_video_provider();
if (rec.encoder->has_error) if (rec.encoder->has_error)
{ {

View file

@ -8,15 +8,32 @@ struct cfg_recording final : cfg::node
bool load(); bool load();
void save() const; void save() const;
struct node_video : cfg::node
{
node_video(cfg::node* _this) : cfg::node(_this, "Video") {}
cfg::uint<0, 60> framerate{this, "Framerate", 30}; cfg::uint<0, 60> framerate{this, "Framerate", 30};
cfg::uint<0, 7680> width{this, "Width", 1280}; cfg::uint<0, 7680> width{this, "Width", 1280};
cfg::uint<0, 4320> height{this, "Height", 720}; cfg::uint<0, 4320> height{this, "Height", 720};
cfg::uint<0, 192> pixel_format{this, "AVPixelFormat", 0}; // AVPixelFormat::AV_PIX_FMT_YUV420P cfg::uint<0, 192> pixel_format{this, "AVPixelFormat", 0}; // AVPixelFormat::AV_PIX_FMT_YUV420P
cfg::uint<0, 32813> video_codec{this, "AVCodecID", 12}; // AVCodecID::AV_CODEC_ID_MPEG4 cfg::uint<0, 0xFFFF> video_codec{this, "AVCodecID", 12}; // AVCodecID::AV_CODEC_ID_MPEG4
cfg::uint<0, 25000000> video_bps{this, "Video Bitrate", 4000000}; cfg::uint<0, 25000000> video_bps{this, "Video Bitrate", 4000000};
cfg::uint<0, 5> max_b_frames{this, "Max B-Frames", 2}; cfg::uint<0, 5> max_b_frames{this, "Max B-Frames", 2};
cfg::uint<0, 20> gop_size{this, "Group of Pictures Size", 12}; cfg::uint<0, 20> gop_size{this, "Group of Pictures Size", 12};
} video{ this };
struct node_audio : cfg::node
{
node_audio(cfg::node* _this) : cfg::node(_this, "Audio") {}
cfg::uint<0x10000, 0x17000> audio_codec{this, "AVCodecID", 86019}; // AVCodecID::AV_CODEC_ID_AC3
cfg::uint<0, 8> channels{this, "Channels", 2};
cfg::uint<0, 25000000> audio_bps{this, "Audio Bitrate", 320000};
cfg::uint<0, 25000000> sample_rate{this, "Sample Rate", 48000};
} audio{ this };
const std::string path; const std::string path;
}; };

View file

@ -618,7 +618,7 @@
<ClInclude Include="Loader\mself.hpp" /> <ClInclude Include="Loader\mself.hpp" />
<ClInclude Include="util\atomic.hpp" /> <ClInclude Include="util\atomic.hpp" />
<ClInclude Include="util\bless.hpp" /> <ClInclude Include="util\bless.hpp" />
<ClInclude Include="util\image_sink.h" /> <ClInclude Include="util\video_sink.h" />
<ClInclude Include="util\video_provider.h" /> <ClInclude Include="util\video_provider.h" />
<ClInclude Include="util\media_utils.h" /> <ClInclude Include="util\media_utils.h" />
<ClInclude Include="util\serialization.hpp" /> <ClInclude Include="util\serialization.hpp" />

View file

@ -2275,7 +2275,7 @@
<ClInclude Include="util\video_provider.h"> <ClInclude Include="util\video_provider.h">
<Filter>Utilities</Filter> <Filter>Utilities</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="util\image_sink.h"> <ClInclude Include="util\video_sink.h">
<Filter>Utilities</Filter> <Filter>Utilities</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="Emu\Io\recording_config.h"> <ClInclude Include="Emu\Io\recording_config.h">

View file

@ -12,6 +12,7 @@
#include "Emu/IdManager.h" #include "Emu/IdManager.h"
#include "Emu/Cell/Modules/cellScreenshot.h" #include "Emu/Cell/Modules/cellScreenshot.h"
#include "Emu/Cell/Modules/cellVideoOut.h" #include "Emu/Cell/Modules/cellVideoOut.h"
#include "Emu/Cell/Modules/cellAudio.h"
#include "Emu/RSX/rsx_utils.h" #include "Emu/RSX/rsx_utils.h"
#include "Emu/RSX/Overlays/overlay_message.h" #include "Emu/RSX/Overlays/overlay_message.h"
#include "Emu/Io/recording_config.h" #include "Emu/Io/recording_config.h"
@ -445,9 +446,9 @@ void gs_frame::toggle_recording()
{ {
m_video_encoder->stop(); m_video_encoder->stop();
if (!video_provider.set_image_sink(nullptr, recording_mode::rpcs3)) if (!video_provider.set_video_sink(nullptr, recording_mode::rpcs3))
{ {
gui_log.warning("The video provider could not release the image sink. A sink with higher priority must have been set."); gui_log.warning("The video provider could not release the video sink. A sink with higher priority must have been set.");
} }
// Play a sound // Play a sound
@ -489,21 +490,23 @@ void gs_frame::toggle_recording()
video_path += "recording_" + date_time::current_time_narrow<'_'>() + ".mp4"; video_path += "recording_" + date_time::current_time_narrow<'_'>() + ".mp4";
utils::video_encoder::frame_format output_format{}; utils::video_encoder::frame_format output_format{};
output_format.av_pixel_format = static_cast<AVPixelFormat>(g_cfg_recording.pixel_format.get()); output_format.av_pixel_format = static_cast<AVPixelFormat>(g_cfg_recording.video.pixel_format.get());
output_format.width = g_cfg_recording.width; output_format.width = g_cfg_recording.video.width;
output_format.height = g_cfg_recording.height; output_format.height = g_cfg_recording.video.height;
output_format.pitch = g_cfg_recording.width * 4; output_format.pitch = g_cfg_recording.video.width * 4;
m_video_encoder->set_path(video_path); m_video_encoder->set_path(video_path);
m_video_encoder->set_framerate(g_cfg_recording.framerate); m_video_encoder->set_framerate(g_cfg_recording.video.framerate);
m_video_encoder->set_video_bitrate(g_cfg_recording.video_bps); m_video_encoder->set_video_bitrate(g_cfg_recording.video.video_bps);
m_video_encoder->set_video_codec(g_cfg_recording.video_codec); m_video_encoder->set_video_codec(g_cfg_recording.video.video_codec);
m_video_encoder->set_max_b_frames(g_cfg_recording.max_b_frames); m_video_encoder->set_max_b_frames(g_cfg_recording.video.max_b_frames);
m_video_encoder->set_gop_size(g_cfg_recording.gop_size); m_video_encoder->set_gop_size(g_cfg_recording.video.gop_size);
m_video_encoder->set_output_format(output_format); m_video_encoder->set_output_format(output_format);
m_video_encoder->set_sample_rate(0); // TODO m_video_encoder->set_sample_rate(g_cfg_recording.audio.sample_rate);
m_video_encoder->set_audio_bitrate(0); // TODO //m_video_encoder->set_audio_channels(static_cast<u32>(g_fxo->get<cell_audio>().cfg.backend_ch_cnt));
m_video_encoder->set_audio_codec(0); // TODO m_video_encoder->set_audio_channels(static_cast<u32>(g_fxo->get<cell_audio>().cfg.audio_channels));
m_video_encoder->set_audio_bitrate(g_cfg_recording.audio.audio_bps);
m_video_encoder->set_audio_codec(g_cfg_recording.audio.audio_codec);
m_video_encoder->encode(); m_video_encoder->encode();
if (m_video_encoder->has_error) if (m_video_encoder->has_error)
@ -513,9 +516,9 @@ void gs_frame::toggle_recording()
return; return;
} }
if (!video_provider.set_image_sink(m_video_encoder, recording_mode::rpcs3)) if (!video_provider.set_video_sink(m_video_encoder, recording_mode::rpcs3))
{ {
gui_log.warning("The video provider could not set the image sink. A sink with higher priority must have been set."); gui_log.warning("The video provider could not set the video sink. A sink with higher priority must have been set.");
rsx::overlays::queue_message(tr("Recording not possible").toStdString()); rsx::overlays::queue_message(tr("Recording not possible").toStdString());
m_video_encoder->stop(); m_video_encoder->stop();
return; return;

View file

@ -1,54 +0,0 @@
#pragma once
#include "util/types.hpp"
#include "util/atomic.hpp"
#include "Utilities/mutex.h"
#include <deque>
#include <cmath>
namespace utils
{
class image_sink
{
public:
image_sink() = default;
virtual void stop(bool flush = true) = 0;
virtual void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms) = 0;
s64 get_pts(usz timestamp_ms) const
{
return static_cast<s64>(std::round((timestamp_ms * m_framerate) / 1000.f));
}
usz get_timestamp_ms(s64 pts) const
{
return static_cast<usz>(std::round((pts * 1000) / static_cast<float>(m_framerate)));
}
atomic_t<bool> has_error{false};
struct encoder_frame
{
encoder_frame() = default;
encoder_frame(usz timestamp_ms, u32 pitch, u32 width, u32 height, s32 av_pixel_format, std::vector<u8>&& data)
: timestamp_ms(timestamp_ms), pitch(pitch), width(width), height(height), av_pixel_format(av_pixel_format), data(std::move(data))
{}
s64 pts = -1; // Optional
usz timestamp_ms = 0;
u32 pitch = 0;
u32 width = 0;
u32 height = 0;
s32 av_pixel_format = 0; // NOTE: Make sure this is a valid AVPixelFormat
std::vector<u8> data;
};
protected:
shared_mutex m_mtx;
std::deque<encoder_frame> m_frames_to_encode;
atomic_t<bool> m_flush = false;
u32 m_framerate = 0;
};
}

File diff suppressed because it is too large Load diff

View file

@ -88,7 +88,7 @@ namespace utils
std::unique_ptr<named_thread<std::function<void()>>> m_thread; std::unique_ptr<named_thread<std::function<void()>>> m_thread;
}; };
class video_encoder : public utils::image_sink class video_encoder : public utils::video_sink
{ {
public: public:
video_encoder(); video_encoder();
@ -108,7 +108,7 @@ namespace utils
}; };
std::string path() const; std::string path() const;
s64 last_pts() const; s64 last_video_pts() const;
void set_path(const std::string& path); void set_path(const std::string& path);
void set_framerate(u32 framerate); void set_framerate(u32 framerate);
@ -118,16 +118,17 @@ namespace utils
void set_max_b_frames(s32 max_b_frames); void set_max_b_frames(s32 max_b_frames);
void set_gop_size(s32 gop_size); void set_gop_size(s32 gop_size);
void set_sample_rate(u32 sample_rate); void set_sample_rate(u32 sample_rate);
void set_audio_channels(u32 channels);
void set_audio_bitrate(u32 bitrate); void set_audio_bitrate(u32 bitrate);
void set_audio_codec(s32 codec_id); void set_audio_codec(s32 codec_id);
void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms) override;
void pause(bool flush = true); void pause(bool flush = true);
void stop(bool flush = true) override; void stop(bool flush = true) override;
void encode(); void encode();
private: private:
std::string m_path; std::string m_path;
s64 m_last_pts = 0; s64 m_last_audio_pts = 0;
s64 m_last_video_pts = 0;
// Thread control // Thread control
std::unique_ptr<named_thread<std::function<void()>>> m_thread; std::unique_ptr<named_thread<std::function<void()>>> m_thread;
@ -136,14 +137,14 @@ namespace utils
// Video parameters // Video parameters
u32 m_video_bitrate_bps = 0; u32 m_video_bitrate_bps = 0;
s32 m_video_codec_id = 12; // AV_CODEC_ID_MPEG4; s32 m_video_codec_id = 12; // AV_CODEC_ID_MPEG4
s32 m_max_b_frames = 2; s32 m_max_b_frames = 2;
s32 m_gop_size = 12; s32 m_gop_size = 12;
frame_format m_out_format{}; frame_format m_out_format{};
// Audio parameters // Audio parameters
u32 m_sample_rate = 48000; u32 m_channels = 2;
u32 m_audio_bitrate_bps = 96000; u32 m_audio_bitrate_bps = 320000;
s32 m_audio_codec_id = 86018; // AV_CODEC_ID_AAC s32 m_audio_codec_id = 86019; // AV_CODEC_ID_AC3
}; };
} }

View file

@ -34,37 +34,37 @@ namespace utils
g_recording_mode = recording_mode::stopped; g_recording_mode = recording_mode::stopped;
} }
bool video_provider::set_image_sink(std::shared_ptr<image_sink> sink, recording_mode type) bool video_provider::set_video_sink(std::shared_ptr<video_sink> sink, recording_mode type)
{ {
media_log.notice("video_provider: setting new image sink. sink=%d, type=%s", !!sink, type); media_log.notice("video_provider: setting new video sink. sink=%d, type=%s", !!sink, type);
if (type == recording_mode::stopped) if (type == recording_mode::stopped)
{ {
// Prevent misuse. type is supposed to be a valid state. // Prevent misuse. type is supposed to be a valid state.
media_log.error("video_provider: cannot set image sink with type %s", type); media_log.error("video_provider: cannot set video sink with type %s", type);
return false; return false;
} }
std::lock_guard lock(m_mutex); std::lock_guard lock(m_mutex);
if (m_image_sink) if (m_video_sink)
{ {
// cell has preference // cell has preference
if (m_type == recording_mode::cell && m_type != type) if (m_type == recording_mode::cell && m_type != type)
{ {
media_log.warning("video_provider: cannot set image sink with type %s if type %s is active", type, m_type); media_log.warning("video_provider: cannot set video sink with type %s if type %s is active", type, m_type);
return false; return false;
} }
if (m_type != type || m_image_sink != sink) if (m_type != type || m_video_sink != sink)
{ {
media_log.warning("video_provider: stopping current image sink of type %s", m_type); media_log.warning("video_provider: stopping current video sink of type %s", m_type);
m_image_sink->stop(); m_video_sink->stop();
} }
} }
m_type = sink ? type : recording_mode::stopped; m_type = sink ? type : recording_mode::stopped;
m_image_sink = sink; m_video_sink = sink;
if (m_type == recording_mode::stopped) if (m_type == recording_mode::stopped)
{ {
@ -84,19 +84,17 @@ namespace utils
{ {
std::lock_guard lock(m_mutex); std::lock_guard lock(m_mutex);
if (!m_image_sink) if (!m_video_sink)
return false; return false;
const usz timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(steady_clock::now() - m_encoder_start).count() - m_pause_time_ms; const usz timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(steady_clock::now() - m_encoder_start).count() - m_pause_time_ms;
const s64 pts = m_image_sink->get_pts(timestamp_ms); const s64 pts = m_video_sink->get_pts(timestamp_ms);
return pts > m_last_pts_incoming; return pts > m_last_video_pts_incoming;
} }
void video_provider::present_frame(std::vector<u8>& data, u32 pitch, u32 width, u32 height, bool is_bgra) recording_mode video_provider::check_state()
{ {
std::lock_guard lock(m_mutex); if (!m_video_sink || m_video_sink->has_error)
if (!m_image_sink || m_image_sink->has_error)
{ {
g_recording_mode = recording_mode::stopped; g_recording_mode = recording_mode::stopped;
rsx::overlays::queue_message(localized_string_id::RECORDING_ABORTED); rsx::overlays::queue_message(localized_string_id::RECORDING_ABORTED);
@ -105,33 +103,86 @@ namespace utils
if (g_recording_mode == recording_mode::stopped) if (g_recording_mode == recording_mode::stopped)
{ {
m_active = false; m_active = false;
return; return g_recording_mode;
} }
if (!m_active.exchange(true)) if (!m_active.exchange(true))
{ {
m_current_encoder_frame = 0; m_current_encoder_frame = 0;
m_last_pts_incoming = -1; m_last_video_pts_incoming = -1;
m_last_audio_pts_incoming = -1;
} }
if (m_current_encoder_frame == 0) if (m_current_encoder_frame == 0 && m_current_encoder_sample == 0)
{ {
m_encoder_start = steady_clock::now(); m_encoder_start = steady_clock::now();
} }
// Calculate presentation timestamp. return g_recording_mode;
const usz timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(steady_clock::now() - m_encoder_start).count() - m_pause_time_ms; }
const s64 pts = m_image_sink->get_pts(timestamp_ms);
// We can just skip this frame if it has the same timestamp. void video_provider::present_frame(std::vector<u8>& data, u32 pitch, u32 width, u32 height, bool is_bgra)
if (pts <= m_last_pts_incoming) {
std::lock_guard lock(m_mutex);
if (check_state() == recording_mode::stopped)
{ {
return; return;
} }
m_last_pts_incoming = pts; // Calculate presentation timestamp.
const usz timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(steady_clock::now() - m_encoder_start).count() - m_pause_time_ms;
const s64 pts = m_video_sink->get_pts(timestamp_ms);
// We can just skip this frame if it has the same timestamp.
if (pts <= m_last_video_pts_incoming)
{
return;
}
m_last_video_pts_incoming = pts;
m_current_encoder_frame++; m_current_encoder_frame++;
m_image_sink->add_frame(data, pitch, width, height, is_bgra ? AVPixelFormat::AV_PIX_FMT_BGRA : AVPixelFormat::AV_PIX_FMT_RGBA, timestamp_ms); m_video_sink->add_frame(data, pitch, width, height, is_bgra ? AVPixelFormat::AV_PIX_FMT_BGRA : AVPixelFormat::AV_PIX_FMT_RGBA, timestamp_ms);
}
bool video_provider::can_consume_sample()
{
std::lock_guard lock(m_mutex);
if (!m_video_sink)
return false;
const usz timestamp_us = std::chrono::duration_cast<std::chrono::microseconds>(steady_clock::now() - m_encoder_start).count() - (m_pause_time_ms * 1000ull);
const s64 pts = m_video_sink->get_audio_pts(timestamp_us);
return pts > m_last_audio_pts_incoming;
}
void video_provider::present_samples(u8* buf, u32 sample_count, u16 channels)
{
if (!buf || !sample_count || !channels)
{
return;
}
std::lock_guard lock(m_mutex);
if (check_state() == recording_mode::stopped)
{
return;
}
// Calculate presentation timestamp.
const usz timestamp_us = std::chrono::duration_cast<std::chrono::microseconds>(steady_clock::now() - m_encoder_start).count() - (m_pause_time_ms * 1000ull);
const s64 pts = m_video_sink->get_audio_pts(timestamp_us);
// We can just skip this sample if it has the same timestamp.
if (pts <= m_last_audio_pts_incoming)
{
return;
}
m_last_audio_pts_incoming = pts;
m_current_encoder_sample += sample_count;
m_video_sink->add_audio_samples(buf, sample_count, channels, timestamp_us);
} }
} }

View file

@ -1,6 +1,6 @@
#pragma once #pragma once
#include "image_sink.h" #include "video_sink.h"
enum class recording_mode enum class recording_mode
{ {
@ -17,19 +17,27 @@ namespace utils
video_provider() = default; video_provider() = default;
~video_provider(); ~video_provider();
bool set_image_sink(std::shared_ptr<image_sink> sink, recording_mode type); bool set_video_sink(std::shared_ptr<video_sink> sink, recording_mode type);
void set_pause_time(usz pause_time_ms); void set_pause_time(usz pause_time_ms);
bool can_consume_frame(); bool can_consume_frame();
void present_frame(std::vector<u8>& data, u32 pitch, u32 width, u32 height, bool is_bgra); void present_frame(std::vector<u8>& data, u32 pitch, u32 width, u32 height, bool is_bgra);
bool can_consume_sample();
void present_samples(u8* buf, u32 sample_count, u16 channels);
private: private:
recording_mode check_state();
recording_mode m_type = recording_mode::stopped; recording_mode m_type = recording_mode::stopped;
std::shared_ptr<image_sink> m_image_sink; std::shared_ptr<video_sink> m_video_sink;
shared_mutex m_mutex{}; shared_mutex m_mutex{};
atomic_t<bool> m_active{false}; atomic_t<bool> m_active{false};
atomic_t<usz> m_current_encoder_frame{0}; atomic_t<usz> m_current_encoder_frame{0};
atomic_t<usz> m_current_encoder_sample{0};
steady_clock::time_point m_encoder_start{}; steady_clock::time_point m_encoder_start{};
s64 m_last_pts_incoming = -1; s64 m_last_video_pts_incoming = -1;
s64 m_last_audio_pts_incoming = -1;
usz m_pause_time_ms = 0; usz m_pause_time_ms = 0;
}; };

104
rpcs3/util/video_sink.h Normal file
View file

@ -0,0 +1,104 @@
#pragma once
#include "util/types.hpp"
#include "util/atomic.hpp"
#include "Utilities/mutex.h"
#include <deque>
#include <cmath>
namespace utils
{
class video_sink
{
public:
video_sink() = default;
virtual void stop(bool flush = true) = 0;
void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms)
{
// Do not allow new frames while flushing
if (m_flush)
return;
std::lock_guard lock(m_mtx);
m_frames_to_encode.emplace_back(timestamp_ms, pitch, width, height, pixel_format, std::move(frame));
}
void add_audio_samples(u8* buf, u32 sample_count, u16 channels, usz timestamp_us)
{
// Do not allow new samples while flushing
if (m_flush || !buf || !sample_count || !channels)
return;
std::vector<u8> sample(buf, buf + sample_count * channels * sizeof(f32));
std::lock_guard lock(m_audio_mtx);
m_samples_to_encode.emplace_back(timestamp_us, sample_count, channels, std::move(sample));
}
s64 get_pts(usz timestamp_ms) const
{
return static_cast<s64>(std::round((timestamp_ms * m_framerate) / 1000.f));
}
s64 get_audio_pts(usz timestamp_us) const
{
static constexpr f32 us_per_sec = 1000000.0f;
const f32 us_per_block = us_per_sec / (m_sample_rate / static_cast<f32>(m_samples_per_block));
return static_cast<s64>(std::ceil(timestamp_us / us_per_block));
}
usz get_timestamp_ms(s64 pts) const
{
return static_cast<usz>(std::round((pts * 1000) / static_cast<float>(m_framerate)));
}
usz get_audio_timestamp_us(s64 pts) const
{
return static_cast<usz>(std::round((pts * 1000) / static_cast<float>(m_sample_rate)));
}
atomic_t<bool> has_error{false};
struct encoder_frame
{
encoder_frame() = default;
encoder_frame(usz timestamp_ms, u32 pitch, u32 width, u32 height, s32 av_pixel_format, std::vector<u8>&& data)
: timestamp_ms(timestamp_ms), pitch(pitch), width(width), height(height), av_pixel_format(av_pixel_format), data(std::move(data))
{}
s64 pts = -1; // Optional
usz timestamp_ms = 0;
u32 pitch = 0;
u32 width = 0;
u32 height = 0;
s32 av_pixel_format = 0; // NOTE: Make sure this is a valid AVPixelFormat
std::vector<u8> data;
};
struct encoder_sample
{
encoder_sample() = default;
encoder_sample(usz timestamp_us, u32 sample_count, u16 channels, std::vector<u8>&& data)
: timestamp_us(timestamp_us), sample_count(sample_count), channels(channels), data(std::move(data))
{
}
usz timestamp_us = 0;
u32 sample_count = 0;
u16 channels = 0;
std::vector<u8> data;
};
protected:
shared_mutex m_mtx;
std::deque<encoder_frame> m_frames_to_encode;
shared_mutex m_audio_mtx;
std::deque<encoder_sample> m_samples_to_encode;
atomic_t<bool> m_flush = false;
u32 m_framerate = 30;
u32 m_sample_rate = 48000;
static constexpr u32 m_samples_per_block = 256;
};
}