/* * CINELERRA * Copyright (C) 2008-2019 Adam Williams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "bcdisplayinfo.h" #include "bchash.h" #include "bcsignals.h" #include "asset.h" #include "clip.h" #include "compressor.h" #include "cursors.h" #include "edl.h" #include "edlsession.h" #include "filexml.h" #include "language.h" #include "samples.h" #include "theme.h" #include "tracking.inc" #include "transportque.inc" #include "units.h" #include "vframe.h" #include #include REGISTER_PLUGIN(CompressorEffect) // More potential compressor algorithms: // Use single readahead time parameter. Negative readahead time uses // readahead. Positive readahead time uses slope. // Smooth input stage if readahead. // Determine slope from current smoothed sample to every sample in readahead area. // Once highest slope is found, count of number of samples remaining until it is // reached. Only search after this count for the next highest slope. // Use highest slope to determine smoothed value. // Smooth input stage if not readahead. // For every sample, calculate slope needed to reach current sample from // current smoothed value in the readahead time. If higher than current slope, // make it the current slope and count number of samples remaining until it is // reached. If this count is met and no higher slopes are found, base slope // on current sample when count is met. // Gain stage. // For every sample, calculate gain from smoothed input value. CompressorEffect::CompressorEffect(PluginServer *server) : PluginAClient(server) { input_buffer = 0; input_size = 0; input_allocated = 0; input_start = 0; need_reconfigure = 1; engine = 0; } CompressorEffect::~CompressorEffect() { if( input_buffer ) { for( int i=0; i input_allocated ) { Samples **new_input_buffer = new Samples*[channels]; for( int i=0; iget_data(), input_buffer[i]->get_data(), input_size * sizeof(double)); delete input_buffer[i]; } if( input_buffer ) delete [] input_buffer; input_allocated = size; input_buffer = new_input_buffer; } } const char* CompressorEffect::plugin_title() { return N_("Compressor"); } int CompressorEffect::is_realtime() { return 1; } int CompressorEffect::is_multichannel() { return 1; } void CompressorEffect::read_data(KeyFrame *keyframe) { FileXML input; BandConfig *band_config = &config.bands[0]; input.set_shared_input(keyframe->xbuf); int result = 0; while( !(result=input.read_tag()) ) { if( input.tag.title_is("COMPRESSOR") ) { // band_config->readahead_len = input.tag.get_property("READAHEAD_LEN", band_config->readahead_len); band_config->attack_len = input.tag.get_property("ATTACK_LEN", band_config->attack_len); band_config->release_len = input.tag.get_property("RELEASE_LEN", band_config->release_len); config.trigger = input.tag.get_property("TRIGGER", config.trigger); config.smoothing_only = input.tag.get_property("SMOOTHING_ONLY", config.smoothing_only); config.input = input.tag.get_property("INPUT", config.input); } else if( input.tag.title_is("COMPRESSORBAND") ) { band_config->read_data(&input, 0); } } config.boundaries(); } void CompressorEffect::save_data(KeyFrame *keyframe) { FileXML output; BandConfig *band_config = &config.bands[0]; output.set_shared_output(keyframe->xbuf); output.tag.set_title("COMPRESSOR"); output.tag.set_property("TRIGGER", config.trigger); output.tag.set_property("SMOOTHING_ONLY", config.smoothing_only); output.tag.set_property("INPUT", config.input); output.tag.set_property("ATTACK_LEN", band_config->attack_len); output.tag.set_property("RELEASE_LEN", band_config->release_len); output.append_tag(); output.append_newline(); band_config->save_data(&output, 0, 0); output.tag.set_title("/COMPRESSOR"); output.append_tag(); output.append_newline(); output.terminate_string(); } void CompressorEffect::update_gui() { if( !thread ) return; // user can't change levels when loading configuration thread->window->lock_window("CompressorEffect::update_gui"); CompressorWindow *window = (CompressorWindow *)thread->window; // Can't update points if the user is editing int reconfigured = window->canvas->is_dragging() ? 0 : load_configuration(); if( reconfigured ) window->update(); // delete frames up to current tracking position int dir = get_tracking_direction() == PLAY_FORWARD ? 1 : -1; double tracking_position = get_tracking_position(); CompressorGainFrame *gain_frame = (CompressorGainFrame *) get_gui_frame(tracking_position, dir); if( gain_frame ) { window->update_meter(gain_frame); delete gain_frame; } window->unlock_window(); } void CompressorEffect::render_stop() { if( !thread ) return; thread->window->lock_window("CompressorEffect::render_stop"); CompressorWindow *window = (CompressorWindow *)thread->window; window->in->reset(); window->gain_change->update(1, 0); window->unlock_window(); } LOAD_CONFIGURATION_MACRO(CompressorEffect, CompressorConfig) NEW_WINDOW_MACRO(CompressorEffect, CompressorWindow) int CompressorEffect::process_buffer(int64_t size, Samples **buffer, int64_t start_position, int sample_rate) { int channels = PluginClient::total_in_buffers; BandConfig *band_config = &config.bands[0]; int sign = get_direction() == PLAY_REVERSE ? -1 : 1; load_configuration(); // restart after seeking if( last_position != start_position ) { last_position = start_position; if( engine ) engine->reset(); input_size = 0; input_start = start_position; } // Calculate linear transfer from db int nbands = band_config->levels.size(); while( levels.size() < nbands ) levels.append(); for( int i=0; ilevels.total; ++i ) { levels[i].x = DB::fromdb(band_config->levels[i].x); levels[i].y = DB::fromdb(band_config->levels[i].y); } // min_x = DB::fromdb(config.min_db); // min_y = DB::fromdb(config.min_db); // max_x = 1.0; // max_y = 1.0; int attack_samples; int release_samples; int preview_samples; if( !engine ) { engine = new CompressorEngine(&config, 0); engine->gui_frame_samples = sample_rate / TRACKING_RATE + 1; } engine->calculate_ranges(&attack_samples, &release_samples, &preview_samples, sample_rate); // Start of new buffer is outside the current buffer. Start buffer over. if( get_direction() == PLAY_FORWARD && (start_position < input_start || start_position >= input_start + input_size) || get_direction() == PLAY_REVERSE && (start_position > input_start || start_position <= input_start - input_size) ) { // printf("CompressorEffect::process_buffer %d start_position=%ld input_start=%ld input_size=%ld\n", // __LINE__, start_position, input_start, input_size); input_size = 0; input_start = start_position; } else // Shift current buffer so the buffer starts on start_position if( get_direction() == PLAY_FORWARD && start_position > input_start && start_position < input_start + input_size || get_direction() == PLAY_REVERSE && start_position < input_start && start_position > input_start - input_size ) { if( input_buffer ) { int len, offset; if( get_direction() == PLAY_FORWARD ) { len = input_start + input_size - start_position; offset = start_position - input_start; } else { len = start_position - (input_start - input_size); offset = input_start - start_position; } for( int i = 0; i < channels; i++ ) { memcpy(input_buffer[i]->get_data(), input_buffer[i]->get_data() + offset, len * sizeof(double)); } input_size = len; input_start = start_position; } } // Expand buffer to handle preview size if( size + preview_samples > input_allocated ) { Samples **new_input_buffer = new Samples*[channels]; for( int i = 0; i < channels; i++ ) { new_input_buffer[i] = new Samples(size + preview_samples); if( input_buffer ) { memcpy(new_input_buffer[i]->get_data(), input_buffer[i]->get_data(), input_size * sizeof(double)); delete input_buffer[i]; } } if( input_buffer ) delete [] input_buffer; input_allocated = size + preview_samples; input_buffer = new_input_buffer; } // Append data to input buffer to construct readahead area. if( input_size < size + preview_samples ) { int fragment_size = size + preview_samples - input_size; for( int i = 0; i < channels; i++ ) { input_buffer[i]->set_offset(input_size); read_samples(input_buffer[i], i, sample_rate, input_start + input_size * sign, fragment_size); input_buffer[i]->set_offset(0); } input_size += fragment_size; } engine->process(buffer, input_buffer, size, sample_rate, PluginClient::total_in_buffers, start_position); double start_pos = (double)start_position / sample_rate; for( int i = 0; i < engine->gui_gains.size(); i++ ) { CompressorGainFrame *gain_frame = new CompressorGainFrame(); gain_frame->position = start_pos + sign*engine->gui_offsets[i]; gain_frame->gain = engine->gui_gains[i]; gain_frame->level = engine->gui_levels[i]; add_gui_frame(gain_frame); } last_position += sign * size; return 0; } CompressorConfig::CompressorConfig() : CompressorConfigBase(1) { } void CompressorConfig::copy_from(CompressorConfig &that) { CompressorConfigBase::copy_from(that); } int CompressorConfig::equivalent(CompressorConfig &that) { return CompressorConfigBase::equivalent(that); } void CompressorConfig::interpolate(CompressorConfig &prev, CompressorConfig &next, int64_t prev_frame, int64_t next_frame, int64_t current_frame) { copy_from(prev); } CompressorWindow::CompressorWindow(CompressorEffect *plugin) : PluginClientWindow(plugin, xS(650), yS(480), xS(650), yS(480), 0) { this->plugin = plugin; } CompressorWindow::~CompressorWindow() { // delete readahead; delete attack; delete release; delete trigger; delete x_text; delete y_text; } void CompressorWindow::create_objects() { int margin = client->get_theme()->widget_border; int x = margin, y = margin; int control_margin = xS(130); int canvas_y2 = get_h()-yS(35); BC_Title *title; add_subwindow(title = new BC_Title(x, y, "In:")); int y2 = y + title->get_h() + margin; EDLSession *session = plugin->get_edl()->session; add_subwindow(in = new BC_Meter(x, y2, METER_VERT, canvas_y2 - y2, session->min_meter_db, session->max_meter_db, session->meter_format, 1, // use_titles -1)); // span x += in->get_w() + margin; add_subwindow(title = new BC_Title(x, y, "Gain:")); add_subwindow(gain_change = new BC_Meter(x, y2, METER_VERT, canvas_y2 - y2, MIN_GAIN_CHANGE, MAX_GAIN_CHANGE, METER_DB, 1, // use_titles -1, // span 0, // is_downmix 1)); // is_gain_change // gain_change->update(1, 0); x += gain_change->get_w() + xS(35); add_subwindow(title = new BC_Title(x, y, _("Sound level (Press shift to snap to grid):"))); y += title->get_h() + 1; add_subwindow(canvas = new CompressorCanvas(plugin, this, x, y, get_w() - x - control_margin - xS(10), canvas_y2 - y)); x = get_w() - control_margin; // add_subwindow(new BC_Title(x, y, _("Lookahead secs:"))); // y += title->get_h() + margin; // readahead = new CompressorLookAhead(plugin, this, x, y); // readahead->create_objects(); // y += readahead->get_h() + margin; add_subwindow(new BC_Title(x, y, _("Attack secs:"))); y += title->get_h() + margin; attack = new CompressorAttack(plugin, this, x, y); attack->create_objects(); y += attack->get_h() + margin; add_subwindow(title = new BC_Title(x, y, _("Release secs:"))); y += title->get_h() + margin; release = new CompressorRelease(plugin, this, x, y); release->create_objects(); y += release->get_h() + margin; add_subwindow(title = new BC_Title(x, y, _("Trigger Type:"))); y += title->get_h() + margin; add_subwindow(input = new CompressorInput(plugin, x, y)); input->create_objects(); y += input->get_h() + margin; add_subwindow(title = new BC_Title(x, y, _("Trigger:"))); y += title->get_h() + margin; trigger = new CompressorTrigger(plugin, this, x, y); trigger->create_objects(); if( plugin->config.input != CompressorConfig::TRIGGER ) trigger->disable(); y += trigger->get_h() + margin; add_subwindow(smooth = new CompressorSmooth(plugin, x, y)); y += smooth->get_h() + margin; add_subwindow(title = new BC_Title(x, y, _("Output:"))); y += title->get_h(); y_text = new CompressorY(plugin, this, x, y); y_text->create_objects(); y += y_text->get_h() + margin; add_subwindow(title = new BC_Title(x, y, _("Input:"))); y += title->get_h(); x_text = new CompressorX(plugin, this, x, y); x_text->create_objects(); y += x_text->get_h() + margin; add_subwindow(clear = new CompressorClear(plugin, this, x, y)); y += clear->get_h() + margin; add_subwindow(title = new BC_Title(x, y, "Gain:")); y += title->get_h() + margin; BandConfig *band_config = &plugin->config.bands[0]; add_subwindow(mkup_gain = new CompressorMkupGain(plugin, this, x, y, &band_config->mkup_gain, -10., 10.)); y += mkup_gain->get_h() + margin; add_subwindow(reset = new CompressorReset(plugin, this, x, y)); // y += reset->get_h() + margin; x = xS(10); y = get_h() - yS(40); canvas->create_objects(); canvas->update(); show_window(); } void CompressorWindow::update() { update_textboxes(); BandConfig *band_config = &plugin->config.bands[0]; mkup_gain->update(band_config->mkup_gain); canvas->update(); } void CompressorWindow::update_meter(CompressorGainFrame *gain_frame) { gain_change->update(gain_frame->gain, 0); in->update(gain_frame->level, 0); } void CompressorWindow::update_textboxes() { BandConfig *band_config = &plugin->config.bands[0]; if( atol(trigger->get_text()) != plugin->config.trigger ) trigger->update((int64_t)plugin->config.trigger); if( strcmp(input->get_text(), CompressorInput::value_to_text(plugin->config.input)) ) input->set_text(CompressorInput::value_to_text(plugin->config.input)); if( plugin->config.input != CompressorConfig::TRIGGER && trigger->get_enabled() ) trigger->disable(); else if( plugin->config.input == CompressorConfig::TRIGGER && !trigger->get_enabled() ) trigger->enable(); // if( !EQUIV(atof(readahead->get_text()), band_config->readahead_len) ) // readahead->update((float)band_config->readahead_len); if( !EQUIV(atof(attack->get_text()), band_config->attack_len) ) attack->update((float)band_config->attack_len); if( !EQUIV(atof(release->get_text()), band_config->release_len) ) release->update((float)band_config->release_len); smooth->update(plugin->config.smoothing_only); if( canvas->current_operation == CompressorCanvas::DRAG ) { x_text->update((float)band_config->levels[canvas->current_point].x); y_text->update((float)band_config->levels[canvas->current_point].y); } } int CompressorWindow::resize_event(int w, int h) { return 1; } CompressorCanvas::CompressorCanvas(CompressorEffect *plugin, CompressorWindow *window, int x, int y, int w, int h) : CompressorCanvasBase(&plugin->config, plugin, window, x, y, w, h) { } void CompressorCanvas::update_window() { ((CompressorWindow*)window)->update(); } // CompressorLookAhead::CompressorLookAhead(CompressorEffect *plugin, // CompressorWindow *window, int x, int y) // : BC_TumbleTextBox(window, (float)plugin->config.bands[0].readahead_len, // (float)MIN_LOOKAHEAD, (float)MAX_LOOKAHEAD, x, y, xS(100)) // { // this->plugin = plugin; // set_increment(0.1); // set_precision(2); // } // // int CompressorLookAhead::handle_event() // { // plugin->config.bands[0].readahead_len = atof(get_text()); // plugin->send_configure_change(); // return 1; // } // // CompressorAttack::CompressorAttack(CompressorEffect *plugin, CompressorWindow *window, int x, int y) : BC_TumbleTextBox(window, (float)plugin->config.bands[0].attack_len, (float)MIN_ATTACK, (float)MAX_ATTACK, x, y, xS(100)) { this->plugin = plugin; set_increment(0.1); set_precision(2); } int CompressorAttack::handle_event() { plugin->config.bands[0].attack_len = atof(get_text()); plugin->send_configure_change(); return 1; } CompressorRelease::CompressorRelease(CompressorEffect *plugin, CompressorWindow *window, int x, int y) : BC_TumbleTextBox(window, (float)plugin->config.bands[0].release_len, (float)MIN_DECAY, (float)MAX_DECAY, x, y, xS(100)) { this->plugin = plugin; set_increment(0.1); set_precision(2); } int CompressorRelease::handle_event() { plugin->config.bands[0].release_len = atof(get_text()); plugin->send_configure_change(); return 1; } CompressorX::CompressorX(CompressorEffect *plugin, CompressorWindow *window, int x, int y) : BC_TumbleTextBox(window, (float)0.0, plugin->config.min_db, plugin->config.max_db, x, y, xS(100)) { this->plugin = plugin; this->window = window; set_increment(0.1); set_precision(2); } int CompressorX::handle_event() { BandConfig *band_config = &plugin->config.bands[0]; int current_point = ((CompressorWindow*)plugin->thread->window)->canvas->current_point; if( current_point < band_config->levels.total ) { band_config->levels[current_point].x = atof(get_text()); window->canvas->update(); plugin->send_configure_change(); } return 1; } CompressorY::CompressorY(CompressorEffect *plugin, CompressorWindow *window, int x, int y) : BC_TumbleTextBox(window, (float)0.0, plugin->config.min_db, plugin->config.max_db, x, y, xS(100)) { this->plugin = plugin; this->window = window; set_increment(0.1); set_precision(2); } int CompressorY::handle_event() { BandConfig *band_config = &plugin->config.bands[0]; int current_point = ((CompressorWindow*)plugin->thread->window)->canvas->current_point; if( current_point < band_config->levels.total ) { band_config->levels[current_point].y = atof(get_text()); window->canvas->update(); plugin->send_configure_change(); } return 1; } CompressorTrigger::CompressorTrigger(CompressorEffect *plugin, CompressorWindow *window, int x, int y) : BC_TumbleTextBox(window, (int)plugin->config.trigger, MIN_TRIGGER, MAX_TRIGGER, x, y, xS(100)) { this->plugin = plugin; set_increment(1); } int CompressorTrigger::handle_event() { plugin->config.trigger = atol(get_text()); plugin->send_configure_change(); return 1; } CompressorInput::CompressorInput(CompressorEffect *plugin, int x, int y) : BC_PopupMenu(x, y, xS(100), CompressorInput::value_to_text(plugin->config.input), 1) { this->plugin = plugin; } int CompressorInput::handle_event() { plugin->config.input = text_to_value(get_text()); ((CompressorWindow*)plugin->thread->window)->update(); plugin->send_configure_change(); return 1; } void CompressorInput::create_objects() { for( int i = 0; i < 3; i++ ) { add_item(new BC_MenuItem(value_to_text(i))); } } const char* CompressorInput::value_to_text(int value) { switch( value ) { case CompressorConfig::TRIGGER: return "Trigger"; case CompressorConfig::MAX: return "Maximum"; case CompressorConfig::SUM: return "Total"; } return "Trigger"; } int CompressorInput::text_to_value(char *text) { for( int i = 0; i < 3; i++ ) { if( !strcmp(value_to_text(i), text) ) return i; } return CompressorConfig::TRIGGER; } CompressorClear::CompressorClear(CompressorEffect *plugin, CompressorWindow *window, int x, int y) : BC_GenericButton(x, y, _("Clear")) { this->plugin = plugin; this->window = window; } int CompressorClear::handle_event() { BandConfig *band_config = &plugin->config.bands[0]; band_config->reset(); //plugin->config.dump(); window->update(); plugin->send_configure_change(); return 1; } CompressorReset::CompressorReset(CompressorEffect *plugin, CompressorWindow *window, int x, int y) : BC_GenericButton(x, y, _("Reset")) { this->plugin = plugin; this->window = window; } int CompressorReset::handle_event() { plugin->config.reset_bands(); plugin->config.reset_base(); //plugin->config.dump(); window->update(); plugin->send_configure_change(); return 1; } CompressorMkupGain::CompressorMkupGain(CompressorEffect *plugin, CompressorWindow *window, int x, int y, double *output, double min, double max) : BC_FPot(x, y, *output, min, max) { this->plugin = plugin; this->window = window; this->output = output; set_precision(0.01); } int CompressorMkupGain::handle_event() { *output = get_value(); plugin->send_configure_change(); window->canvas->update(); return 1; } CompressorSmooth::CompressorSmooth(CompressorEffect *plugin, int x, int y) : BC_CheckBox(x, y, plugin->config.smoothing_only, _("Smooth only")) { this->plugin = plugin; } int CompressorSmooth::handle_event() { plugin->config.smoothing_only = get_value(); plugin->send_configure_change(); return 1; }