From 4c0cebf7f98ecd14873d26b6f4d8bdedd37cb994 Mon Sep 17 00:00:00 2001 From: David Robillard Date: Sun, 28 Dec 2014 15:50:57 -0500 Subject: [PATCH] MIDI transform dialog. --- gtk2_ardour/editor.cc | 2 + gtk2_ardour/editor.h | 2 + gtk2_ardour/editor_actions.cc | 1 + gtk2_ardour/editor_ops.cc | 28 +++ gtk2_ardour/transform_dialog.cc | 359 ++++++++++++++++++++++++++++++++ gtk2_ardour/transform_dialog.h | 140 +++++++++++++ gtk2_ardour/wscript | 1 + libs/ardour/ardour/midi_model.h | 2 + libs/ardour/ardour/transform.h | 145 +++++++++++++ libs/ardour/ardour/variant.h | 11 +- libs/ardour/midi_model.cc | 14 ++ libs/ardour/transform.cc | 162 ++++++++++++++ libs/ardour/wscript | 1 + 13 files changed, 866 insertions(+), 2 deletions(-) create mode 100644 gtk2_ardour/transform_dialog.cc create mode 100644 gtk2_ardour/transform_dialog.h create mode 100644 libs/ardour/ardour/transform.h create mode 100644 libs/ardour/transform.cc 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" @@ -5038,6 +5039,33 @@ Editor::legatize_regions (const RegionSelection& rs, bool shrink_only) apply_midi_note_edit_op (legatize, rs); } +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) { 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 +#include +#include + +#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& 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& 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& choosers = _operations_box.get_children(); + for (std::vector::const_iterator o = choosers.begin(); + o != choosers.end(); ++o) { + OperationChooser* chooser = dynamic_cast(*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 +#include + +#include +#include +#include +#include + +#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; + Gtk::TreeModelColumn label; + }; + + struct PropertyCols : public Gtk::TreeModelColumnRecord { + PropertyCols() { add(property); add(label); } + + Gtk::TreeModelColumn property; + Gtk::TreeModelColumn label; + }; + + struct OperatorCols : public Gtk::TreeModelColumnRecord { + OperatorCols() { add(op); add(label); } + + Gtk::TreeModelColumn op; + Gtk::TreeModelColumn label; + }; + + struct Model { + Model(); + + SourceCols source_cols; + Glib::RefPtr source_list; + PropertyCols property_cols; + Glib::RefPtr property_list; + OperatorCols operator_cols; + Glib::RefPtr operator_list; + }; + + struct ValueChooser : public Gtk::HBox { + ValueChooser(const Model& model); + + /** Append code to `ops` that pushes value to stack. */ + void get(std::list& 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& 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', diff --git a/libs/ardour/ardour/midi_model.h b/libs/ardour/ardour/midi_model.h index 52bb5a6b27..b86a7436bb 100644 --- a/libs/ardour/ardour/midi_model.h +++ b/libs/ardour/ardour/midi_model.h @@ -124,6 +124,8 @@ public: static Variant get_value (const NotePtr note, Property prop); + static Variant::Type value_type (Property prop); + private: struct NoteChange { NoteDiffCommand::Property property; diff --git a/libs/ardour/ardour/transform.h b/libs/ardour/ardour/transform.h new file mode 100644 index 0000000000..2b63bb6af0 --- /dev/null +++ b/libs/ardour/ardour/transform.h @@ -0,0 +1,145 @@ +/* + Copyright (C) 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 __ardour_transform_h__ +#define __ardour_transform_h__ + +#include +#include + +#include "ardour/libardour_visibility.h" +#include "ardour/midi_model.h" +#include "ardour/midi_operator.h" +#include "ardour/types.h" +#include "ardour/variant.h" + +namespace ARDOUR { + +/** Transform notes with a user-defined transformation. + * + * This is essentially an interpreter for a simple concatenative note + * transformation language (as an AST only, no source code). A "program" + * calculates a note property value from operations on literal values, and/or + * values from the current or previous note in the sequence. This allows + * simple things like "set all notes' velocity to 64" or transitions over time + * like "set velocity to the previous note's velocity + 10". + * + * The language is forth-like: everything is on a stack, operations pop their + * arguments from the stack and push their result back on to it. + * + * This is a sweet spot between simplicity and power, it should be simple to + * use this (with perhaps some minor extensions) to do most "linear-ish" + * transformations, though it could be extended to have random access + * and more special values as the need arises. + */ +class LIBARDOUR_API Transform : public MidiOperator { +public: + typedef Evoral::Sequence::NotePtr NotePtr; + typedef Evoral::Sequence::Notes Notes; + typedef ARDOUR::MidiModel::NoteDiffCommand::Property Property; + + /** Context while iterating over notes during transformation. */ + struct Context { + Context() : index(0) {} + + Variant pop(); + + std::stack stack; ///< The stack of everything + size_t index; ///< Index of current note + size_t n_notes; ///< Total number of notes to process + NotePtr prev_note; ///< Previous note + NotePtr this_note; ///< Current note + }; + + /** Value in a transformation expression. */ + struct Value { + /** Value source. Some of these would be better modeled as properties, + like note.index or sequence.size, but until the sequence stuff is + more fundamentally property based, we special-case them here. */ + enum Source { + NOWHERE, ///< Null + THIS_NOTE, ///< Value from this note + PREV_NOTE, ///< Value from the previous note + INDEX, ///< Index of the current note + N_NOTES, ///< Total number of notes to process + LITERAL, ///< Given literal value + RANDOM ///< Random normal + }; + + Value() : source(NOWHERE) {} + Value(Source s) : source(s) {} + Value(const Variant& v) : source(LITERAL), value(v) {} + Value(double v) : source(LITERAL), value(Variant(v)) {} + + /** Calculate and return value. */ + Variant eval(const Context& context) const; + + Source source; ///< Source of value + Variant value; ///< Value for LITERAL + Property prop; ///< Property for all other sources + }; + + /** An operation to transform the running result. + * + * All operations except PUSH take their arguments from the stack, and put + * the result back on the stack. + */ + struct Operation { + enum Operator { + PUSH, ///< Push argument to the stack + ADD, ///< Add top two values + SUB, ///< Subtract top from second-top + MULT, ///< Multiply top two values + DIV ///< Divide second-top by top + }; + + Operation(Operator o, const Value& a=Value()) : op(o), arg(a) {} + + /** Apply operation. */ + void eval(Context& context) const; + + Operator op; + Value arg; + }; + + /** A transformation program. + * + * A program is a list of operations to calculate the target property's + * final value. The first operation must be a PUSH to seed the stack. + */ + struct Program { + Property prop; ///< Property to calculate + std::list ops; ///< List of operations + }; + + Transform(const Program& prog); + + Command* operator()(boost::shared_ptr model, + Evoral::MusicalTime position, + std::vector& seqs); + + std::string name() const { return std::string ("transform"); } + +private: + const Program _prog; +}; + +} /* namespace */ + +#endif /* __ardour_transform_h__ */ diff --git a/libs/ardour/ardour/variant.h b/libs/ardour/ardour/variant.h index d99c0e4fd3..0402ffaa0b 100644 --- a/libs/ardour/ardour/variant.h +++ b/libs/ardour/ardour/variant.h @@ -49,7 +49,8 @@ public: URI ///< URI string }; - explicit Variant() : _type(NOTHING) { _long = 0; } + Variant() : _type(NOTHING) { _long = 0; } + explicit Variant(bool value) : _type(BOOL) { _bool = value; } explicit Variant(double value) : _type(DOUBLE) { _double = value; } explicit Variant(float value) : _type(FLOAT) { _float = value; } @@ -92,6 +93,9 @@ public: _long = (int64_t)lrint(std::max((double)INT64_MIN, std::min(value, (double)INT64_MAX))); break; + case BEATS: + _beats = Evoral::MusicalTime(value); + break; default: _type = NOTHING; _long = 0; @@ -106,6 +110,7 @@ public: case FLOAT: return _float; case INT: return _int; case LONG: return _long; + case BEATS: return _beats.to_double(); default: return 0.0; } } @@ -157,6 +162,8 @@ public: return _type == BEATS && _beats == v; } + bool operator!() const { return _type == NOTHING; } + Variant& operator=(Evoral::MusicalTime v) { _type = BEATS; _beats = v; @@ -171,7 +178,7 @@ public: static bool type_is_numeric(Type type) { switch (type) { - case BOOL: case DOUBLE: case FLOAT: case INT: case LONG: + case BOOL: case DOUBLE: case FLOAT: case INT: case LONG: case BEATS: return true; default: return false; diff --git a/libs/ardour/midi_model.cc b/libs/ardour/midi_model.cc index e68068de2b..b1dbb759d5 100644 --- a/libs/ardour/midi_model.cc +++ b/libs/ardour/midi_model.cc @@ -181,6 +181,20 @@ MidiModel::NoteDiffCommand::get_value (const NotePtr note, Property prop) } } +Variant::Type +MidiModel::NoteDiffCommand::value_type(Property prop) +{ + switch (prop) { + case NoteNumber: + case Velocity: + case Channel: + return Variant::INT; + case StartTime: + case Length: + return Variant::BEATS; + } +} + void MidiModel::NoteDiffCommand::change (const NotePtr note, Property prop, diff --git a/libs/ardour/transform.cc b/libs/ardour/transform.cc new file mode 100644 index 0000000000..ddf23aeecc --- /dev/null +++ b/libs/ardour/transform.cc @@ -0,0 +1,162 @@ +/* + Copyright (C) 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 + +#include "ardour/transform.h" +#include "ardour/midi_model.h" + +namespace ARDOUR { + +Transform::Transform(const Program& prog) + : _prog(prog) +{} + +Variant +Transform::Context::pop() +{ + if (stack.empty()) { + return Variant(); + } + + const Variant top = stack.top(); + stack.pop(); + return top; +} + +Variant +Transform::Value::eval(const Context& ctx) const +{ + switch (source) { + case NOWHERE: + return Variant(); + case THIS_NOTE: + return MidiModel::NoteDiffCommand::get_value(ctx.this_note, prop); + case PREV_NOTE: + if (!ctx.prev_note) { + return Variant(); + } + return MidiModel::NoteDiffCommand::get_value(ctx.prev_note, prop); + case INDEX: + return Variant(Variant::INT, ctx.index); + case N_NOTES: + return Variant(Variant::INT, ctx.n_notes); + case LITERAL: + return value; + case RANDOM: + return Variant(g_random_double()); + } +} + +void +Transform::Operation::eval(Context& ctx) const +{ + if (op == PUSH) { + const Variant a = arg.eval(ctx); + if (!!a) { + /* Argument evaluated to a value, push it to the stack. Otherwise, + there was a reference to the previous note, but this is the + first, so skip this operation and do nothing. */ + ctx.stack.push(a); + } + return; + } + + // Pop operands off the stack + const Variant rhs = ctx.pop(); + const Variant lhs = ctx.pop(); + if (!lhs || !rhs) { + // Stack underflow (probably previous note reference), do nothing + return; + } + + // We can get away with just using double math and converting twice + double value = lhs.to_double(); + switch (op) { + case ADD: + value += rhs.to_double(); + break; + case SUB: + value -= rhs.to_double(); + break; + case MULT: + value *= rhs.to_double(); + break; + case DIV: + if (rhs.to_double() == 0.0) { + return; // Program will fail safely + } + value /= rhs.to_double(); + break; + default: break; + } + + // Push result on to the stack + ctx.stack.push(Variant(lhs.type(), value)); +} + +Command* +Transform::operator()(boost::shared_ptr model, + Evoral::MusicalTime position, + std::vector& seqs) +{ + typedef MidiModel::NoteDiffCommand Command; + + Command* cmd = new Command(model, name()); + + for (std::vector::iterator s = seqs.begin(); s != seqs.end(); ++s) { + Context ctx; + ctx.n_notes = (*s).size(); + for (Notes::const_iterator i = (*s).begin(); i != (*s).end(); ++i) { + const NotePtr note = *i; + + // Clear stack and run program + ctx.stack = std::stack(); + ctx.this_note = note; + for (std::list::const_iterator o = _prog.ops.begin(); + o != _prog.ops.end(); + ++o) { + (*o).eval(ctx); + } + + // Result is on top of the stack + if (!ctx.stack.empty() && !!ctx.stack.top()) { + // Get the result from the top of the stack + Variant result = ctx.stack.top(); + if (result.type() != Command::value_type(_prog.prop)) { + // Coerce to appropriate type + result = Variant(Command::value_type(_prog.prop), + result.to_double()); + } + + // Apply change + cmd->change(note, _prog.prop, result); + } + // else error or reference to note before the first, skip + + // Move forward + ctx.prev_note = note; + ++ctx.index; + } + } + + return cmd; +} + +} // namespace ARDOUR diff --git a/libs/ardour/wscript b/libs/ardour/wscript index 76d39bd0c3..748c412312 100644 --- a/libs/ardour/wscript +++ b/libs/ardour/wscript @@ -213,6 +213,7 @@ libardour_sources = [ 'ticker.cc', 'track.cc', 'transient_detector.cc', + 'transform.cc', 'unknown_processor.cc', 'user_bundle.cc', 'utils.cc',