} // namespace
+void QueueLengthPolicy::update_policy(int queue_length)
+{
+ if (queue_length < 0) { // Starvation.
+ if (been_at_safe_point_since_last_starvation && safe_queue_length < 5) {
+ ++safe_queue_length;
+ fprintf(stderr, "Card %u: Starvation, increasing safe limit to %u frames\n",
+ card_index, safe_queue_length);
+ }
+ frames_with_at_least_one = 0;
+ been_at_safe_point_since_last_starvation = false;
+ return;
+ }
+ if (queue_length > 0) {
+ if (queue_length >= int(safe_queue_length)) {
+ been_at_safe_point_since_last_starvation = true;
+ }
+ if (++frames_with_at_least_one >= 50 && safe_queue_length > 0) {
+ --safe_queue_length;
+ fprintf(stderr, "Card %u: Spare frames for more than 50 frames, reducing safe limit to %u frames\n",
+ card_index, safe_queue_length);
+ frames_with_at_least_one = 0;
+ }
+ } else {
+ frames_with_at_least_one = 0;
+ }
+}
+
Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
: httpd(WIDTH, HEIGHT),
num_cards(num_cards),
movit_texel_subpixel_precision /= 2.0;
resource_pool.reset(new ResourcePool);
- theme.reset(new Theme("theme.lua", resource_pool.get(), num_cards));
+ theme.reset(new Theme(global_flags.theme_filename.c_str(), resource_pool.get(), num_cards));
for (unsigned i = 0; i < NUM_OUTPUTS; ++i) {
output_channel[i].parent = this;
}
}
for (card_index = 0; card_index < num_cards; ++card_index) {
+ cards[card_index].queue_length_policy.reset(card_index);
cards[card_index].capture->start_bm_capture();
}
locut.init(FILTER_HPF, 2);
+ // If --flat-audio is given, turn off everything that messes with the sound,
+ // except the final makeup gain.
+ if (global_flags.flat_audio) {
+ set_locut_enabled(false);
+ set_limiter_enabled(false);
+ set_compressor_enabled(false);
+ }
+
// hlen=16 is pretty low quality, but we use quite a bit of CPU otherwise,
// and there's a limit to how important the peak meter is.
peak_resampler.setup(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY * 4, /*num_channels=*/2, /*hlen=*/16, /*frel=*/1.0);
{
unique_lock<mutex> lock(bmusb_mutex);
cards[card_index].should_quit = true; // Unblock thread.
- cards[card_index].new_data_ready_changed.notify_all();
+ cards[card_index].new_frames_changed.notify_all();
}
cards[card_index].capture->stop_dequeue_thread();
}
int64_t frame_length = int64_t(TIMEBASE * video_format.frame_rate_den) / video_format.frame_rate_nom;
- size_t num_samples = (audio_frame.len >= audio_offset) ? (audio_frame.len - audio_offset) / audio_format.num_channels / (audio_format.bits_per_sample / 8) : 0;
+ size_t num_samples = (audio_frame.len > audio_offset) ? (audio_frame.len - audio_offset) / audio_format.num_channels / (audio_format.bits_per_sample / 8) : 0;
if (num_samples > OUTPUT_FREQUENCY / 10) {
printf("Card %d: Dropping frame with implausible audio length (len=%d, offset=%d) [timecode=0x%04x video_len=%d video_offset=%d video_format=%x)\n",
card_index, int(audio_frame.len), int(audio_offset),
vector<float> audio;
audio.resize(num_samples * 2);
switch (audio_format.bits_per_sample) {
+ case 0:
+ assert(num_samples == 0);
+ break;
case 24:
convert_fixed24_to_fp32(&audio[0], 2, audio_frame.data + audio_offset, audio_format.num_channels, num_samples);
break;
audio_frame.owner->release_frame(audio_frame);
}
- {
- // Wait until the previous frame was consumed.
- unique_lock<mutex> lock(bmusb_mutex);
- card->new_data_ready_changed.wait(lock, [card]{ return !card->new_data_ready || card->should_quit; });
- if (card->should_quit) return;
- }
-
size_t expected_length = video_format.width * (video_format.height + video_format.extra_lines_top + video_format.extra_lines_bottom) * 2;
if (video_frame.len - video_offset == 0 ||
video_frame.len - video_offset != expected_length) {
// so that pts can go up accordingly.
{
unique_lock<mutex> lock(bmusb_mutex);
- card->new_data_ready = true;
- card->new_frame = RefCountedFrame(FrameAllocator::Frame());
- card->new_frame_length = frame_length;
- card->new_frame_interlaced = false;
- card->new_data_ready_fence = nullptr;
- card->dropped_frames = dropped_frames;
- card->new_data_ready_changed.notify_all();
+ CaptureCard::NewFrame new_frame;
+ new_frame.frame = RefCountedFrame(FrameAllocator::Frame());
+ new_frame.length = frame_length;
+ new_frame.interlaced = false;
+ new_frame.dropped_frames = dropped_frames;
+ card->new_frames.push(move(new_frame));
+ card->new_frames_changed.notify_all();
}
return;
}
userdata->last_has_signal = video_format.has_signal;
userdata->last_frame_rate_nom = video_format.frame_rate_nom;
userdata->last_frame_rate_den = video_format.frame_rate_den;
- RefCountedFrame new_frame(video_frame);
+ RefCountedFrame frame(video_frame);
// Upload the textures.
size_t cbcr_width = video_format.width / 2;
GLuint pbo = userdata->pbo;
check_error();
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, pbo);
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
check_error();
- glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT);
+ glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, video_frame.size);
check_error();
glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
check_error();
glBindTexture(GL_TEXTURE_2D, 0);
check_error();
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
check_error();
- GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
+ RefCountedGLsync fence(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
check_error();
- assert(fence != nullptr);
+ assert(fence.get() != nullptr);
if (field == 1) {
// Don't upload the second field as fast as we can; wait until
{
unique_lock<mutex> lock(bmusb_mutex);
- card->new_data_ready = true;
- card->new_frame = new_frame;
- card->new_frame_length = frame_length;
- card->new_frame_field = field;
- card->new_frame_interlaced = video_format.interlaced;
- card->new_data_ready_fence = fence;
- card->dropped_frames = dropped_frames;
- card->new_data_ready_changed.notify_all();
-
- if (field != num_fields - 1) {
- // Wait until the previous frame was consumed.
- card->new_data_ready_changed.wait(lock, [card]{ return !card->new_data_ready || card->should_quit; });
- if (card->should_quit) return;
- }
+ CaptureCard::NewFrame new_frame;
+ new_frame.frame = frame;
+ new_frame.length = frame_length;
+ new_frame.field = field;
+ new_frame.interlaced = video_format.interlaced;
+ new_frame.ready_fence = fence;
+ new_frame.dropped_frames = dropped_frames;
+ card->new_frames.push(move(new_frame));
+ card->new_frames_changed.notify_all();
}
}
}
int stats_dropped_frames = 0;
while (!should_quit) {
- CaptureCard card_copy[MAX_CARDS];
- int num_samples[MAX_CARDS];
+ CaptureCard::NewFrame new_frames[MAX_CARDS];
+ bool has_new_frame[MAX_CARDS] = { false };
+ int num_samples[MAX_CARDS] = { 0 };
- {
- unique_lock<mutex> lock(bmusb_mutex);
+ // TODO: Add a timeout.
+ unsigned master_card_index = theme->map_signal(master_clock_channel);
+ assert(master_card_index < num_cards);
- // The first card is the master timer, so wait for it to have a new frame.
- // TODO: Make configurable, and with a timeout.
- cards[0].new_data_ready_changed.wait(lock, [this]{ return cards[0].new_data_ready; });
-
- for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
- CaptureCard *card = &cards[card_index];
- card_copy[card_index].new_data_ready = card->new_data_ready;
- card_copy[card_index].new_frame = card->new_frame;
- card_copy[card_index].new_frame_length = card->new_frame_length;
- card_copy[card_index].new_frame_field = card->new_frame_field;
- card_copy[card_index].new_frame_interlaced = card->new_frame_interlaced;
- card_copy[card_index].new_data_ready_fence = card->new_data_ready_fence;
- card_copy[card_index].dropped_frames = card->dropped_frames;
- card->new_data_ready = false;
- card->new_data_ready_changed.notify_all();
-
- int num_samples_times_timebase = OUTPUT_FREQUENCY * card->new_frame_length + card->fractional_samples;
- num_samples[card_index] = num_samples_times_timebase / TIMEBASE;
- card->fractional_samples = num_samples_times_timebase % TIMEBASE;
- assert(num_samples[card_index] >= 0);
- }
- }
+ get_one_frame_from_each_card(master_card_index, new_frames, has_new_frame, num_samples);
+ schedule_audio_resampling_tasks(new_frames[master_card_index].dropped_frames, num_samples[master_card_index], new_frames[master_card_index].length);
+ stats_dropped_frames += new_frames[master_card_index].dropped_frames;
+ send_audio_level_callback();
- // Resample the audio as needed, including from previously dropped frames.
- assert(num_cards > 0);
- for (unsigned frame_num = 0; frame_num < card_copy[0].dropped_frames + 1; ++frame_num) {
- {
- // Signal to the audio thread to process this frame.
- unique_lock<mutex> lock(audio_mutex);
- audio_task_queue.push(AudioTask{pts_int, num_samples[0]});
- audio_task_queue_changed.notify_one();
- }
- if (frame_num != card_copy[0].dropped_frames) {
- // For dropped frames, increase the pts. Note that if the format changed
- // in the meantime, we have no way of detecting that; we just have to
- // assume the frame length is always the same.
- ++stats_dropped_frames;
- pts_int += card_copy[0].new_frame_length;
+ for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+ if (card_index == master_card_index || !has_new_frame[card_index]) {
+ continue;
}
- }
-
- if (audio_level_callback != nullptr) {
- unique_lock<mutex> lock(compressor_mutex);
- double loudness_s = r128.loudness_S();
- double loudness_i = r128.integrated();
- double loudness_range_low = r128.range_min();
- double loudness_range_high = r128.range_max();
-
- audio_level_callback(loudness_s, 20.0 * log10(peak),
- loudness_i, loudness_range_low, loudness_range_high,
- gain_staging_db, 20.0 * log10(final_makeup_gain),
- correlation.get_correlation());
- }
-
- for (unsigned card_index = 1; card_index < num_cards; ++card_index) {
- if (card_copy[card_index].new_data_ready && card_copy[card_index].new_frame->len == 0) {
- ++card_copy[card_index].dropped_frames;
+ if (new_frames[card_index].frame->len == 0) {
+ ++new_frames[card_index].dropped_frames;
}
- if (card_copy[card_index].dropped_frames > 0) {
+ if (new_frames[card_index].dropped_frames > 0) {
printf("Card %u dropped %d frames before this\n",
- card_index, int(card_copy[card_index].dropped_frames));
+ card_index, int(new_frames[card_index].dropped_frames));
}
}
// If the first card is reporting a corrupted or otherwise dropped frame,
// just increase the pts (skipping over this frame) and don't try to compute anything new.
- if (card_copy[0].new_frame->len == 0) {
+ if (new_frames[master_card_index].frame->len == 0) {
++stats_dropped_frames;
- pts_int += card_copy[0].new_frame_length;
+ pts_int += new_frames[master_card_index].length;
continue;
}
for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
- CaptureCard *card = &card_copy[card_index];
- if (!card->new_data_ready || card->new_frame->len == 0)
+ if (!has_new_frame[card_index] || new_frames[card_index].frame->len == 0)
continue;
- assert(card->new_frame != nullptr);
- insert_new_frame(card->new_frame, card->new_frame_field, card->new_frame_interlaced, card_index, &input_state);
+ CaptureCard::NewFrame *new_frame = &new_frames[card_index];
+ assert(new_frame->frame != nullptr);
+ insert_new_frame(new_frame->frame, new_frame->field, new_frame->interlaced, card_index, &input_state);
check_error();
// The new texture might still be uploaded,
// tell the GPU to wait until it's there.
- if (card->new_data_ready_fence) {
- glWaitSync(card->new_data_ready_fence, /*flags=*/0, GL_TIMEOUT_IGNORED);
+ if (new_frame->ready_fence) {
+ glWaitSync(new_frame->ready_fence.get(), /*flags=*/0, GL_TIMEOUT_IGNORED);
check_error();
- glDeleteSync(card->new_data_ready_fence);
+ new_frame->ready_fence.reset();
check_error();
}
}
- // Get the main chain from the theme, and set its state immediately.
- Theme::Chain theme_main_chain = theme->get_chain(0, pts(), WIDTH, HEIGHT, input_state);
- EffectChain *chain = theme_main_chain.chain;
- theme_main_chain.setup_chain();
- //theme_main_chain.chain->enable_phase_timing(true);
-
- GLuint y_tex, cbcr_tex;
- bool got_frame = h264_encoder->begin_frame(&y_tex, &cbcr_tex);
- assert(got_frame);
-
- // Render main chain.
- GLuint cbcr_full_tex = resource_pool->create_2d_texture(GL_RG8, WIDTH, HEIGHT);
- GLuint rgba_tex = resource_pool->create_2d_texture(GL_RGB565, WIDTH, HEIGHT); // Saves texture bandwidth, although dithering gets messed up.
- GLuint fbo = resource_pool->create_fbo(y_tex, cbcr_full_tex, rgba_tex);
- check_error();
- chain->render_to_fbo(fbo, WIDTH, HEIGHT);
- resource_pool->release_fbo(fbo);
-
- subsample_chroma(cbcr_full_tex, cbcr_tex);
- resource_pool->release_2d_texture(cbcr_full_tex);
-
- // Set the right state for rgba_tex.
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- glBindTexture(GL_TEXTURE_2D, rgba_tex);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-
- RefCountedGLsync fence(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
- check_error();
-
- const int64_t av_delay = TIMEBASE / 10; // Corresponds to the fixed delay in resampling_queue.h. TODO: Make less hard-coded.
- h264_encoder->end_frame(fence, pts_int + av_delay, theme_main_chain.input_frames);
+ render_one_frame();
++frame;
- pts_int += card_copy[0].new_frame_length;
-
- // The live frame just shows the RGBA texture we just rendered.
- // It owns rgba_tex now.
- DisplayFrame live_frame;
- live_frame.chain = display_chain.get();
- live_frame.setup_chain = [this, rgba_tex]{
- display_input->set_texture_num(rgba_tex);
- };
- live_frame.ready_fence = fence;
- live_frame.input_frames = {};
- live_frame.temp_textures = { rgba_tex };
- output_channel[OUTPUT_LIVE].output_frame(live_frame);
-
- // Set up preview and any additional channels.
- for (int i = 1; i < theme->get_num_channels() + 2; ++i) {
- DisplayFrame display_frame;
- Theme::Chain chain = theme->get_chain(i, pts(), WIDTH, HEIGHT, input_state); // FIXME: dimensions
- display_frame.chain = chain.chain;
- display_frame.setup_chain = chain.setup_chain;
- display_frame.ready_fence = fence;
- display_frame.input_frames = chain.input_frames;
- display_frame.temp_textures = {};
- output_channel[i].output_frame(display_frame);
- }
+ pts_int += new_frames[master_card_index].length;
clock_gettime(CLOCK_MONOTONIC, &now);
double elapsed = now.tv_sec - start.tv_sec +
resource_pool->clean_context();
}
+void Mixer::get_one_frame_from_each_card(unsigned master_card_index, CaptureCard::NewFrame new_frames[MAX_CARDS], bool has_new_frame[MAX_CARDS], int num_samples[MAX_CARDS])
+{
+ // The first card is the master timer, so wait for it to have a new frame.
+ unique_lock<mutex> lock(bmusb_mutex);
+ cards[master_card_index].new_frames_changed.wait(lock, [this, master_card_index]{ return !cards[master_card_index].new_frames.empty(); });
+
+ for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+ CaptureCard *card = &cards[card_index];
+ if (card->new_frames.empty()) {
+ assert(card_index != master_card_index);
+ card->queue_length_policy.update_policy(-1);
+ continue;
+ }
+ new_frames[card_index] = move(card->new_frames.front());
+ has_new_frame[card_index] = true;
+ card->new_frames.pop();
+ card->new_frames_changed.notify_all();
+
+ int num_samples_times_timebase = OUTPUT_FREQUENCY * new_frames[card_index].length + card->fractional_samples;
+ num_samples[card_index] = num_samples_times_timebase / TIMEBASE;
+ card->fractional_samples = num_samples_times_timebase % TIMEBASE;
+ assert(num_samples[card_index] >= 0);
+
+ if (card_index == master_card_index) {
+ // We don't use the queue length policy for the master card,
+ // but we will if it stops being the master. Thus, clear out
+ // the policy in case we switch in the future.
+ card->queue_length_policy.reset(card_index);
+ } else {
+ // If we have excess frames compared to the policy for this card,
+ // drop frames from the head.
+ card->queue_length_policy.update_policy(card->new_frames.size());
+ while (card->new_frames.size() > card->queue_length_policy.get_safe_queue_length()) {
+ card->new_frames.pop();
+ }
+ }
+ }
+}
+
+void Mixer::schedule_audio_resampling_tasks(unsigned dropped_frames, int num_samples_per_frame, int length_per_frame)
+{
+ // Resample the audio as needed, including from previously dropped frames.
+ assert(num_cards > 0);
+ for (unsigned frame_num = 0; frame_num < dropped_frames + 1; ++frame_num) {
+ {
+ // Signal to the audio thread to process this frame.
+ unique_lock<mutex> lock(audio_mutex);
+ audio_task_queue.push(AudioTask{pts_int, num_samples_per_frame});
+ audio_task_queue_changed.notify_one();
+ }
+ if (frame_num != dropped_frames) {
+ // For dropped frames, increase the pts. Note that if the format changed
+ // in the meantime, we have no way of detecting that; we just have to
+ // assume the frame length is always the same.
+ pts_int += length_per_frame;
+ }
+ }
+}
+
+void Mixer::render_one_frame()
+{
+ // Get the main chain from the theme, and set its state immediately.
+ Theme::Chain theme_main_chain = theme->get_chain(0, pts(), WIDTH, HEIGHT, input_state);
+ EffectChain *chain = theme_main_chain.chain;
+ theme_main_chain.setup_chain();
+ //theme_main_chain.chain->enable_phase_timing(true);
+
+ GLuint y_tex, cbcr_tex;
+ bool got_frame = h264_encoder->begin_frame(&y_tex, &cbcr_tex);
+ assert(got_frame);
+
+ // Render main chain.
+ GLuint cbcr_full_tex = resource_pool->create_2d_texture(GL_RG8, WIDTH, HEIGHT);
+ GLuint rgba_tex = resource_pool->create_2d_texture(GL_RGB565, WIDTH, HEIGHT); // Saves texture bandwidth, although dithering gets messed up.
+ GLuint fbo = resource_pool->create_fbo(y_tex, cbcr_full_tex, rgba_tex);
+ check_error();
+ chain->render_to_fbo(fbo, WIDTH, HEIGHT);
+ resource_pool->release_fbo(fbo);
+
+ subsample_chroma(cbcr_full_tex, cbcr_tex);
+ resource_pool->release_2d_texture(cbcr_full_tex);
+
+ // Set the right state for rgba_tex.
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glBindTexture(GL_TEXTURE_2D, rgba_tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ RefCountedGLsync fence(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
+ check_error();
+
+ const int64_t av_delay = TIMEBASE / 10; // Corresponds to the fixed delay in resampling_queue.h. TODO: Make less hard-coded.
+ h264_encoder->end_frame(fence, pts_int + av_delay, theme_main_chain.input_frames);
+
+ // The live frame just shows the RGBA texture we just rendered.
+ // It owns rgba_tex now.
+ DisplayFrame live_frame;
+ live_frame.chain = display_chain.get();
+ live_frame.setup_chain = [this, rgba_tex]{
+ display_input->set_texture_num(rgba_tex);
+ };
+ live_frame.ready_fence = fence;
+ live_frame.input_frames = {};
+ live_frame.temp_textures = { rgba_tex };
+ output_channel[OUTPUT_LIVE].output_frame(live_frame);
+
+ // Set up preview and any additional channels.
+ for (int i = 1; i < theme->get_num_channels() + 2; ++i) {
+ DisplayFrame display_frame;
+ Theme::Chain chain = theme->get_chain(i, pts(), WIDTH, HEIGHT, input_state); // FIXME: dimensions
+ display_frame.chain = chain.chain;
+ display_frame.setup_chain = chain.setup_chain;
+ display_frame.ready_fence = fence;
+ display_frame.input_frames = chain.input_frames;
+ display_frame.temp_textures = {};
+ output_channel[i].output_frame(display_frame);
+ }
+}
+
+void Mixer::send_audio_level_callback()
+{
+ if (audio_level_callback == nullptr) {
+ return;
+ }
+
+ unique_lock<mutex> lock(compressor_mutex);
+ double loudness_s = r128.loudness_S();
+ double loudness_i = r128.integrated();
+ double loudness_range_low = r128.range_min();
+ double loudness_range_high = r128.range_max();
+
+ audio_level_callback(loudness_s, 20.0 * log10(peak),
+ loudness_i, loudness_range_low, loudness_range_high,
+ gain_staging_db, 20.0 * log10(final_makeup_gain),
+ correlation.get_correlation());
+}
+
void Mixer::audio_thread_func()
{
while (!should_quit) {