summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gtk2_ardour/editor.cc2
-rw-r--r--gtk2_ardour/editor.h2
-rw-r--r--gtk2_ardour/editor_actions.cc1
-rw-r--r--gtk2_ardour/editor_ops.cc28
-rw-r--r--gtk2_ardour/transform_dialog.cc359
-rw-r--r--gtk2_ardour/transform_dialog.h140
-rw-r--r--gtk2_ardour/wscript1
-rw-r--r--libs/ardour/ardour/midi_model.h2
-rw-r--r--libs/ardour/ardour/transform.h145
-rw-r--r--libs/ardour/ardour/variant.h11
-rw-r--r--libs/ardour/midi_model.cc14
-rw-r--r--libs/ardour/transform.cc162
-rw-r--r--libs/ardour/wscript1
13 files changed, 866 insertions, 2 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',
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 <stack>
+#include <string>
+
+#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<Evoral::MusicalTime>::NotePtr NotePtr;
+ typedef Evoral::Sequence<Evoral::MusicalTime>::Notes Notes;
+ typedef ARDOUR::MidiModel::NoteDiffCommand::Property Property;
+
+ /** Context while iterating over notes during transformation. */
+ struct Context {
+ Context() : index(0) {}
+
+ Variant pop();
+
+ std::stack<Variant> 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<Operation> ops; ///< List of operations
+ };
+
+ Transform(const Program& prog);
+
+ Command* operator()(boost::shared_ptr<ARDOUR::MidiModel> model,
+ Evoral::MusicalTime position,
+ std::vector<Notes>& 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 <glib.h>
+
+#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<MidiModel> model,
+ Evoral::MusicalTime position,
+ std::vector<Notes>& seqs)
+{
+ typedef MidiModel::NoteDiffCommand Command;
+
+ Command* cmd = new Command(model, name());
+
+ for (std::vector<Notes>::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<Variant>();
+ ctx.this_note = note;
+ for (std::list<Operation>::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',