diff options
author | David Robillard <d@drobilla.net> | 2014-12-28 15:50:57 -0500 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2014-12-28 16:06:44 -0500 |
commit | 4c0cebf7f98ecd14873d26b6f4d8bdedd37cb994 (patch) | |
tree | 8efbd17e96fc1c56392ff77e2ef3d99e5f3af731 /gtk2_ardour | |
parent | ec947ff8fd2cf229284f757b8bd6b0f96cbd6383 (diff) |
MIDI transform dialog.
Diffstat (limited to 'gtk2_ardour')
-rw-r--r-- | gtk2_ardour/editor.cc | 2 | ||||
-rw-r--r-- | gtk2_ardour/editor.h | 2 | ||||
-rw-r--r-- | gtk2_ardour/editor_actions.cc | 1 | ||||
-rw-r--r-- | gtk2_ardour/editor_ops.cc | 28 | ||||
-rw-r--r-- | gtk2_ardour/transform_dialog.cc | 359 | ||||
-rw-r--r-- | gtk2_ardour/transform_dialog.h | 140 | ||||
-rw-r--r-- | gtk2_ardour/wscript | 1 |
7 files changed, 533 insertions, 0 deletions
diff --git a/gtk2_ardour/editor.cc b/gtk2_ardour/editor.cc index a681dd44da..3fc05d2f36 100644 --- a/gtk2_ardour/editor.cc +++ b/gtk2_ardour/editor.cc @@ -5771,6 +5771,8 @@ Editor::popup_note_context_menu (ArdourCanvas::Item* item, GdkEvent* event) sigc::bind(sigc::mem_fun(*this, &Editor::quantize_regions), rs))); items.push_back(MenuElem(_("Remove Overlap"), sigc::bind(sigc::mem_fun(*this, &Editor::legatize_regions), rs, true))); + items.push_back(MenuElem(_("Transform..."), + sigc::bind(sigc::mem_fun(*this, &Editor::transform_regions), rs))); _note_context_menu.popup (event->button.button, event->button.time); } diff --git a/gtk2_ardour/editor.h b/gtk2_ardour/editor.h index ce5aa8bde4..98e8b151e4 100644 --- a/gtk2_ardour/editor.h +++ b/gtk2_ardour/editor.h @@ -1233,6 +1233,8 @@ class Editor : public PublicEditor, public PBD::ScopedConnectionList, public ARD void quantize_regions (const RegionSelection& rs); void legatize_region (bool shrink_only); void legatize_regions (const RegionSelection& rs, bool shrink_only); + void transform_region (); + void transform_regions (const RegionSelection& rs); void insert_patch_change (bool from_context); void fork_region (); diff --git a/gtk2_ardour/editor_actions.cc b/gtk2_ardour/editor_actions.cc index 4807bdf72e..8f24a4b91b 100644 --- a/gtk2_ardour/editor_actions.cc +++ b/gtk2_ardour/editor_actions.cc @@ -1938,6 +1938,7 @@ Editor::register_region_actions () reg_sens (_region_actions, "quantize-region", _("Quantize..."), sigc::mem_fun (*this, &Editor::quantize_region)); reg_sens (_region_actions, "legatize-region", _("Legatize"), sigc::bind(sigc::mem_fun (*this, &Editor::legatize_region), false)); + reg_sens (_region_actions, "transform-region", _("Transform..."), sigc::mem_fun (*this, &Editor::transform_region)); reg_sens (_region_actions, "remove-overlap", _("Remove Overlap"), sigc::bind(sigc::mem_fun (*this, &Editor::legatize_region), true)); reg_sens (_region_actions, "insert-patch-change", _("Insert Patch Change..."), sigc::bind (sigc::mem_fun (*this, &Editor::insert_patch_change), false)); reg_sens (_region_actions, "insert-patch-change-context", _("Insert Patch Change..."), sigc::bind (sigc::mem_fun (*this, &Editor::insert_patch_change), true)); diff --git a/gtk2_ardour/editor_ops.cc b/gtk2_ardour/editor_ops.cc index 4589844bf6..625579d6a8 100644 --- a/gtk2_ardour/editor_ops.cc +++ b/gtk2_ardour/editor_ops.cc @@ -97,6 +97,7 @@ #include "strip_silence_dialog.h" #include "time_axis_view.h" #include "transpose_dialog.h" +#include "transform_dialog.h" #include "i18n.h" @@ -5039,6 +5040,33 @@ Editor::legatize_regions (const RegionSelection& rs, bool shrink_only) } void +Editor::transform_region () +{ + if (_session) { + transform_regions(get_regions_from_selection_and_entered ()); + } +} + +void +Editor::transform_regions (const RegionSelection& rs) +{ + if (rs.n_midi_regions() == 0) { + return; + } + + TransformDialog* td = new TransformDialog(); + + td->present(); + const int r = td->run(); + td->hide(); + + if (r == Gtk::RESPONSE_OK) { + Transform transform(td->get()); + apply_midi_note_edit_op(transform, rs); + } +} + +void Editor::insert_patch_change (bool from_context) { RegionSelection rs = get_regions_from_selection_and_entered (); diff --git a/gtk2_ardour/transform_dialog.cc b/gtk2_ardour/transform_dialog.cc new file mode 100644 index 0000000000..35df027187 --- /dev/null +++ b/gtk2_ardour/transform_dialog.cc @@ -0,0 +1,359 @@ +/* + Copyright (C) 2009-2014 Paul Davis + Author: David Robillard + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/stock.h> + +#include "transform_dialog.h" + +#include "i18n.h" + +using namespace std; +using namespace Gtk; +using namespace ARDOUR; + +TransformDialog::Model::Model() + : source_list(Gtk::ListStore::create(source_cols)) + , property_list(Gtk::ListStore::create(property_cols)) + , operator_list(Gtk::ListStore::create(operator_cols)) +{ + static const char* source_labels[] = { + /* no NOTHING */ + _("this note's"), + _("the previous note's"), + _("this note's index"), + _("the number of notes"), + _("exactly"), + _("a random number from"), + NULL + }; + for (int s = 0; source_labels[s]; ++s) { + Gtk::TreeModel::Row row = *(source_list->append()); + row[source_cols.source] = (Source)(s + 1); // Skip NOTHING + row[source_cols.label] = source_labels[s]; + } + // Special row for ramp, which doesn't correspond to a source + Gtk::TreeModel::Row row = *(source_list->append()); + row[source_cols.source] = Value::NOWHERE; + row[source_cols.label] = _("equal steps from"); + + static const char* property_labels[] = { + _("note number"), + _("velocity"), + _("start time"), + _("length"), + _("channel"), + NULL + }; + for (int p = 0; property_labels[p]; ++p) { + Gtk::TreeModel::Row row = *(property_list->append()); + row[property_cols.property] = (Property)p; + row[property_cols.label] = property_labels[p]; + } + + static const char* operator_labels[] = { + /* no PUSH */ "+", "-", "*", "/", NULL + }; + for (int o = 0; operator_labels[o]; ++o) { + Gtk::TreeModel::Row row = *(operator_list->append()); + row[operator_cols.op] = (Operator)(o + 1); // Skip PUSH + row[operator_cols.label] = operator_labels[o]; + } +} + +TransformDialog::TransformDialog() + : ArdourDialog(_("Transform"), false, false) +{ + _property_combo.set_model(_model.property_list); + _property_combo.pack_start(_model.property_cols.label); + _property_combo.set_active(1); + _property_combo.signal_changed().connect( + sigc::mem_fun(this, &TransformDialog::property_changed)); + + Gtk::HBox* property_hbox = Gtk::manage(new Gtk::HBox); + property_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Set "))), false, false); + property_hbox->pack_start(_property_combo, false, false); + property_hbox->pack_start(*Gtk::manage(new Gtk::Label(_(" to "))), false, false); + + _seed_chooser = Gtk::manage(new ValueChooser(_model)); + _seed_chooser->set_target_property(MidiModel::NoteDiffCommand::Velocity); + _seed_chooser->source_combo.set_active(0); + property_hbox->pack_start(*_seed_chooser, false, false); + + Gtk::HBox* add_hbox = Gtk::manage(new Gtk::HBox); + _add_button.add( + *manage(new Gtk::Image(Gtk::Stock::ADD, Gtk::ICON_SIZE_BUTTON))); + add_hbox->pack_start(_add_button, false, false); + _add_button.signal_clicked().connect( + sigc::mem_fun(*this, &TransformDialog::add_clicked)); + + get_vbox()->set_spacing(6); + get_vbox()->pack_start(*property_hbox, false, false); + get_vbox()->pack_start(_operations_box, false, false); + get_vbox()->pack_start(*add_hbox, false, false); + + add_button(Stock::CANCEL, Gtk::RESPONSE_CANCEL); + add_button(_("Transform"), Gtk::RESPONSE_OK); + + show_all(); + _seed_chooser->value_spinner.hide(); +} + +TransformDialog::ValueChooser::ValueChooser(const Model& model) + : model(model) + , target_property((Property)1) + , to_label(" to ") +{ + source_combo.set_model(model.source_list); + source_combo.pack_start(model.source_cols.label); + source_combo.signal_changed().connect( + sigc::mem_fun(this, &TransformDialog::ValueChooser::source_changed)); + + property_combo.set_model(model.property_list); + property_combo.pack_start(model.property_cols.label); + + set_spacing(4); + pack_start(source_combo, false, false); + pack_start(property_combo, false, false); + pack_start(value_spinner, false, false); + pack_start(to_label, false, false); + pack_start(max_spinner, false, false); + show_all(); + + source_combo.set_active(4); + property_combo.set_active(1); + set_target_property(MidiModel::NoteDiffCommand::Velocity); + max_spinner.set_value(127); + source_changed(); +} + +static void +set_spinner_for(Gtk::SpinButton& spinner, + MidiModel::NoteDiffCommand::Property prop) +{ + switch (prop) { + case MidiModel::NoteDiffCommand::NoteNumber: + case MidiModel::NoteDiffCommand::Velocity: + spinner.get_adjustment()->set_lower(1); // no 0, note off + spinner.get_adjustment()->set_upper(127); + spinner.get_adjustment()->set_step_increment(1); + spinner.get_adjustment()->set_page_increment(10); + spinner.set_digits(0); + break; + case MidiModel::NoteDiffCommand::StartTime: + spinner.get_adjustment()->set_lower(0); + spinner.get_adjustment()->set_upper(1024); + spinner.get_adjustment()->set_step_increment(0.125); + spinner.get_adjustment()->set_page_increment(1.0); + spinner.set_digits(2); + break; + case MidiModel::NoteDiffCommand::Length: + spinner.get_adjustment()->set_lower(1.0 / 64.0); + spinner.get_adjustment()->set_upper(32); + spinner.get_adjustment()->set_step_increment(1.0 / 64.0); + spinner.get_adjustment()->set_page_increment(1.0); + spinner.set_digits(2); + break; + case MidiModel::NoteDiffCommand::Channel: + spinner.get_adjustment()->set_lower(0); + spinner.get_adjustment()->set_upper(15); + spinner.get_adjustment()->set_step_increment(1); + spinner.get_adjustment()->set_page_increment(10); + spinner.set_digits(0); + break; + } +} + +void +TransformDialog::ValueChooser::set_target_property(Property prop) +{ + target_property = prop; + set_spinner_for(value_spinner, prop); + set_spinner_for(max_spinner, prop); +} + +void +TransformDialog::ValueChooser::source_changed() +{ + Gtk::TreeModel::const_iterator s = source_combo.get_active(); + const Source source = (*s)[model.source_cols.source]; + + value_spinner.hide(); + to_label.hide(); + max_spinner.hide(); + if (source == Value::LITERAL) { + value_spinner.show(); + property_combo.hide(); + } else if (source == Value::RANDOM) { + value_spinner.show(); + to_label.show(); + max_spinner.show(); + property_combo.hide(); + } else if (source == Value::NOWHERE) { + /* Bit of a kludge, hijack this for ramps since it's the only thing + that doesn't correspond to a source. When we add more fancy + code-generating value chooser options, the column model will need to + be changed a bit to reflect this. */ + value_spinner.show(); + to_label.show(); + max_spinner.show(); + property_combo.hide(); + } else if (source == Value::INDEX || source == Value::N_NOTES) { + value_spinner.hide(); + property_combo.hide(); + } else { + value_spinner.hide(); + property_combo.show(); + } +} + +void +TransformDialog::ValueChooser::get(std::list<Operation>& ops) +{ + Gtk::TreeModel::const_iterator s = source_combo.get_active(); + const Source source = (*s)[model.source_cols.source]; + + if (source == Transform::Value::RANDOM) { + /* Special case: a RANDOM value is always 0..1, so here we produce some + code to produce a random number in a range: "rand value *". */ + const double a = value_spinner.get_value(); + const double b = max_spinner.get_value(); + const double min = std::min(a, b); + const double max = std::max(a, b); + const double range = max - min; + + // "rand range * min +" (i.e. (rand * range) + min) + ops.push_back(Operation(Operation::PUSH, Value(Value::RANDOM))); + ops.push_back(Operation(Operation::PUSH, Value(range))); + ops.push_back(Operation(Operation::MULT)); + ops.push_back(Operation(Operation::PUSH, Value(min))); + ops.push_back(Operation(Operation::ADD)); + return; + } else if (source == Transform::Value::NOWHERE) { + /* Special case: hijack NOWHERE for ramps (see above). The language + knows nothing of ramps, we generate code to calculate the + appropriate value here. */ + const double first = value_spinner.get_value(); + const double last = max_spinner.get_value(); + const double rise = last - first; + + // "index rise * n_notes / first +" (i.e. index * rise / n_notes + first) + ops.push_back(Operation(Operation::PUSH, Value(Value::INDEX))); + ops.push_back(Operation(Operation::PUSH, Value(rise))); + ops.push_back(Operation(Operation::MULT)); + ops.push_back(Operation(Operation::PUSH, Value(Value::N_NOTES))); + ops.push_back(Operation(Operation::DIV)); + ops.push_back(Operation(Operation::PUSH, Value(first))); + ops.push_back(Operation(Operation::ADD)); + return; + } + + // Produce a simple Value + Value val((*s)[model.source_cols.source]); + if (val.source == Transform::Value::THIS_NOTE || + val.source == Transform::Value::PREV_NOTE) { + Gtk::TreeModel::const_iterator p = property_combo.get_active(); + val.prop = (*p)[model.property_cols.property]; + } else if (val.source == Transform::Value::LITERAL) { + val.value = Variant( + MidiModel::NoteDiffCommand::value_type(target_property), + value_spinner.get_value()); + } + ops.push_back(Operation(Operation::PUSH, val)); +} + +TransformDialog::OperationChooser::OperationChooser(const Model& model) + : model(model) + , value_chooser(model) +{ + operator_combo.set_model(model.operator_list); + operator_combo.pack_start(model.operator_cols.label); + operator_combo.set_active(0); + + pack_start(operator_combo, false, false); + pack_start(value_chooser, false, false); + pack_start(*Gtk::manage(new Gtk::Label(" ")), true, true); + pack_start(remove_button, false, false); + + remove_button.add( + *manage(new Gtk::Image(Gtk::Stock::REMOVE, Gtk::ICON_SIZE_BUTTON))); + + remove_button.signal_clicked().connect( + sigc::mem_fun(*this, &TransformDialog::OperationChooser::remove_clicked)); + + value_chooser.source_combo.set_active(0); + + show_all(); + value_chooser.property_combo.hide(); + value_chooser.value_spinner.set_value(1); +} + +void +TransformDialog::OperationChooser::get(std::list<Operation>& ops) +{ + Gtk::TreeModel::const_iterator o = operator_combo.get_active(); + + value_chooser.get(ops); + ops.push_back(Operation((*o)[model.operator_cols.op])); +} + +void +TransformDialog::OperationChooser::remove_clicked() +{ + delete this; +} + +Transform::Program +TransformDialog::get() +{ + Transform::Program prog; + + // Set target property + prog.prop = (*_property_combo.get_active())[_model.property_cols.property]; + + // Append code to push seed to stack + _seed_chooser->get(prog.ops); + + // Append all operations' code to program + const std::vector<Gtk::Widget*>& choosers = _operations_box.get_children(); + for (std::vector<Gtk::Widget*>::const_iterator o = choosers.begin(); + o != choosers.end(); ++o) { + OperationChooser* chooser = dynamic_cast<OperationChooser*>(*o); + if (chooser) { + chooser->get(prog.ops); + } + } + + return prog; +} + +void +TransformDialog::property_changed() +{ + Gtk::TreeModel::const_iterator i = _property_combo.get_active(); + _seed_chooser->set_target_property((*i)[_model.property_cols.property]); +} + +void +TransformDialog::add_clicked() +{ + _operations_box.pack_start( + *Gtk::manage(new OperationChooser(_model)), false, false); +} diff --git a/gtk2_ardour/transform_dialog.h b/gtk2_ardour/transform_dialog.h new file mode 100644 index 0000000000..5111aa7543 --- /dev/null +++ b/gtk2_ardour/transform_dialog.h @@ -0,0 +1,140 @@ +/* + Copyright (C) 2009-2014 Paul Davis + Author: David Robillard + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + +#ifndef __transform_dialog_h__ +#define __transform_dialog_h__ + +#include <list> +#include <string> + +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/spinbutton.h> + +#include "ardour/midi_model.h" +#include "ardour/transform.h" +#include "ardour/types.h" +#include "evoral/types.hpp" + +#include "ardour_dialog.h" + +/** Dialog for building a MIDI note transformation. + * + * This can build transformations with any number of operations, but is limited + * in power and can't build arbitrary transformations since there is no way to do + * conceptually parenthetical things (i.e. push things to the stack). + * + * With this, it is possible to build transformations that process a single + * value in a series of steps starting with a seed, like: "value = seed OP + * value OP value ..." where OP is +, -, *, or /, left associative with no + * precedence. This is simple and pretty clear to the user what's going to + * happen, though a bit limited. It would be nice if the GUI could build + * fancier transformations, but it's not obvious how to do this without making + * things more confusing. + */ +class TransformDialog : public ArdourDialog +{ +public: + TransformDialog(); + + ARDOUR::Transform::Program get(); + +private: + typedef ARDOUR::MidiModel::NoteDiffCommand::Property Property; + typedef ARDOUR::Transform::Value Value; + typedef ARDOUR::Transform::Value::Source Source; + typedef ARDOUR::Transform::Operation::Operator Operator; + typedef ARDOUR::Transform::Operation Operation; + + struct SourceCols : public Gtk::TreeModelColumnRecord { + SourceCols() { add(source); add(label); } + + Gtk::TreeModelColumn<Source> source; + Gtk::TreeModelColumn<std::string> label; + }; + + struct PropertyCols : public Gtk::TreeModelColumnRecord { + PropertyCols() { add(property); add(label); } + + Gtk::TreeModelColumn<Property> property; + Gtk::TreeModelColumn<std::string> label; + }; + + struct OperatorCols : public Gtk::TreeModelColumnRecord { + OperatorCols() { add(op); add(label); } + + Gtk::TreeModelColumn<Operator> op; + Gtk::TreeModelColumn<std::string> label; + }; + + struct Model { + Model(); + + SourceCols source_cols; + Glib::RefPtr<Gtk::ListStore> source_list; + PropertyCols property_cols; + Glib::RefPtr<Gtk::ListStore> property_list; + OperatorCols operator_cols; + Glib::RefPtr<Gtk::ListStore> operator_list; + }; + + struct ValueChooser : public Gtk::HBox { + ValueChooser(const Model& model); + + /** Append code to `ops` that pushes value to stack. */ + void get(std::list<Operation>& ops); + + void set_target_property(Property prop); + void source_changed(); + + const Model& model; ///< Models for combo boxes + Property target_property; ///< Property on source + Gtk::ComboBox source_combo; ///< Value source chooser + Gtk::ComboBox property_combo; ///< Property chooser + Gtk::SpinButton value_spinner; ///< Value or minimum for RANDOM + Gtk::Label to_label; ///< "to" label for RANDOM + Gtk::SpinButton max_spinner; ///< Maximum for RANDOM + }; + + struct OperationChooser : public Gtk::HBox { + OperationChooser(const Model& model); + + /** Append operations to `ops`. */ + void get(std::list<Operation>& ops); + + void remove_clicked(); + + const Model& model; + Gtk::ComboBox operator_combo; + ValueChooser value_chooser; + Gtk::Button remove_button; + }; + + void property_changed(); + void add_clicked(); + + Model _model; + Gtk::ComboBox _property_combo; + ValueChooser* _seed_chooser; + Gtk::VBox _operations_box; + Gtk::Button _add_button; +}; + +#endif /* __transform_dialog_h__ */ diff --git a/gtk2_ardour/wscript b/gtk2_ardour/wscript index b0d4917151..d2aa69226d 100644 --- a/gtk2_ardour/wscript +++ b/gtk2_ardour/wscript @@ -233,6 +233,7 @@ gtk2_ardour_sources = [ 'time_selection.cc', 'track_selection.cc', 'track_view_list.cc', + 'transform_dialog.cc', 'transpose_dialog.cc', 'ui_config.cc', 'utils.cc', |