X-Git-Url: http://git.cinelerra-gg.org/git/?a=blobdiff_plain;f=cinelerra-5.1%2Fplugins%2Fcompressor%2Fcompressor.C;h=956d2ae7279bc41beb3b9723f0bf52f61e685aa4;hb=0e6cf5b52d1ebce9272270144bcf43df4683507e;hp=486a498e4f3b13116c5b15e50cbb869950bd2f2d;hpb=0df48ad2d876409c5beeae2e21933a728ea76c33;p=goodguy%2Fcinelerra.git diff --git a/cinelerra-5.1/plugins/compressor/compressor.C b/cinelerra-5.1/plugins/compressor/compressor.C index 486a498e..956d2ae7 100644 --- a/cinelerra-5.1/plugins/compressor/compressor.C +++ b/cinelerra-5.1/plugins/compressor/compressor.C @@ -1,7 +1,6 @@ - /* * CINELERRA - * Copyright (C) 2008 Adam Williams + * 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 @@ -20,35 +19,31 @@ */ #include "bcdisplayinfo.h" +#include "bchash.h" #include "bcsignals.h" +#include "asset.h" #include "clip.h" #include "compressor.h" #include "cursors.h" -#include "bchash.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 reaction time parameter. Negative reaction time uses -// readahead. Positive reaction time uses slope. +// 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. @@ -58,7 +53,7 @@ REGISTER_PLUGIN(CompressorEffect) // Smooth input stage if not readahead. // For every sample, calculate slope needed to reach current sample from -// current smoothed value in the reaction time. If higher than current slope, +// 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. @@ -66,123 +61,97 @@ REGISTER_PLUGIN(CompressorEffect) // Gain stage. // For every sample, calculate gain from smoothed input value. - - - - CompressorEffect::CompressorEffect(PluginServer *server) : PluginAClient(server) { - reset(); + input_buffer = 0; + input_size = 0; + input_allocated = 0; + input_start = 0; + need_reconfigure = 1; + engine = 0; } CompressorEffect::~CompressorEffect() { - - delete_dsp(); - levels.remove_all(); -} - -void CompressorEffect::delete_dsp() -{ - if(input_buffer) - { - for(int i = 0; i < PluginClient::total_in_buffers; i++) + 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; - next_target = 1.0; - previous_target = 1.0; - target_samples = 1; - target_current_sample = -1; - current_value = 1.0; + 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; - input.set_shared_input(keyframe->xbuf); + BandConfig *band_config = &config.bands[0]; + input.set_shared_input(keyframe->xbuf); int result = 0; - config.levels.remove_all(); - while(!result) - { - result = input.read_tag(); - - if(!result) - { - if(input.tag.title_is("COMPRESSOR")) - { - config.reaction_len = input.tag.get_property("REACTION_LEN", config.reaction_len); - config.decay_len = input.tag.get_property("DECAY_LEN", config.decay_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("LEVEL")) - { - double x = input.tag.get_property("X", (double)0); - double y = input.tag.get_property("Y", (double)0); - compressor_point_t point = { x, y }; - - config.levels.append(point); - } + 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("REACTION_LEN", config.reaction_len); - output.tag.set_property("DECAY_LEN", config.decay_len); 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(); - - - for(int i = 0; i < config.levels.total; i++) - { - output.tag.set_title("LEVEL"); - output.tag.set_property("X", config.levels.values[i].x); - output.tag.set_property("Y", config.levels.values[i].y); - - output.append_tag(); - output.tag.set_title("/LEVEL"); - output.append_tag(); - output.append_newline(); - } - output.terminate_string(); } @@ -190,469 +159,184 @@ void CompressorEffect::save_data(KeyFrame *keyframe) void CompressorEffect::update_gui() { if( !thread ) return; - CompressorWindow *window = (CompressorWindow*)thread->window; -// load_configuration,read_data deletes levels - window->lock_window("CompressorEffect::update_gui"); - if( load_configuration() ) +// 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 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(); -// Calculate linear transfer from db - levels.remove_all(); - for(int i = 0; i < config.levels.total; i++) - { - levels.append(); - levels.values[i].x = DB::fromdb(config.levels.values[i].x); - levels.values[i].y = DB::fromdb(config.levels.values[i].y); +// restart after seeking + if( last_position != start_position ) { + last_position = start_position; + if( engine ) + engine->reset(); + input_size = 0; + input_start = start_position; } - min_x = DB::fromdb(config.min_db); - min_y = DB::fromdb(config.min_db); - max_x = 1.0; - max_y = 1.0; - - - int reaction_samples = (int)(config.reaction_len * sample_rate + 0.5); - int decay_samples = (int)(config.decay_len * sample_rate + 0.5); - int trigger = CLIP(config.trigger, 0, PluginAClient::total_in_buffers - 1); - CLAMP(reaction_samples, -1000000, 1000000); - CLAMP(decay_samples, reaction_samples, 1000000); - CLAMP(decay_samples, 1, 1000000); - if(labs(reaction_samples) < 1) reaction_samples = 1; - if(labs(decay_samples) < 1) decay_samples = 1; - - int total_buffers = get_total_buffers(); - if(reaction_samples >= 0) - { - if(target_current_sample < 0) target_current_sample = reaction_samples; - for(int i = 0; i < total_buffers; i++) - { - read_samples(buffer[i], - i, - sample_rate, - start_position, - size); - } - - double current_slope = (next_target - previous_target) / - reaction_samples; - double *trigger_buffer = buffer[trigger]->get_data(); - for(int i = 0; i < size; i++) - { -// Get slope required to reach current sample from smoothed sample over reaction -// length. - double sample = 0.; - switch(config.input) - { - case CompressorConfig::MAX: - { - double max = 0; - for(int j = 0; j < total_buffers; j++) - { - sample = fabs(buffer[j]->get_data()[i]); - if(sample > max) max = sample; - } - sample = max; - break; - } - - case CompressorConfig::TRIGGER: - sample = fabs(trigger_buffer[i]); - break; - - case CompressorConfig::SUM: - { - double max = 0; - for(int j = 0; j < total_buffers; j++) - { - sample = fabs(buffer[j]->get_data()[i]); - max += sample; - } - sample = max; - break; - } - } +// Calculate linear transfer from db + int nbands = band_config->levels.size(); + while( levels.size() < nbands ) levels.append(); - double new_slope = (sample - current_value) / - reaction_samples; - -// Slope greater than current slope - if(new_slope >= current_slope && - (current_slope >= 0 || - new_slope >= 0)) - { - next_target = sample; - previous_target = current_value; - target_current_sample = 0; - target_samples = reaction_samples; - current_slope = new_slope; - } - else - if(sample > next_target && current_slope < 0) - { - next_target = sample; - previous_target = current_value; - target_current_sample = 0; - target_samples = decay_samples; - current_slope = (sample - current_value) / decay_samples; - } -// Current smoothed sample came up without finding higher slope - if(target_current_sample >= target_samples) - { - next_target = sample; - previous_target = current_value; - target_current_sample = 0; - target_samples = decay_samples; - current_slope = (sample - current_value) / decay_samples; - } + for( int i=0; ilevels.total; ++i ) { + levels.values[i].x = DB::fromdb(band_config->levels.values[i].x); + levels.values[i].y = DB::fromdb(band_config->levels.values[i].y); + } +// min_x = DB::fromdb(config.min_db); +// min_y = DB::fromdb(config.min_db); +// max_x = 1.0; +// max_y = 1.0; -// Update current value and store gain - current_value = (next_target * target_current_sample + - previous_target * (target_samples - target_current_sample)) / - target_samples; - target_current_sample++; + int attack_samples; + int release_samples; + int preview_samples; - if(config.smoothing_only) - { - for(int j = 0; j < total_buffers; j++) - buffer[j]->get_data()[i] = current_value; - } - else - { - double gain = calculate_gain(current_value); - for(int j = 0; j < total_buffers; j++) - { - buffer[j]->get_data()[i] *= gain; - } - } - } + if( !engine ) { + engine = new CompressorEngine(&config, 0); + engine->gui_frame_samples = sample_rate / TRACKING_RATE + 1; } - else - { - if(target_current_sample < 0) target_current_sample = target_samples; - int64_t preview_samples = -reaction_samples; + engine->calculate_ranges(&attack_samples, &release_samples, + &preview_samples, sample_rate); // Start of new buffer is outside the current buffer. Start buffer over. - if(start_position < input_start || - start_position >= input_start + input_size) - { - input_size = 0; - input_start = start_position; - } - else + 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(start_position > input_start && - start_position < input_start + input_size) - { - if(input_buffer) - { - int len = input_start + input_size - start_position; - for(int i = 0; i < total_buffers; i++) - { - memcpy(input_buffer[i]->get_data(), - input_buffer[i]->get_data() + (start_position - input_start), - len * sizeof(double)); - } - input_size = len; - input_start = 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; } - } - -// Expand buffer to handle preview size - if(size + preview_samples > input_allocated) - { - Samples **new_input_buffer = new Samples*[total_buffers]; - for(int i = 0; i < total_buffers; 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]; - } + else { + len = start_position - (input_start - input_size); + offset = input_start - start_position; } - 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. -#define MAX_FRAGMENT_SIZE 131072 - while(input_size < size + preview_samples) - { - int fragment_size = MAX_FRAGMENT_SIZE; - if(fragment_size + input_size > size + preview_samples) - fragment_size = size + preview_samples - input_size; - for(int i = 0; i < total_buffers; i++) - { - input_buffer[i]->set_offset(input_size); -//printf("CompressorEffect::process_buffer %d %p %d\n", __LINE__, input_buffer[i], input_size); - read_samples(input_buffer[i], - i, - sample_rate, - input_start + input_size, - fragment_size); - input_buffer[i]->set_offset(0); + for( int i = 0; i < channels; i++ ) { + memcpy(input_buffer[i]->get_data(), + input_buffer[i]->get_data() + offset, + len * sizeof(double)); } - input_size += fragment_size; + input_size = len; + input_start = start_position; } + } - - double current_slope = (next_target - previous_target) / - target_samples; - double *trigger_buffer = input_buffer[trigger]->get_data(); - for(int i = 0; i < size; i++) - { -// Get slope from current sample to every sample in preview_samples. -// Take highest one or first one after target_samples are up. - -// For optimization, calculate the first slope we really need. -// Assume every slope up to the end of preview_samples has been calculated and -// found <= to current slope. - int first_slope = preview_samples - 1; -// Need new slope immediately - if(target_current_sample >= target_samples) - first_slope = 1; - for(int j = first_slope; - j < preview_samples; - j++) - { - double sample = 0.; - switch(config.input) - { - case CompressorConfig::MAX: - { - double max = 0; - for(int k = 0; k < total_buffers; k++) - { - sample = fabs(input_buffer[k]->get_data()[i + j]); - if(sample > max) max = sample; - } - sample = max; - break; - } - - case CompressorConfig::TRIGGER: - sample = fabs(trigger_buffer[i + j]); - break; - - case CompressorConfig::SUM: - { - double max = 0; - for(int k = 0; k < total_buffers; k++) - { - sample = fabs(input_buffer[k]->get_data()[i + j]); - max += sample; - } - sample = max; - break; - } - } - - - - - - - double new_slope = (sample - current_value) / - j; -// Got equal or higher slope - if(new_slope >= current_slope && - (current_slope >= 0 || - new_slope >= 0)) - { - target_current_sample = 0; - target_samples = j; - current_slope = new_slope; - next_target = sample; - previous_target = current_value; - } - else - if(sample > next_target && current_slope < 0) - { - target_current_sample = 0; - target_samples = decay_samples; - current_slope = (sample - current_value) / - decay_samples; - next_target = sample; - previous_target = current_value; - } - -// Hit end of current slope range without finding higher slope - if(target_current_sample >= target_samples) - { - target_current_sample = 0; - target_samples = decay_samples; - current_slope = (sample - current_value) / decay_samples; - next_target = sample; - previous_target = current_value; - } - } - -// Update current value and multiply gain - current_value = (next_target * target_current_sample + - previous_target * (target_samples - target_current_sample)) / - target_samples; -//buffer[0][i] = current_value; - target_current_sample++; - - if(config.smoothing_only) - { - for(int j = 0; j < total_buffers; j++) - { - buffer[j]->get_data()[i] = current_value; - } - } - else - { - double gain = calculate_gain(current_value); - for(int j = 0; j < total_buffers; j++) - { - buffer[j]->get_data()[i] = input_buffer[j]->get_data()[i] * gain; - } +// 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; } - - - - - return 0; -} - -double CompressorEffect::calculate_output(double x) -{ - if(x > 0.999) return 1.0; - - for(int i = levels.total - 1; i >= 0; i--) - { - if(levels.values[i].x <= x) - { - if(i < levels.total - 1) - { - return levels.values[i].y + - (x - levels.values[i].x) * - (levels.values[i + 1].y - levels.values[i].y) / - (levels.values[i + 1].x - levels.values[i].x); - } - else - { - return levels.values[i].y + - (x - levels.values[i].x) * - (max_y - levels.values[i].y) / - (max_x - levels.values[i].x); - } +// 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; } - if(levels.total) - { - return min_y + - (x - min_x) * - (levels.values[0].y - min_y) / - (levels.values[0].x - min_x); - } - else - return x; -} + 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); + } -double CompressorEffect::calculate_gain(double input) -{ -// double x_db = DB::todb(input); -// double y_db = config.calculate_db(x_db); -// double y_linear = DB::fromdb(y_db); - double y_linear = calculate_output(input); - double gain; - if(input != 0) - gain = y_linear / input; - else - gain = 100000; - return gain; + last_position += sign * size; + return 0; } - - - - - - - - CompressorConfig::CompressorConfig() + : CompressorConfigBase(1) { - reaction_len = 1.0; - min_db = -80.0; - min_x = min_db; - min_y = min_db; - max_x = 0; - max_y = 0; - trigger = 0; - input = CompressorConfig::TRIGGER; - smoothing_only = 0; - decay_len = 1.0; } void CompressorConfig::copy_from(CompressorConfig &that) { - this->reaction_len = that.reaction_len; - this->decay_len = that.decay_len; - this->min_db = that.min_db; - this->min_x = that.min_x; - this->min_y = that.min_y; - this->max_x = that.max_x; - this->max_y = that.max_y; - this->trigger = that.trigger; - this->input = that.input; - this->smoothing_only = that.smoothing_only; - levels.remove_all(); - for(int i = 0; i < that.levels.total; i++) - this->levels.append(that.levels.values[i]); + CompressorConfigBase::copy_from(that); } int CompressorConfig::equivalent(CompressorConfig &that) { - if(!EQUIV(this->reaction_len, that.reaction_len) || - !EQUIV(this->decay_len, that.decay_len) || - this->trigger != that.trigger || - this->input != that.input || - this->smoothing_only != that.smoothing_only) - return 0; - if(this->levels.total != that.levels.total) return 0; - for(int i = 0; - i < this->levels.total && i < that.levels.total; - i++) - { - compressor_point_t *this_level = &this->levels.values[i]; - compressor_point_t *that_level = &that.levels.values[i]; - if(!EQUIV(this_level->x, that_level->x) || - !EQUIV(this_level->y, that_level->y)) - return 0; - } + return CompressorConfigBase::equivalent(that); return 1; } @@ -665,395 +349,149 @@ void CompressorConfig::interpolate(CompressorConfig &prev, copy_from(prev); } -int CompressorConfig::total_points() -{ - if(!levels.total) - return 1; - else - return levels.total; -} - -void CompressorConfig::dump() -{ - printf("CompressorConfig::dump\n"); - for(int i = 0; i < levels.total; i++) - { - printf(" %f %f\n", levels.values[i].x, levels.values[i].y); - } -} - - -double CompressorConfig::get_y(int number) -{ - if(!levels.total) - return 1.0; - else - if(number >= levels.total) - return levels.values[levels.total - 1].y; - else - return levels.values[number].y; -} - -double CompressorConfig::get_x(int number) -{ - if(!levels.total) - return 0.0; - else - if(number >= levels.total) - return levels.values[levels.total - 1].x; - else - return levels.values[number].x; -} - -double CompressorConfig::calculate_db(double x) -{ - if(x > -0.001) return 0.0; - - for(int i = levels.total - 1; i >= 0; i--) - { - if(levels.values[i].x <= x) - { - if(i < levels.total - 1) - { - return levels.values[i].y + - (x - levels.values[i].x) * - (levels.values[i + 1].y - levels.values[i].y) / - (levels.values[i + 1].x - levels.values[i].x); - } - else - { - return levels.values[i].y + - (x - levels.values[i].x) * - (max_y - levels.values[i].y) / - (max_x - levels.values[i].x); - } - } - } - - if(levels.total) - { - return min_y + - (x - min_x) * - (levels.values[0].y - min_y) / - (levels.values[0].x - min_x); - } - else - return x; -} - - -int CompressorConfig::set_point(double x, double y) -{ - for(int i = levels.total - 1; i >= 0; i--) - { - if(levels.values[i].x < x) - { - levels.append(); - i++; - for(int j = levels.total - 2; j >= i; j--) - { - levels.values[j + 1] = levels.values[j]; - } - levels.values[i].x = x; - levels.values[i].y = y; - - return i; - } - } - - levels.append(); - for(int j = levels.total - 2; j >= 0; j--) - { - levels.values[j + 1] = levels.values[j]; - } - levels.values[0].x = x; - levels.values[0].y = y; - return 0; -} - -void CompressorConfig::remove_point(int number) -{ - for(int j = number; j < levels.total - 1; j++) - { - levels.values[j] = levels.values[j + 1]; - } - levels.remove(); -} - -void CompressorConfig::optimize() +CompressorWindow::CompressorWindow(CompressorEffect *plugin) + : PluginClientWindow(plugin, xS(650), yS(480), xS(650), yS(480), 0) { - int done = 0; - - while(!done) - { - done = 1; - - - for(int i = 0; i < levels.total - 1; i++) - { - if(levels.values[i].x >= levels.values[i + 1].x) - { - done = 0; - for(int j = i + 1; j < levels.total - 1; j++) - { - levels.values[j] = levels.values[j + 1]; - } - levels.remove(); - } - } - - } + this->plugin = plugin; } - - - - - - - - - - - - - - - - - - - - - - - - - - -CompressorWindow::CompressorWindow(CompressorEffect *plugin) - : PluginClientWindow(plugin, - xS(650), - yS(480), - xS(650), - yS(480), - 0) +CompressorWindow::~CompressorWindow() { - this->plugin = plugin; +// delete readahead; + delete attack; + delete release; + delete trigger; + delete x_text; + delete y_text; } void CompressorWindow::create_objects() { - int xs10 = xS(10), xs20 = xS(20), xs50 = xS(50), xs110 = xS(110); - int ys20 = yS(20), ys30 = yS(30), ys40 = yS(40), ys60 = yS(60), ys70 = yS(70); - int x = xS(35), y = yS(10); + int margin = client->get_theme()->widget_border; + int x = margin, y = margin; int control_margin = xS(130); - - add_subwindow(canvas = new CompressorCanvas(plugin, - x, - y, - get_w() - x - control_margin - xs10, - get_h() - y - ys70)); - canvas->set_cursor(CROSS_CURSOR, 0, 0); + 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, _("Reaction secs:"))); - y += ys20; - add_subwindow(reaction = new CompressorReaction(plugin, x, y)); - y += ys30; - add_subwindow(new BC_Title(x, y, _("Decay secs:"))); - y += ys20; - add_subwindow(decay = new CompressorDecay(plugin, x, y)); - y += ys30; - add_subwindow(new BC_Title(x, y, _("Trigger Type:"))); - y += ys20; + +// 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 += ys30; - add_subwindow(new BC_Title(x, y, _("Trigger:"))); - y += ys20; - add_subwindow(trigger = new CompressorTrigger(plugin, x, y)); - if(plugin->config.input != CompressorConfig::TRIGGER) trigger->disable(); - y += ys30; - add_subwindow(smooth = new CompressorSmooth(plugin, x, y)); - y += ys60; - add_subwindow(clear = new CompressorClear(plugin, x, y)); - x = xs10; - y = get_h() - ys40; - add_subwindow(new BC_Title(x, y, _("Point:"))); - x += xs50; - add_subwindow(x_text = new CompressorX(plugin, x, y)); - x += xs110; - add_subwindow(new BC_Title(x, y, _("x"))); - x += xs20; - add_subwindow(y_text = new CompressorY(plugin, x, y)); - draw_scales(); - - update_canvas(); - show_window(); -} + y += input->get_h() + margin; -void CompressorWindow::draw_scales() -{ - draw_3d_border(canvas->get_x() - 2, - canvas->get_y() - 2, - canvas->get_w() + 4, - canvas->get_h() + 4, - get_bg_color(), - BLACK, - MDGREY, - get_bg_color()); - - - set_font(SMALLFONT); - set_color(get_resources()->default_text_color); - -#define DIVISIONS 8 - for(int i = 0; i <= DIVISIONS; i++) - { - int y = canvas->get_y() + yS(10) + canvas->get_h() / DIVISIONS * i; - int x = canvas->get_x() - xS(30); - char string[BCTEXTLEN]; - - sprintf(string, "%.0f", (float)i / DIVISIONS * plugin->config.min_db); - draw_text(x, y, string); - - int y1 = canvas->get_y() + canvas->get_h() / DIVISIONS * i; - int y2 = canvas->get_y() + canvas->get_h() / DIVISIONS * (i + 1); - for(int j = 0; j < 10; j++) - { - y = y1 + (y2 - y1) * j / 10; - if(j == 0) - { - draw_line(canvas->get_x() - xS(10), y, canvas->get_x(), y); - } - else - if(i < DIVISIONS) - { - draw_line(canvas->get_x() - xS(5), y, canvas->get_x(), y); - } - } - } + 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; - for(int i = 0; i <= DIVISIONS; i++) - { - int y = canvas->get_h() + yS(30); - int x = canvas->get_x() + (canvas->get_w() - xS(10)) / DIVISIONS * i; - char string[BCTEXTLEN]; - - sprintf(string, "%.0f", (1.0 - (float)i / DIVISIONS) * plugin->config.min_db); - draw_text(x, y, string); - - int x1 = canvas->get_x() + canvas->get_w() / DIVISIONS * i; - int x2 = canvas->get_x() + canvas->get_w() / DIVISIONS * (i + 1); - for(int j = 0; j < 10; j++) - { - x = x1 + (x2 - x1) * j / 10; - if(j == 0) - { - draw_line(x, canvas->get_y() + canvas->get_h(), x, canvas->get_y() + canvas->get_h() + yS(10)); - } - else - if(i < DIVISIONS) - { - draw_line(x, canvas->get_y() + canvas->get_h(), x, canvas->get_y() + canvas->get_h() + yS(5)); - } - } - } + 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, x, y)); + x = xS(10); + y = get_h() - yS(40); - flash(); + canvas->create_objects(); + canvas->update(); + show_window(); } + void CompressorWindow::update() { update_textboxes(); - update_canvas(); + 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() { - if(atol(trigger->get_text()) != plugin->config.trigger) + 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))) + 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()) + if( plugin->config.input != CompressorConfig::TRIGGER && trigger->get_enabled() ) trigger->disable(); else - if(plugin->config.input == CompressorConfig::TRIGGER && !trigger->get_enabled()) + if( plugin->config.input == CompressorConfig::TRIGGER && !trigger->get_enabled() ) trigger->enable(); - if(!EQUIV(atof(reaction->get_text()), plugin->config.reaction_len)) - reaction->update((float)plugin->config.reaction_len); - if(!EQUIV(atof(decay->get_text()), plugin->config.decay_len)) - decay->update((float)plugin->config.decay_len); +// 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)plugin->config.levels.values[canvas->current_point].x); - y_text->update((float)plugin->config.levels.values[canvas->current_point].y); - } -} - -#define POINT_W xS(10) -void CompressorWindow::update_canvas() -{ - canvas->clear_box(0, 0, canvas->get_w(), canvas->get_h()); - canvas->set_line_dashes(1); - canvas->set_color(GREEN); - - for(int i = 1; i < DIVISIONS; i++) - { - int y = canvas->get_h() * i / DIVISIONS; - canvas->draw_line(0, y, canvas->get_w(), y); - - int x = canvas->get_w() * i / DIVISIONS; - canvas->draw_line(x, 0, x, canvas->get_h()); - } - canvas->set_line_dashes(0); - - - canvas->set_font(MEDIUMFONT); - canvas->draw_text(plugin->get_theme()->widget_border, - canvas->get_h() / 2, - _("Output")); - canvas->draw_text(canvas->get_w() / 2 - canvas->get_text_width(MEDIUMFONT, _("Input")) / 2, - canvas->get_h() - plugin->get_theme()->widget_border, - _("Input")); - - - canvas->set_color(WHITE); - canvas->set_line_width(2); - - double x_db = plugin->config.min_db; - double y_db = plugin->config.calculate_db(x_db); - int y1 = (int)(y_db / plugin->config.min_db * canvas->get_h()); - - for(int i=1; iget_w(); i++) - { - x_db = (1. - (double)i / canvas->get_w()) * plugin->config.min_db; - y_db = plugin->config.calculate_db(x_db); - int y2 = (int)(y_db / plugin->config.min_db * canvas->get_h()); - canvas->draw_line(i-1, y1, i, y2); - y1 = y2; + if( canvas->current_operation == CompressorCanvas::DRAG ) { + x_text->update((float)band_config->levels.values[canvas->current_point].x); + y_text->update((float)band_config->levels.values[canvas->current_point].y); } - canvas->set_line_width(1); - - int total = plugin->config.levels.total ? plugin->config.levels.total : 1; - for(int i=0; i < total; i++) - { - x_db = plugin->config.get_x(i); - y_db = plugin->config.get_y(i); - - int x = (int)((1. - x_db / plugin->config.min_db) * canvas->get_w()); - int y = (int)(y_db / plugin->config.min_db * canvas->get_h()); - - canvas->draw_box(x - POINT_W / 2, y - POINT_W / 2, POINT_W, POINT_W); - } - - canvas->flash(); } int CompressorWindow::resize_event(int w, int h) @@ -1062,250 +500,123 @@ int CompressorWindow::resize_event(int w, int h) } - - - - - - -CompressorCanvas::CompressorCanvas(CompressorEffect *plugin, int x, int y, int w, int h) - : BC_SubWindow(x, y, w, h, BLACK) +CompressorCanvas::CompressorCanvas(CompressorEffect *plugin, + CompressorWindow *window, int x, int y, int w, int h) + : CompressorCanvasBase(&plugin->config, plugin, window, x, y, w, h) { - this->plugin = plugin; - current_operation = NONE; - current_point = 0; } -int CompressorCanvas::button_press_event() -{ -// Check existing points - if(is_event_win() && cursor_inside()) - { - for(int i = 0; i < plugin->config.levels.total; i++) - { - double x_db = plugin->config.get_x(i); - double y_db = plugin->config.get_y(i); - - int x = (int)(((double)1 - x_db / plugin->config.min_db) * get_w()); - int y = (int)(y_db / plugin->config.min_db * get_h()); - - if(get_cursor_x() < x + POINT_W / 2 && get_cursor_x() >= x - POINT_W / 2 && - get_cursor_y() < y + POINT_W / 2 && get_cursor_y() >= y - POINT_W / 2) - { - current_operation = DRAG; - current_point = i; - return 1; - } - } - - - -// Create new point - double x_db = (double)(1 - (double)get_cursor_x() / get_w()) * plugin->config.min_db; - double y_db = (double)get_cursor_y() / get_h() * plugin->config.min_db; - - current_point = plugin->config.set_point(x_db, y_db); - current_operation = DRAG; - ((CompressorWindow*)plugin->thread->window)->update(); - plugin->send_configure_change(); - return 1; - } - return 0; -//plugin->config.dump(); -} - -int CompressorCanvas::button_release_event() -{ - if(current_operation == DRAG) - { - if(current_point > 0) - { - if(plugin->config.levels.values[current_point].x < - plugin->config.levels.values[current_point - 1].x) - plugin->config.remove_point(current_point); - } - - if(current_point < plugin->config.levels.total - 1) - { - if(plugin->config.levels.values[current_point].x >= - plugin->config.levels.values[current_point + 1].x) - plugin->config.remove_point(current_point); - } - - ((CompressorWindow*)plugin->thread->window)->update(); - plugin->send_configure_change(); - current_operation = NONE; - return 1; - } - - return 0; -} -int CompressorCanvas::cursor_motion_event() +void CompressorCanvas::update_window() { - if(current_operation == DRAG) - { - int x = get_cursor_x(); - int y = get_cursor_y(); - CLAMP(x, 0, get_w()); - CLAMP(y, 0, get_h()); - double x_db = (double)(1 - (double)x / get_w()) * plugin->config.min_db; - double y_db = (double)y / get_h() * plugin->config.min_db; - plugin->config.levels.values[current_point].x = x_db; - plugin->config.levels.values[current_point].y = y_db; - ((CompressorWindow*)plugin->thread->window)->update(); - plugin->send_configure_change(); - return 1; -//plugin->config.dump(); - } - else -// Change cursor over points - if(is_event_win() && cursor_inside()) - { - int new_cursor = CROSS_CURSOR; - - for(int i = 0; i < plugin->config.levels.total; i++) - { - double x_db = plugin->config.get_x(i); - double y_db = plugin->config.get_y(i); - - int x = (int)(((double)1 - x_db / plugin->config.min_db) * get_w()); - int y = (int)(y_db / plugin->config.min_db * get_h()); - - if(get_cursor_x() < x + POINT_W / 2 && get_cursor_x() >= x - POINT_W / 2 && - get_cursor_y() < y + POINT_W / 2 && get_cursor_y() >= y - POINT_W / 2) - { - new_cursor = UPRIGHT_ARROW_CURSOR; - break; - } - } - - - if(new_cursor != get_cursor()) - { - set_cursor(new_cursor, 0, 1); - } - } - return 0; + ((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; +// } +// +// - -CompressorReaction::CompressorReaction(CompressorEffect *plugin, int x, int y) - : BC_TextBox(x, y, xS(100), 1, (float)plugin->config.reaction_len) +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 CompressorReaction::handle_event() +int CompressorAttack::handle_event() { - plugin->config.reaction_len = atof(get_text()); + plugin->config.bands[0].attack_len = atof(get_text()); plugin->send_configure_change(); return 1; } -int CompressorReaction::button_press_event() -{ - if(is_event_win()) - { - if(get_buttonpress() < 4) return BC_TextBox::button_press_event(); - if(get_buttonpress() == 4) - { - plugin->config.reaction_len += 0.1; - } - else - if(get_buttonpress() == 5) - { - plugin->config.reaction_len -= 0.1; - } - update((float)plugin->config.reaction_len); - plugin->send_configure_change(); - return 1; - } - return 0; -} - -CompressorDecay::CompressorDecay(CompressorEffect *plugin, int x, int y) - : BC_TextBox(x, y, xS(100), 1, (float)plugin->config.decay_len) +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 CompressorDecay::handle_event() +int CompressorRelease::handle_event() { - plugin->config.decay_len = atof(get_text()); + plugin->config.bands[0].release_len = atof(get_text()); plugin->send_configure_change(); return 1; } -int CompressorDecay::button_press_event() -{ - if(is_event_win()) - { - if(get_buttonpress() < 4) return BC_TextBox::button_press_event(); - if(get_buttonpress() == 4) - { - plugin->config.decay_len += 0.1; - } - else - if(get_buttonpress() == 5) - { - plugin->config.decay_len -= 0.1; - } - update((float)plugin->config.decay_len); - plugin->send_configure_change(); - return 1; - } - return 0; -} - - -CompressorX::CompressorX(CompressorEffect *plugin, int x, int y) - : BC_TextBox(x, y, xS(100), 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; + 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 < plugin->config.levels.total) - { - plugin->config.levels.values[current_point].x = atof(get_text()); - ((CompressorWindow*)plugin->thread->window)->update_canvas(); + if( current_point < band_config->levels.total ) { + band_config->levels.values[current_point].x = atof(get_text()); + ((CompressorWindow*)plugin->thread->window)->canvas->update(); plugin->send_configure_change(); } return 1; } - - -CompressorY::CompressorY(CompressorEffect *plugin, int x, int y) - : BC_TextBox(x, y, xS(100), 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; + 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 < plugin->config.levels.total) - { - plugin->config.levels.values[current_point].y = atof(get_text()); - ((CompressorWindow*)plugin->thread->window)->update_canvas(); + if( current_point < band_config->levels.total ) { + band_config->levels.values[current_point].y = atof(get_text()); + ((CompressorWindow*)plugin->thread->window)->canvas->update(); plugin->send_configure_change(); } return 1; } - - - -CompressorTrigger::CompressorTrigger(CompressorEffect *plugin, int x, int y) - : BC_TextBox(x, y, xS(100), 1, (int64_t)plugin->config.trigger) +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() { @@ -1314,37 +625,10 @@ int CompressorTrigger::handle_event() return 1; } -int CompressorTrigger::button_press_event() -{ - if(is_event_win()) - { - if(get_buttonpress() < 4) return BC_TextBox::button_press_event(); - if(get_buttonpress() == 4) - { - plugin->config.trigger++; - } - else - if(get_buttonpress() == 5) - { - plugin->config.trigger--; - } - update((int64_t)plugin->config.trigger); - plugin->send_configure_change(); - return 1; - } - return 0; -} - - - - CompressorInput::CompressorInput(CompressorEffect *plugin, int x, int y) - : BC_PopupMenu(x, - y, - xS(100), - CompressorInput::value_to_text(plugin->config.input), - 1) + : BC_PopupMenu(x, y, xS(100), + CompressorInput::value_to_text(plugin->config.input), 1) { this->plugin = plugin; } @@ -1358,39 +642,32 @@ int CompressorInput::handle_event() void CompressorInput::create_objects() { - for(int i = 0; i < 3; i++) - { + 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) + switch( value ) { - case CompressorConfig::TRIGGER: return _("Trigger"); - case CompressorConfig::MAX: return _("Maximum"); - case CompressorConfig::SUM: return _("Total"); + case CompressorConfig::TRIGGER: return "Trigger"; + case CompressorConfig::MAX: return "Maximum"; + case CompressorConfig::SUM: return "Total"; } - return _("Trigger"); + 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; + for( int i = 0; i < 3; i++ ) { + if( !strcmp(value_to_text(i), text) ) return i; } return CompressorConfig::TRIGGER; } - - - - - CompressorClear::CompressorClear(CompressorEffect *plugin, int x, int y) : BC_GenericButton(x, y, _("Clear")) { @@ -1399,15 +676,14 @@ CompressorClear::CompressorClear(CompressorEffect *plugin, int x, int y) int CompressorClear::handle_event() { - plugin->config.levels.remove_all(); + BandConfig *band_config = &plugin->config.bands[0]; + band_config->levels.remove_all(); //plugin->config.dump(); ((CompressorWindow*)plugin->thread->window)->update(); plugin->send_configure_change(); return 1; } - - CompressorSmooth::CompressorSmooth(CompressorEffect *plugin, int x, int y) : BC_CheckBox(x, y, plugin->config.smoothing_only, _("Smooth only")) { @@ -1421,6 +697,3 @@ int CompressorSmooth::handle_event() return 1; } - - -