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;
+ output_channel[i].channel = i;
}
ImageFormat inout_format;
// except the final makeup gain.
if (global_flags.flat_audio) {
set_locut_enabled(false);
+ set_gain_staging_auto(false);
set_limiter_enabled(false);
set_compressor_enabled(false);
}
// 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);
- alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
+ if (global_flags.enable_alsa_output) {
+ alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
+ }
}
Mixer::~Mixer()
card->frame_allocator.reset(new PBOFrameAllocator(8 << 20, WIDTH, HEIGHT)); // 8 MB.
card->capture->set_video_frame_allocator(card->frame_allocator.get());
card->surface = create_surface(format);
- card->capture->set_dequeue_thread_callbacks(
- [card]{
- eglBindAPI(EGL_OPENGL_API);
- card->context = create_context(card->surface);
- if (!make_current(card->context, card->surface)) {
- printf("failed to create bmusb context\n");
- exit(1);
- }
- },
- [this]{
- resource_pool->clean_context();
- });
- card->resampling_queue.reset(new ResamplingQueue(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
+ card->resampling_queue.reset(new ResamplingQueue(card_index, OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
card->capture->configure_card();
}
}
}
- int64_t frame_length = int64_t(TIMEBASE * video_format.frame_rate_den) / video_format.frame_rate_nom;
+ int64_t frame_length = int64_t(TIMEBASE) * video_format.frame_rate_den / video_format.frame_rate_nom;
+ assert(frame_length > 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) {
if (dropped_frames > MAX_FPS * 2) {
fprintf(stderr, "Card %d lost more than two seconds (or time code jumping around; from 0x%04x to 0x%04x), resetting resampler\n",
card_index, card->last_timecode, timecode);
- card->resampling_queue.reset(new ResamplingQueue(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
+ card->resampling_queue.reset(new ResamplingQueue(card_index, OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
dropped_frames = 0;
} else if (dropped_frames > 0) {
// Insert silence as needed.
size_t y_offset = video_frame.size / 2 + video_offset / 2;
for (unsigned field = 0; field < num_fields; ++field) {
- unsigned field_start_line = (field == 1) ? video_format.second_field_start : video_format.extra_lines_top + field * (video_format.height + 22);
-
- if (userdata->tex_y[field] == 0 ||
- userdata->tex_cbcr[field] == 0 ||
- video_format.width != userdata->last_width[field] ||
- video_format.height != userdata->last_height[field]) {
- // We changed resolution since last use of this texture, so we need to create
- // a new object. Note that this each card has its own PBOFrameAllocator,
- // we don't need to worry about these flip-flopping between resolutions.
+ // Put the actual texture upload in a lambda that is executed in the main thread.
+ // It is entirely possible to do this in the same thread (and it might even be
+ // faster, depending on the GPU and driver), but it appears to be trickling
+ // driver bugs very easily.
+ //
+ // Note that this means we must hold on to the actual frame data in <userdata>
+ // until the upload command is run, but we hold on to <frame> much longer than that
+ // (in fact, all the way until we no longer use the texture in rendering).
+ auto upload_func = [field, video_format, y_offset, cbcr_offset, cbcr_width, userdata]() {
+ unsigned field_start_line = (field == 1) ? video_format.second_field_start : video_format.extra_lines_top + field * (video_format.height + 22);
+
+ if (userdata->tex_y[field] == 0 ||
+ userdata->tex_cbcr[field] == 0 ||
+ video_format.width != userdata->last_width[field] ||
+ video_format.height != userdata->last_height[field]) {
+ // We changed resolution since last use of this texture, so we need to create
+ // a new object. Note that this each card has its own PBOFrameAllocator,
+ // we don't need to worry about these flip-flopping between resolutions.
+ glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
+ check_error();
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, video_format.height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
+ check_error();
+ glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
+ check_error();
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, video_format.width, video_format.height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+ check_error();
+ userdata->last_width[field] = video_format.width;
+ userdata->last_height[field] = video_format.height;
+ }
+
+ GLuint pbo = userdata->pbo;
+ check_error();
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ check_error();
+
+ size_t field_y_start = y_offset + video_format.width * field_start_line;
+ size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
+
+ if (global_flags.flush_pbos) {
+ glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_y_start, video_format.width * video_format.height);
+ check_error();
+ glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_cbcr_start, cbcr_width * video_format.height * sizeof(uint16_t));
+ check_error();
+ }
+
glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
check_error();
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, video_format.height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, cbcr_width, video_format.height, GL_RG, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_cbcr_start));
check_error();
glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
check_error();
- glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, video_format.width, video_format.height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, video_format.width, video_format.height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_y_start));
check_error();
- userdata->last_width[field] = video_format.width;
- userdata->last_height[field] = video_format.height;
- }
-
- GLuint pbo = userdata->pbo;
- check_error();
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
- check_error();
-
- size_t field_y_start = y_offset + video_format.width * field_start_line;
- size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
-
- if (global_flags.flush_pbos) {
- glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_y_start, video_format.width * video_format.height);
+ glBindTexture(GL_TEXTURE_2D, 0);
check_error();
- glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_cbcr_start, cbcr_width * video_format.height * sizeof(uint16_t));
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
check_error();
- }
-
- glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
- check_error();
- glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, cbcr_width, video_format.height, GL_RG, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_cbcr_start));
- check_error();
- glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
- check_error();
- glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, video_format.width, video_format.height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_y_start));
- check_error();
- glBindTexture(GL_TEXTURE_2D, 0);
- check_error();
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
- check_error();
- RefCountedGLsync fence(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
- check_error();
- assert(fence.get() != nullptr);
- glFlush(); // Make sure the main thread doesn't have to wait until we push out enough frames to make a new command buffer.
- check_error();
+ };
if (field == 1) {
// Don't upload the second field as fast as we can; wait until
new_frame.length = frame_length;
new_frame.field = field;
new_frame.interlaced = video_format.interlaced;
- new_frame.ready_fence = fence;
+ new_frame.upload_func = upload_func;
new_frame.dropped_frames = dropped_frames;
card->new_frames.push(move(new_frame));
card->new_frames_changed.notify_all();
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 (new_frame->ready_fence) {
- glWaitSync(new_frame->ready_fence.get(), /*flags=*/0, GL_TIMEOUT_IGNORED);
- check_error();
- new_frame->ready_fence.reset();
- check_error();
+ // The new texture might need uploading before use.
+ if (new_frame->upload_func) {
+ new_frame->upload_func();
+ new_frame->upload_func = nullptr;
}
}
{
unique_lock<mutex> lock(audio_mutex);
- audio_task_queue_changed.wait(lock, [this]{ return !audio_task_queue.empty(); });
+ audio_task_queue_changed.wait(lock, [this]{ return should_quit || !audio_task_queue.empty(); });
+ if (should_quit) {
+ return;
+ }
task = audio_task_queue.front();
audio_task_queue.pop();
}
samples_card.resize(num_samples * 2);
{
unique_lock<mutex> lock(cards[card_index].audio_mutex);
- if (!cards[card_index].resampling_queue->get_output_samples(double(frame_pts_int) / TIMEBASE, &samples_card[0], num_samples)) {
- printf("Card %d reported previous underrun.\n", card_index);
- }
+ cards[card_index].resampling_queue->get_output_samples(double(frame_pts_int) / TIMEBASE, &samples_card[0], num_samples);
}
if (card_index == selected_audio_card) {
samples_out = move(samples_card);
void Mixer::quit()
{
should_quit = true;
+ audio_task_queue_changed.notify_one();
mixer_thread.join();
audio_thread.join();
}
has_ready_frame = true;
}
- if (has_new_frame_ready_callback) {
+ if (new_frame_ready_callback) {
new_frame_ready_callback();
}
+
+ // Reduce the number of callbacks by filtering duplicates. The reason
+ // why we bother doing this is that Qt seemingly can get into a state
+ // where its builds up an essentially unbounded queue of signals,
+ // consuming more and more memory, and there's no good way of collapsing
+ // user-defined signals or limiting the length of the queue.
+ if (transition_names_updated_callback) {
+ vector<string> transition_names = global_mixer->get_transition_names();
+ bool changed = false;
+ if (transition_names.size() != last_transition_names.size()) {
+ changed = true;
+ } else {
+ for (unsigned i = 0; i < transition_names.size(); ++i) {
+ if (transition_names[i] != last_transition_names[i]) {
+ changed = true;
+ break;
+ }
+ }
+ }
+ if (changed) {
+ transition_names_updated_callback(transition_names);
+ last_transition_names = transition_names;
+ }
+ }
+ if (name_updated_callback) {
+ string name = global_mixer->get_channel_name(channel);
+ if (name != last_name) {
+ name_updated_callback(name);
+ last_name = name;
+ }
+ }
+ if (color_updated_callback) {
+ string color = global_mixer->get_channel_color(channel);
+ if (color != last_color) {
+ color_updated_callback(color);
+ last_color = color;
+ }
+ }
}
bool Mixer::OutputChannel::get_display_frame(DisplayFrame *frame)
void Mixer::OutputChannel::set_frame_ready_callback(Mixer::new_frame_ready_callback_t callback)
{
new_frame_ready_callback = callback;
- has_new_frame_ready_callback = true;
+}
+
+void Mixer::OutputChannel::set_transition_names_updated_callback(Mixer::transition_names_updated_callback_t callback)
+{
+ transition_names_updated_callback = callback;
+}
+
+void Mixer::OutputChannel::set_name_updated_callback(Mixer::name_updated_callback_t callback)
+{
+ name_updated_callback = callback;
+}
+
+void Mixer::OutputChannel::set_color_updated_callback(Mixer::color_updated_callback_t callback)
+{
+ color_updated_callback = callback;
}
mutex RefCountedGLsync::fence_lock;