X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=mixer.cpp;h=6f9bf05d725d73aebc47639fdc0d26cad55ca608;hb=bc5f55d6b555a82eb1b770509ccde0f41ade0d0a;hp=308f22c99937a1b830f1b74e75902fa579c20962;hpb=0323944a99ea3c0e175f0f3d004fa42fecd02ece;p=nageru diff --git a/mixer.cpp b/mixer.cpp index 308f22c..6f9bf05 100644 --- a/mixer.cpp +++ b/mixer.cpp @@ -7,27 +7,20 @@ #include "mixer.h" #include -#include -#include -#include #include -#include -#include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include #include #include #include -#include -#include -#include +#include #include #include #include @@ -35,6 +28,7 @@ #include #include #include +#include #include #include "bmusb/bmusb.h" @@ -70,22 +64,27 @@ void convert_fixed24_to_fp32(float *dst, size_t out_channels, const uint8_t *src } // namespace -Mixer::Mixer(const QSurfaceFormat &format) +Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) : httpd("test.ts", WIDTH, HEIGHT), + num_cards(num_cards), mixer_surface(create_surface(format)), - h264_encoder_surface(create_surface(format)) + h264_encoder_surface(create_surface(format)), + level_compressor(48000.0f) { httpd.start(9095); CHECK(init_movit(MOVIT_SHADER_DIR, MOVIT_DEBUG_OFF)); check_error(); + // Since we allow non-bouncing 4:2:2 YCbCrInputs, effective subpixel precision + // will be halved when sampling them, and we need to compensate here. + movit_texel_subpixel_precision /= 2.0; + resource_pool.reset(new ResourcePool); - theme.reset(new Theme("theme.lua", resource_pool.get())); - output_channel[OUTPUT_LIVE].parent = this; - output_channel[OUTPUT_PREVIEW].parent = this; - output_channel[OUTPUT_INPUT0].parent = this; - output_channel[OUTPUT_INPUT1].parent = this; + theme.reset(new Theme("theme.lua", resource_pool.get(), num_cards)); + for (unsigned i = 0; i < NUM_OUTPUTS; ++i) { + output_channel[i].parent = this; + } ImageFormat inout_format; inout_format.color_space = COLORSPACE_sRGB; @@ -102,7 +101,7 @@ Mixer::Mixer(const QSurfaceFormat &format) h264_encoder.reset(new H264Encoder(h264_encoder_surface, WIDTH, HEIGHT, &httpd)); - for (int card_index = 0; card_index < NUM_CARDS; ++card_index) { + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { printf("Configuring card %d...\n", card_index); CaptureCard *card = &cards[card_index]; card->usb = new BMUSBCapture(card_index); @@ -113,7 +112,7 @@ Mixer::Mixer(const QSurfaceFormat &format) card->usb->set_dequeue_thread_callbacks( [card]{ eglBindAPI(EGL_OPENGL_API); - card->context = create_context(); + card->context = create_context(card->surface); if (!make_current(card->context, card->surface)) { printf("failed to create bmusb context\n"); exit(1); @@ -128,7 +127,7 @@ Mixer::Mixer(const QSurfaceFormat &format) BMUSBCapture::start_bm_thread(); - for (int card_index = 0; card_index < NUM_CARDS; ++card_index) { + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { cards[card_index].usb->start_bm_capture(); } @@ -147,7 +146,8 @@ Mixer::Mixer(const QSurfaceFormat &format) "} \n"; cbcr_program_num = resource_pool->compile_glsl_program(cbcr_vert_shader, cbcr_frag_shader); - r128_state = ebur128_init(2, 48000, EBUR128_MODE_SAMPLE_PEAK | EBUR128_MODE_M | EBUR128_MODE_S | EBUR128_MODE_I | EBUR128_MODE_LRA); + r128.init(2, 48000); + r128.integr_start(); } Mixer::~Mixer() @@ -155,7 +155,7 @@ Mixer::~Mixer() resource_pool->release_glsl_program(cbcr_program_num); BMUSBCapture::stop_bm_thread(); - for (int card_index = 0; card_index < NUM_CARDS; ++card_index) { + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { { unique_lock lock(bmusb_mutex); cards[card_index].should_quit = true; // Unblock thread. @@ -163,8 +163,6 @@ Mixer::~Mixer() } cards[card_index].usb->stop_dequeue_thread(); } - - ebur128_destroy(&r128_state); } namespace { @@ -179,9 +177,33 @@ int unwrap_timecode(uint16_t current_wrapped, int last) } } +float find_peak(const vector &samples) +{ + float m = fabs(samples[0]); + for (size_t i = 1; i < samples.size(); ++i) { + m = std::max(m, fabs(samples[i])); + } + return m; +} + +void deinterleave_samples(const vector &in, vector *out_l, vector *out_r) +{ + size_t num_samples = in.size() / 2; + out_l->resize(num_samples); + out_r->resize(num_samples); + + const float *inptr = in.data(); + float *lptr = &(*out_l)[0]; + float *rptr = &(*out_r)[0]; + for (size_t i = 0; i < num_samples; ++i) { + *lptr++ = *inptr++; + *rptr++ = *inptr++; + } +} + } // namespace -void Mixer::bm_frame(int card_index, uint16_t timecode, +void Mixer::bm_frame(unsigned card_index, uint16_t timecode, FrameAllocator::Frame video_frame, size_t video_offset, uint16_t video_format, FrameAllocator::Frame audio_frame, size_t audio_offset, uint16_t audio_format) { @@ -200,12 +222,6 @@ void Mixer::bm_frame(int card_index, uint16_t timecode, return; } - // Convert the audio to stereo fp32 and add it. - size_t num_samples = (audio_frame.len - audio_offset) / 8 / 3; - vector audio; - audio.resize(num_samples * 2); - convert_fixed24_to_fp32(&audio[0], 2, audio_frame.data + audio_offset, 8, num_samples); - int unwrapped_timecode = timecode; int dropped_frames = 0; if (card->last_timecode != -1) { @@ -214,6 +230,12 @@ void Mixer::bm_frame(int card_index, uint16_t timecode, } card->last_timecode = unwrapped_timecode; + // Convert the audio to stereo fp32 and add it. + size_t num_samples = (audio_frame.len >= audio_offset) ? (audio_frame.len - audio_offset) / 8 / 3 : 0; + vector audio; + audio.resize(num_samples * 2); + convert_fixed24_to_fp32(&audio[0], 2, audio_frame.data + audio_offset, 8, num_samples); + // Add the audio. { unique_lock lock(card->audio_mutex); @@ -308,7 +330,7 @@ void Mixer::bm_frame(int card_index, uint16_t timecode, void Mixer::thread_func() { eglBindAPI(EGL_OPENGL_API); - QOpenGLContext *context = create_context(); + QOpenGLContext *context = create_context(mixer_surface); if (!make_current(context, mixer_surface)) { printf("oops\n"); exit(1); @@ -321,7 +343,7 @@ void Mixer::thread_func() int dropped_frames = 0; while (!should_quit) { - CaptureCard card_copy[NUM_CARDS]; + CaptureCard card_copy[MAX_CARDS]; { unique_lock lock(bmusb_mutex); @@ -330,13 +352,12 @@ void Mixer::thread_func() // TODO: Make configurable, and with a timeout. cards[0].new_data_ready_changed.wait(lock, [this]{ return cards[0].new_data_ready; }); - for (int card_index = 0; card_index < NUM_CARDS; ++card_index) { + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { CaptureCard *card = &cards[card_index]; card_copy[card_index].usb = card->usb; 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_data_ready_fence = card->new_data_ready_fence; - card_copy[card_index].new_frame_audio = move(card->new_frame_audio); card_copy[card_index].dropped_frames = card->dropped_frames; card->new_data_ready = false; card->new_data_ready_changed.notify_all(); @@ -344,22 +365,8 @@ void Mixer::thread_func() } // Resample the audio as needed, including from previously dropped frames. - vector samples_out; - // TODO: Allow using audio from the other card(s) as well. for (unsigned frame_num = 0; frame_num < card_copy[0].dropped_frames + 1; ++frame_num) { - for (unsigned card_index = 0; card_index < NUM_CARDS; ++card_index) { - samples_out.resize((48000 / 60) * 2); - { - unique_lock lock(cards[card_index].audio_mutex); - if (!cards[card_index].resampler->get_output_samples(pts(), &samples_out[0], 48000 / 60)) { - printf("Card %d reported previous underrun.\n", card_index); - } - } - if (card_index == 0) { - ebur128_add_frames_float(r128_state, samples_out.data(), samples_out.size() / 2); - h264_encoder->add_audio(pts_int, move(samples_out)); - } - } + process_audio_one_frame(); if (frame_num != card_copy[0].dropped_frames) { // For dropped frames, increase the pts. ++dropped_frames; @@ -368,22 +375,25 @@ void Mixer::thread_func() } if (audio_level_callback != nullptr) { - double loudness_s, loudness_i, peak_level_l, peak_level_r; - double lra; - ebur128_loudness_shortterm(r128_state, &loudness_s); - ebur128_loudness_global(r128_state, &loudness_i); - ebur128_loudness_range(r128_state, &lra); - ebur128_sample_peak(r128_state, 0, &peak_level_l); - ebur128_sample_peak(r128_state, 1, &peak_level_r); - - // FIXME: This is wrong. We need proper support from libebur128 for this. - double loudness_range_low = loudness_i - 0.5 * lra; - double loudness_range_high = loudness_i + 0.5 * lra; - - audio_level_callback(loudness_s, 20.0 * log10(max(peak_level_l, peak_level_r)), + 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); } + 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 (card_copy[card_index].dropped_frames > 0) { + printf("Card %u dropped %d frames before this\n", + card_index, int(card_copy[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) { @@ -392,7 +402,7 @@ void Mixer::thread_func() continue; } - for (int card_index = 0; card_index < NUM_CARDS; ++card_index) { + 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) continue; @@ -426,6 +436,7 @@ void Mixer::thread_func() 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); @@ -446,7 +457,7 @@ void Mixer::thread_func() // input frames needed, so that they are not released back // until the rendering is done. vector input_frames; - for (int card_index = 0; card_index < NUM_CARDS; ++card_index) { + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { input_frames.push_back(bmusb_current_rendering_frame[card_index]); } const int64_t av_delay = TIMEBASE / 10; // Corresponds to the fixed delay in resampler.h. TODO: Make less hard-coded. @@ -473,7 +484,11 @@ void Mixer::thread_func() display_frame.chain = chain.first; display_frame.setup_chain = chain.second; display_frame.ready_fence = fence; - display_frame.input_frames = { bmusb_current_rendering_frame[0], bmusb_current_rendering_frame[1] }; // FIXME: possible to do better? + + // FIXME: possible to do better? + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { + display_frame.input_frames.push_back(bmusb_current_rendering_frame[card_index]); + } display_frame.temp_textures = {}; output_channel[i].output_frame(display_frame); } @@ -504,6 +519,62 @@ void Mixer::thread_func() resource_pool->clean_context(); } +void Mixer::process_audio_one_frame() +{ + vector samples_card; + vector samples_out; + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { + samples_card.resize((48000 / 60) * 2); + { + unique_lock lock(cards[card_index].audio_mutex); + if (!cards[card_index].resampler->get_output_samples(pts(), &samples_card[0], 48000 / 60)) { + printf("Card %d reported previous underrun.\n", card_index); + } + } + // TODO: Allow using audio from the other card(s) as well. + if (card_index == 0) { + samples_out = move(samples_card); + } + } + + // Apply a level compressor to get the general level right. + // Basically, if it's over about -40 dBFS, we squeeze it down to that level + // (or more precisely, near it, since we don't use infinite ratio), + // then apply a makeup gain to get it to -12 dBFS. -12 dBFS is, of course, + // entirely arbitrary, but from practical tests with speech, it seems to + // put ut around -23 LUFS, so it's a reasonable starting point for later use. + // + // TODO: Hook this up to a UI, so we can see the effects, and/or turn it off + // to control the gain manually instead. For now, there's only the #if-ed out + // code below. + // + // TODO: Add the actual compressors/limiters (for taking care of transients) + // later in the chain. + float threshold = 0.01f; // -40 dBFS. + float ratio = 20.0f; + float attack_time = 0.1f; + float release_time = 10.0f; + float makeup_gain = pow(10.0f, 28.0f / 20.0f); // +28 dB takes us to -12 dBFS. + level_compressor.process(samples_out.data(), samples_out.size() / 2, threshold, ratio, attack_time, release_time, makeup_gain); + +#if 0 + printf("level=%f (%+5.2f dBFS) attenuation=%f (%+5.2f dB) end_result=%+5.2f dB\n", + compressor.get_level(), 20.0 * log10(compressor.get_level()), + compressor.get_attenuation(), 20.0 * log10(compressor.get_attenuation()), + 20.0 * log10(compressor.get_level() * compressor.get_attenuation() * makeup_gain)); +#endif + + // Find peak and R128 levels. + peak = std::max(peak, find_peak(samples_out)); + vector left, right; + deinterleave_samples(samples_out, &left, &right); + float *ptrs[] = { left.data(), right.data() }; + r128.process(left.size(), ptrs); + + // Actually add the samples to the output. + h264_encoder->add_audio(pts_int, move(samples_out)); +} + void Mixer::subsample_chroma(GLuint src_tex, GLuint dst_tex) { GLuint vao;