diff options
Diffstat (limited to 'src')
78 files changed, 10235 insertions, 0 deletions
diff --git a/src/client/ClientModel.cpp b/src/client/ClientModel.cpp new file mode 100644 index 0000000..3a8770f --- /dev/null +++ b/src/client/ClientModel.cpp @@ -0,0 +1,94 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ClientModel.hpp" + +namespace machina { +namespace client { + +SPtr<ClientObject> +ClientModel::find(uint64_t id) +{ + SPtr<ClientObject> key(new ClientObjectKey(id)); + Objects::iterator i = _objects.find(key); + if (i != _objects.end()) { + return *i; + } else { + return SPtr<ClientObject>(); + } +} + +SPtr<const ClientObject> +ClientModel::find(uint64_t id) const +{ + SPtr<ClientObject> key(new ClientObjectKey(id)); + Objects::const_iterator i = _objects.find(key); + if (i != _objects.end()) { + return *i; + } else { + return SPtr<ClientObject>(); + } +} + +void +ClientModel::new_object(uint64_t id, const Properties& properties) +{ + SPtr<ClientObject> key(new ClientObjectKey(id)); + Objects::iterator i = _objects.find(key); + if (i == _objects.end()) { + SPtr<ClientObject> object(new ClientObject(id, properties)); + _objects.insert(object); + signal_new_object.emit(object); + } else { + for (const auto& p : properties) { + (*i)->set(p.first, p.second); + } + } +} + +void +ClientModel::erase_object(uint64_t id) +{ + SPtr<ClientObject> key(new ClientObjectKey(id)); + Objects::iterator i = _objects.find(key); + if (i == _objects.end()) { + return; + } + + signal_erase_object.emit(*i); + (*i)->set_view(NULL); + _objects.erase(i); +} + +void +ClientModel::set(uint64_t id, URIInt key, const Atom& value) +{ + SPtr<ClientObject> object = find(id); + if (object) { + object->set(key, value); + } +} + +const Atom& +ClientModel::get(uint64_t id, URIInt key) const +{ + static const Atom null_atom; + SPtr<const ClientObject> object = find(id); + return object ? object->get(key) : null_atom; +} + +} +} diff --git a/src/client/ClientModel.hpp b/src/client/ClientModel.hpp new file mode 100644 index 0000000..e65fb93 --- /dev/null +++ b/src/client/ClientModel.hpp @@ -0,0 +1,67 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_CLIENTMODEL_HPP +#define MACHINA_CLIENTMODEL_HPP + +#include <set> + +#include <sigc++/sigc++.h> + +#include "machina/Model.hpp" + +#include "ClientObject.hpp" + +namespace Raul { +class Atom; +} + +namespace machina { +namespace client { + +class ClientModel : public Model +{ +public: + void new_object(uint64_t id, const Properties& properties); + + void erase_object(uint64_t id); + + SPtr<ClientObject> find(uint64_t id); + SPtr<const ClientObject> find(uint64_t id) const; + + void set(uint64_t id, URIInt key, const Atom& value); + const Atom& get(uint64_t id, URIInt key) const; + + sigc::signal< void, SPtr<ClientObject> > signal_new_object; + sigc::signal< void, SPtr<ClientObject> > signal_erase_object; + +private: + struct ClientObjectComparator { + inline bool operator()(SPtr<ClientObject> lhs, + SPtr<ClientObject> rhs) const { + return lhs->id() < rhs->id(); + } + + }; + + typedef std::set<SPtr<ClientObject>, ClientObjectComparator> Objects; + Objects _objects; +}; + +} +} + +#endif // MACHINA_CLIENTMODEL_HPP diff --git a/src/client/ClientObject.cpp b/src/client/ClientObject.cpp new file mode 100644 index 0000000..20b50a2 --- /dev/null +++ b/src/client/ClientObject.cpp @@ -0,0 +1,56 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> + +#include "ClientObject.hpp" + +namespace machina { +namespace client { + +ClientObject::ClientObject(uint64_t id, const Properties& properties) + : _id(id) + , _view(NULL) + , _properties(properties) +{} + +ClientObject::ClientObject(const ClientObject& copy, uint64_t id) + : _id(id) + , _view(NULL) + , _properties(copy._properties) +{} + +void +ClientObject::set(URIInt key, const Atom& value) +{ + _properties[key] = value; + signal_property.emit(key, value); +} + +const Atom& +ClientObject::get(URIInt key) const +{ + static const Atom null_atom; + Properties::const_iterator i = _properties.find(key); + if (i != _properties.end()) { + return i->second; + } else { + return null_atom; + } +} + +} +} diff --git a/src/client/ClientObject.hpp b/src/client/ClientObject.hpp new file mode 100644 index 0000000..6520b11 --- /dev/null +++ b/src/client/ClientObject.hpp @@ -0,0 +1,71 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_CLIENTOBJECT_HPP +#define MACHINA_CLIENTOBJECT_HPP + +#include <map> + +#include <sigc++/sigc++.h> + +#include "machina/Atom.hpp" +#include "machina/types.hpp" + +namespace machina { +namespace client { + +class ClientObject +{ +public: + explicit ClientObject(uint64_t id, const Properties& properties={}); + ClientObject(const ClientObject& copy, uint64_t id); + + inline uint64_t id() const { return _id; } + + void set(URIInt key, const Atom& value); + const Atom& get(URIInt key) const; + + sigc::signal<void, URIInt, Atom> signal_property; + + class View + { + public: + virtual ~View() {} + }; + + typedef std::map<URIInt, Atom> Properties; + const Properties& properties() { return _properties; } + + View* view() const { return _view; } + void set_view(View* view) { _view = view; } + +private: + uint64_t _id; + View* _view; + + Properties _properties; +}; + +/** Stub client object to use as a search key. */ +struct ClientObjectKey : public ClientObject +{ + explicit ClientObjectKey(uint64_t id) : ClientObject(id) {} +}; + +} +} + +#endif // MACHINA_CLIENTOBJECT_HPP diff --git a/src/client/wscript b/src/client/wscript new file mode 100644 index 0000000..8333b43 --- /dev/null +++ b/src/client/wscript @@ -0,0 +1,17 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + obj = bld(features = 'cxx cxxshlib') + obj.source = ''' + ClientModel.cpp + ClientObject.cpp + ''' + obj.export_includes = ['.'] + obj.includes = ['.', '..', '../..'] + obj.name = 'libmachina_client' + obj.target = 'machina_client' + obj.use = 'libmachina_engine' + autowaf.use_lib(bld, obj, 'GLIBMM GTKMM RAUL LV2') + + bld.add_post_fun(autowaf.run_ldconfig) diff --git a/src/engine/Action.hpp b/src/engine/Action.hpp new file mode 100644 index 0000000..2916099 --- /dev/null +++ b/src/engine/Action.hpp @@ -0,0 +1,60 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_ACTION_HPP +#define MACHINA_ACTION_HPP + +#include <string> +#include <iostream> + +#include "raul/Maid.hpp" +#include "raul/TimeSlice.hpp" + +#include "machina/types.hpp" + +#include "MIDISink.hpp" +#include "Stateful.hpp" + +namespace machina { + +/** An Action, executed on entering or exiting of a state. + */ +struct Action + : public Raul::Maid::Disposable + , public Stateful +{ + bool operator==(const Action& rhs) const { return false; } + + virtual void execute(MIDISink* sink, Raul::TimeStamp time) = 0; + + virtual void write_state(Sord::Model& model) {} +}; + +class PrintAction : public Action +{ +public: + explicit PrintAction(const std::string& msg) : _msg(msg) {} + + void execute(MIDISink* sink, Raul::TimeStamp time) + { std::cout << "t=" << time << ": " << _msg << std::endl; } + +private: + std::string _msg; +}; + +} // namespace machina + +#endif // MACHINA_ACTION_HPP diff --git a/src/engine/ActionFactory.cpp b/src/engine/ActionFactory.cpp new file mode 100644 index 0000000..f031149 --- /dev/null +++ b/src/engine/ActionFactory.cpp @@ -0,0 +1,55 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ActionFactory.hpp" +#include "MidiAction.hpp" + +namespace machina { + +SPtr<Action> +ActionFactory::copy(SPtr<Action> copy) +{ + SPtr<MidiAction> ma = dynamic_ptr_cast<MidiAction>(copy); + if (ma) { + return SPtr<Action>(new MidiAction(ma->event_size(), ma->event())); + } else { + return SPtr<Action>(); + } +} + +SPtr<Action> +ActionFactory::note_on(uint8_t note, uint8_t velocity) +{ + uint8_t buf[3]; + buf[0] = 0x90; + buf[1] = note; + buf[2] = velocity; + + return SPtr<Action>(new MidiAction(3, buf)); +} + +SPtr<Action> +ActionFactory::note_off(uint8_t note, uint8_t velocity) +{ + uint8_t buf[3]; + buf[0] = 0x80; + buf[1] = note; + buf[2] = velocity; + + return SPtr<Action>(new MidiAction(3, buf)); +} + +} // namespace Machine diff --git a/src/engine/ActionFactory.hpp b/src/engine/ActionFactory.hpp new file mode 100644 index 0000000..db7aa68 --- /dev/null +++ b/src/engine/ActionFactory.hpp @@ -0,0 +1,34 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_ACTIONFACTORY_HPP +#define MACHINA_ACTIONFACTORY_HPP + +#include "machina/types.hpp" + +namespace machina { + +struct Action; + +namespace ActionFactory { +SPtr<Action> copy(SPtr<Action> copy); +SPtr<Action> note_on(uint8_t note, uint8_t velocity=64); +SPtr<Action> note_off(uint8_t note, uint8_t velocity=64); +} + +} // namespace machina + +#endif // MACHINA_ACTIONFACTORY_HPP diff --git a/src/engine/Controller.cpp b/src/engine/Controller.cpp new file mode 100644 index 0000000..8f9e1a2 --- /dev/null +++ b/src/engine/Controller.cpp @@ -0,0 +1,228 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "machina/Controller.hpp" +#include "machina/Engine.hpp" +#include "machina/Machine.hpp" +#include "machina/Updates.hpp" + +#include "Edge.hpp" +#include "MidiAction.hpp" + +namespace machina { + +Controller::Controller(SPtr<Engine> engine, + Model& model) + : _engine(engine) + , _model(model) + , _updates(new Raul::RingBuffer(4096)) +{ + _engine->driver()->set_update_sink(_updates); +} + +uint64_t +Controller::create(const Properties& properties) +{ + std::map<URIInt, Atom>::const_iterator d = properties.find( + URIs::instance().machina_duration); + + double duration = 0.0; + if (d != properties.end()) { + duration = d->second.get<float>(); + } else { + std::cerr << "warning: new object has no duration" << std::endl; + } + + TimeDuration dur(_engine->machine()->time().unit(), duration); + SPtr<Node> node(new Node(dur)); + _objects.insert(node); + _model.new_object(node->id(), properties); + _engine->machine()->add_node(node); + return node->id(); +} + +void +Controller::announce(SPtr<Machine> machine) +{ + if (!machine) { + return; + } + + Forge& forge = _engine->forge(); + + // Announce nodes + for (const auto& n : machine->nodes()) { + Properties properties { + { URIs::instance().rdf_type, + forge.make_urid(URIs::instance().machina_Node) }, + { URIs::instance().machina_duration, + forge.make(float(n->duration().to_double())) } }; + if (n->is_initial()) { + properties.insert({ URIs::instance().machina_initial, + forge.make(true) }); + } + + SPtr<MidiAction> midi_action = dynamic_ptr_cast<MidiAction>( + n->enter_action()); + if (midi_action) { + Properties action_properties { + { URIs::instance().machina_note_number, + forge.make((int32_t)midi_action->event()[1]) } }; + + _model.new_object(midi_action->id(), action_properties); + properties.insert({ URIs::instance().machina_enter_action, + forge.make(int32_t(n->enter_action()->id())) }); + } + + _objects.insert(n); + _model.new_object(n->id(), properties); + } + + // Announce edges + for (const auto& n : machine->nodes()) { + for (const auto& e : n->edges()) { + const Properties properties { + { URIs::instance().rdf_type, + forge.make_urid(URIs::instance().machina_Edge) }, + { URIs::instance().machina_probability, + forge.make(e->probability()) }, + { URIs::instance().machina_tail_id, + forge.make((int32_t)n->id()) }, + { URIs::instance().machina_head_id, + forge.make((int32_t)e->head()->id()) } }; + + _objects.insert(e); + _model.new_object(e->id(), properties); + } + } +} + +SPtr<Stateful> +Controller::find(uint64_t id) +{ + SPtr<StatefulKey> key(new StatefulKey(id)); + Objects::iterator i = _objects.find(key); + if (i != _objects.end()) { + return *i; + } + return SPtr<Stateful>(); +} + +void +Controller::learn(SPtr<Raul::Maid> maid, uint64_t node_id) +{ + SPtr<Node> node = dynamic_ptr_cast<Node>(find(node_id)); + if (node) { + _engine->machine()->learn(maid, node); + } else { + std::cerr << "Failed to find node " << node_id << " for learn" + << std::endl; + } +} + +void +Controller::set_property(uint64_t object_id, + URIInt key, + const Atom& value) +{ + SPtr<Stateful> object = find(object_id); + if (object) { + object->set(key, value); + _model.set(object_id, key, value); + } +} + +uint64_t +Controller::connect(uint64_t tail_id, uint64_t head_id) +{ + SPtr<Node> tail = dynamic_ptr_cast<Node>(find(tail_id)); + SPtr<Node> head = dynamic_ptr_cast<Node>(find(head_id)); + + if (!tail) { + std::cerr << "error: tail node " << tail_id << " not found" << std::endl; + return 0; + } else if (!head) { + std::cerr << "error: head node " << head_id << " not found" << std::endl; + return 0; + } + + SPtr<Edge> edge(new Edge(tail, head)); + tail->add_edge(edge); + _objects.insert(edge); + + Forge& forge = _engine->forge(); + + const Properties properties = { + { URIs::instance().rdf_type, + forge.make_urid(URIs::instance().machina_Edge) }, + { URIs::instance().machina_probability, forge.make(1.0f) }, + { URIs::instance().machina_tail_id, + forge.make((int32_t)tail->id()) }, + { URIs::instance().machina_head_id, + forge.make((int32_t)head->id()) } }; + + _model.new_object(edge->id(), properties); + + return edge->id(); +} + +void +Controller::disconnect(uint64_t tail_id, uint64_t head_id) +{ + SPtr<Node> tail = dynamic_ptr_cast<Node>(find(tail_id)); + SPtr<Node> head = dynamic_ptr_cast<Node>(find(head_id)); + + SPtr<Edge> edge = tail->remove_edge_to(head); + if (edge) { + _model.erase_object(edge->id()); + } else { + std::cerr << "Edge not found" << std::endl; + } +} + +void +Controller::erase(uint64_t id) +{ + SPtr<StatefulKey> key(new StatefulKey(id)); + Objects::iterator i = _objects.find(key); + if (i == _objects.end()) { + return; + } + + SPtr<Node> node = dynamic_ptr_cast<Node>(*i); + if (node) { + _engine->machine()->remove_node(node); + } + + _model.erase_object((*i)->id()); + _objects.erase(i); +} + +void +Controller::process_updates() +{ + const uint32_t read_space = _updates->read_space(); + + uint64_t subject; + URIInt key; + Atom value; + for (uint32_t i = 0; i < read_space; ) { + i += read_set(_updates, &subject, &key, &value); + _model.set(subject, key, value); + } +} + +} diff --git a/src/engine/Edge.cpp b/src/engine/Edge.cpp new file mode 100644 index 0000000..7c206f9 --- /dev/null +++ b/src/engine/Edge.cpp @@ -0,0 +1,64 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "machina/Atom.hpp" +#include "machina/URIs.hpp" +#include "sord/sordmm.hpp" + +#include "Edge.hpp" +#include "Node.hpp" + +namespace machina { + +void +Edge::set(URIInt key, const Atom& value) +{ + if (key == URIs::instance().machina_probability) { + _probability = value.get<float>(); + } +} + +void +Edge::write_state(Sord::Model& model) +{ + using namespace Raul; + + const Sord::Node& rdf_id = this->rdf_id(model.world()); + + SPtr<Node> tail = _tail.lock(); + SPtr<Node> head = _head; + + if (!tail || !head) + return; + + assert(tail->rdf_id(model.world()).is_valid() + && head->rdf_id(model.world()).is_valid()); + + model.add_statement(rdf_id, + Sord::URI(model.world(), MACHINA_NS_tail), + tail->rdf_id(model.world())); + + model.add_statement(rdf_id, + Sord::URI(model.world(), MACHINA_NS_head), + head->rdf_id(model.world())); + + model.add_statement( + rdf_id, + Sord::URI(model.world(), MACHINA_NS_probability), + Sord::Literal::decimal(model.world(), _probability, 7)); +} + +} // namespace machina diff --git a/src/engine/Edge.hpp b/src/engine/Edge.hpp new file mode 100644 index 0000000..a04dd73 --- /dev/null +++ b/src/engine/Edge.hpp @@ -0,0 +1,60 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_EDGE_HPP +#define MACHINA_EDGE_HPP + +#include <list> + +#include "machina/types.hpp" + +#include "Action.hpp" +#include "Stateful.hpp" + +namespace machina { + +class Node; + +class Edge : public Stateful +{ +public: + Edge(WPtr<Node> tail, SPtr<Node> head, float probability=1.0f) + : _tail(tail) + , _head(head) + , _probability(probability) + {} + + void set(URIInt key, const Atom& value); + void write_state(Sord::Model& model); + + WPtr<Node> tail() { return _tail; } + SPtr<Node> head() { return _head; } + + void set_tail(WPtr<Node> tail) { _tail = tail; } + void set_head(SPtr<Node> head) { _head = head; } + + inline float probability() const { return _probability; } + inline void set_probability(float p) { _probability = p; } + +private: + WPtr<Node> _tail; + SPtr<Node> _head; + float _probability; +}; + +} // namespace machina + +#endif // MACHINA_EDGE_HPP diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp new file mode 100644 index 0000000..1aed7b6 --- /dev/null +++ b/src/engine/Engine.cpp @@ -0,0 +1,126 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "machina_config.h" +#include "machina/Engine.hpp" +#include "machina/Loader.hpp" +#include "machina/Machine.hpp" +#include "SMFDriver.hpp" +#ifdef HAVE_JACK +#include "JackDriver.hpp" +#endif + +namespace machina { + +Engine::Engine(Forge& forge, + SPtr<Driver> driver, + Sord::World& rdf_world) + : _driver(driver) + , _rdf_world(rdf_world) + , _loader(_forge, _rdf_world) + , _forge(forge) +{} + +SPtr<Driver> +Engine::new_driver(Forge& forge, + const std::string& name, + SPtr<Machine> machine) +{ + if (name == "jack") { +#ifdef HAVE_JACK + JackDriver* driver = new JackDriver(forge, machine); + driver->attach("machina"); + return SPtr<Driver>(driver); +#else + return SPtr<Driver>(); +#endif + } + if (name == "smf") { + return SPtr<Driver>(new SMFDriver(forge, machine->time().unit())); + } + + std::cerr << "Error: Unknown driver type `" << name << "'" << std::endl; + return SPtr<Driver>(); +} + +/** Load the machine at `uri`, and run it (replacing current machine). + * Safe to call while engine is processing. + */ +SPtr<Machine> +Engine::load_machine(const std::string& uri) +{ + SPtr<Machine> machine = _loader.load(uri); + SPtr<Machine> old_machine = _driver->machine(); // Keep reference + if (machine) { + _driver->set_machine(machine); // Switch driver to new machine and wait + } + + // Drop (possibly last) reference to old_machine in this thread + return machine; +} + +/** Build a machine from the MIDI at `uri`, and run it (replacing current machine). + * Safe to call while engine is processing. + */ +SPtr<Machine> +Engine::load_machine_midi(const std::string& uri, + double q, + Raul::TimeDuration dur) +{ + SPtr<Machine> machine = _loader.load_midi(uri, q, dur); + SPtr<Machine> old_machine = _driver->machine(); // Keep reference + if (machine) { + _driver->set_machine(machine); // Switch driver to new machine and wait + } + + // Drop (possibly last) reference to old_machine in this thread + return machine; +} + +void +Engine::export_midi(const std::string& filename, Raul::TimeDuration dur) +{ + SPtr<Machine> machine = _driver->machine(); + SPtr<machina::SMFDriver> file_driver( + new machina::SMFDriver(_forge, dur.unit())); + + const bool activated = _driver->is_activated(); + if (activated) { + _driver->deactivate(); // FIXME: disable instead + } + file_driver->writer()->start(filename, TimeStamp(dur.unit(), 0.0)); + file_driver->run(machine, dur); + machine->reset(NULL, machine->time()); + file_driver->writer()->finish(); + + if (activated) { + _driver->activate(); + } +} + +void +Engine::set_bpm(double bpm) +{ + _driver->set_bpm(bpm); +} + +void +Engine::set_quantization(double q) +{ + _driver->set_quantization(q); +} + +} // namespace machina diff --git a/src/engine/Evolver.cpp b/src/engine/Evolver.cpp new file mode 100644 index 0000000..973c9c8 --- /dev/null +++ b/src/engine/Evolver.cpp @@ -0,0 +1,139 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> + +#include "eugene/HybridMutation.hpp" +#include "eugene/Mutation.hpp" +#include "eugene/TournamentSelection.hpp" + +#include "machina/Evolver.hpp" +#include "machina/Mutation.hpp" + +#include "Problem.hpp" + +using namespace std; +using namespace eugene; + +namespace machina { + +Evolver::Evolver(TimeUnit unit, + const string& target_midi, + SPtr<Machine> seed) + : _rng(0) + , _problem(new Problem(unit, target_midi, seed)) + , _seed_fitness(-FLT_MAX) + , _exit_flag(false) +{ + SPtr<eugene::HybridMutation<Machine> > m(new HybridMutation<Machine>()); + + m->append_mutation(1 / 6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + new Mutation::Compress())); + m->append_mutation(1 / 6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + new Mutation::AddNode())); + //m->append_mutation(1/6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + // new Mutation::RemoveNode())); + //m->append_mutation(1/6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + // new Mutation::AdjustNode())); + m->append_mutation(1 / 6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + new Mutation::SwapNodes())); + m->append_mutation(1 / 6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + new Mutation::AddEdge())); + m->append_mutation(1 / 6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + new Mutation::RemoveEdge())); + m->append_mutation(1 / 6.0f, std::shared_ptr< eugene::Mutation<Machine> >( + new Mutation::AdjustEdge())); + + std::shared_ptr< Selection<Machine> > s( + new TournamentSelection<Machine>(*_problem.get(), 3, 0.8)); + std::shared_ptr< Crossover<Machine> > crossover; + size_t gene_length = 20; // FIXME + Problem::Population pop; + _ga = SPtr<MachinaGA>( + new MachinaGA(_rng, + _problem, + s, + crossover, + m, + pop, + gene_length, + 20, + 2, + 1.0, + 0.0)); +} + +void +Evolver::start() +{ + if (!_thread) { + _thread = std::unique_ptr<std::thread>( + new std::thread(&Evolver::run, this)); + } +} + +void +Evolver::join() +{ + if (_thread && _thread->joinable()) { + _exit_flag = true; + _thread->join(); + } +} + +void +Evolver::seed(SPtr<Machine> parent) +{ + /*_best = SPtr<Machine>(new Machine(*parent.get())); + _best_fitness = _problem->fitness(*_best.get());*/ + _problem->seed(parent); + _seed_fitness = _problem->evaluate(*parent.get()); +} + +void +Evolver::run() +{ + float old_best = _ga->best_fitness(); + + //cout << "ORIGINAL BEST: " << _ga->best_fitness() << endl; + + _improvement = true; + + while (!_exit_flag) { + //cout << "{" << endl; + _problem->clear_fitness_cache(); + _ga->iteration(); + + float new_best = _ga->best_fitness(); + + /*cout << _problem->fitness_less(old_best, *_ga->best().get()) << endl; + cout << "best: " << _ga->best().get() << endl; + cout << "best fitness: " << _problem->fitness(*_ga->best().get()) << endl; + cout << "old best: " << old_best << endl; + cout << "new best: " << new_best << endl;*/ + cout << "generation best: " << new_best << endl; + + if (_problem->fitness_less_than(old_best, new_best)) { + _improvement = true; + old_best = new_best; + cout << "*** NEW BEST: " << new_best << endl; + } + + //cout << "}" << endl; + } +} + +} // namespace machina diff --git a/src/engine/JackDriver.cpp b/src/engine/JackDriver.cpp new file mode 100644 index 0000000..e7f2e97 --- /dev/null +++ b/src/engine/JackDriver.cpp @@ -0,0 +1,479 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> + +#include "machina/Context.hpp" +#include "machina/URIs.hpp" +#include "machina/Updates.hpp" +#include "machina_config.h" + +#include "Edge.hpp" +#include "JackDriver.hpp" +#include "LearnRequest.hpp" +#include "MachineBuilder.hpp" +#include "MidiAction.hpp" + +using namespace machina; +using namespace std; + +namespace machina { + +JackDriver::JackDriver(Forge& forge, SPtr<Machine> machine) + : Driver(forge, machine) + , _client(NULL) + , _machine_changed(0) + , _input_port(NULL) + , _output_port(NULL) + , _context(forge, 48000, MACHINA_PPQN, 120.0) + , _frames_unit(TimeUnit::FRAMES, 48000) + , _beats_unit(TimeUnit::BEATS, 19200) + , _stop(0) + , _stop_flag(false) + , _record_dur(_frames_unit) // = 0 + , _is_activated(false) +{ + _context.set_sink(this); +} + +JackDriver::~JackDriver() +{ + detach(); +} + +void +JackDriver::attach(const std::string& client_name) +{ + // Already connected + if (_client) { + return; + } + + jack_set_error_function(jack_error_cb); + + _client = jack_client_open(client_name.c_str(), JackNullOption, NULL, NULL); + + if (_client == NULL) { + _is_activated = false; + } else { + jack_set_error_function(jack_error_cb); + jack_on_shutdown(_client, jack_shutdown_cb, this); + jack_set_process_callback(_client, jack_process_cb, this); + } + + if (jack_client()) { + + _context.time().set_tick_rate(sample_rate()); + + _input_port = jack_port_register(jack_client(), + "in", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, + 0); + + if (!_input_port) { + std::cerr << "WARNING: Failed to create MIDI input port." + << std::endl; + } + + _output_port = jack_port_register(jack_client(), + "out", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsOutput, + 0); + + if (!_output_port) { + std::cerr << "WARNING: Failed to create MIDI output port." + << std::endl; + } + + if (!_machine) { + _machine = SPtr<Machine>( + new Machine(TimeUnit::frames( + jack_get_sample_rate(jack_client())))); + } + } +} + +void +JackDriver::detach() +{ + if (_is_activated) { + _is_activated = false; + _stop.timed_wait(std::chrono::seconds(1)); + } + + if (_input_port) { + jack_port_unregister(jack_client(), _input_port); + _input_port = NULL; + } + + if (_output_port) { + jack_port_unregister(jack_client(), _output_port); + _output_port = NULL; + } + + if (_client) { + deactivate(); + jack_client_close(_client); + _client = NULL; + _is_activated = false; + } +} + +void +JackDriver::activate() +{ + if (!jack_activate(_client)) { + _is_activated = true; + } else { + _is_activated = false; + } +} + +void +JackDriver::deactivate() +{ + if (_client) { + jack_deactivate(_client); + } + + _is_activated = false; +} + +void +JackDriver::set_machine(SPtr<Machine> machine) +{ + if (machine == _machine) { + return; + } + + SPtr<Machine> last_machine = _last_machine; // Keep a reference + _machine_changed.reset(0); + assert(!last_machine.unique()); + _machine = machine; + if (is_activated()) { + _machine_changed.wait(); + } + assert(_machine == machine); + last_machine.reset(); +} + +void +JackDriver::read_input_recording(SPtr<Machine> machine, + const Raul::TimeSlice& time) +{ + const jack_nframes_t nframes = time.length_ticks().ticks(); + void* buf = jack_port_get_buffer(_input_port, nframes); + const jack_nframes_t n_events = jack_midi_get_event_count(buf); + + for (jack_nframes_t i = 0; i < n_events; ++i) { + jack_midi_event_t ev; + jack_midi_event_get(&ev, buf, i); + + const TimeStamp rel_time_frames = TimeStamp(_frames_unit, ev.time); + const TimeStamp time_frames = _record_dur + rel_time_frames; + _recorder->write(time.ticks_to_beats(time_frames), ev.size, ev.buffer); + } + + if (n_events > 0) { + _recorder->whip(); + } + + _record_dur += time.length_ticks(); +} + +void +JackDriver::read_input_playing(SPtr<Machine> machine, + const Raul::TimeSlice& time) +{ + const jack_nframes_t nframes = time.length_ticks().ticks(); + void* buf = jack_port_get_buffer(_input_port, nframes); + const jack_nframes_t n_events = jack_midi_get_event_count(buf); + + for (jack_nframes_t i = 0; i < n_events; ++i) { + jack_midi_event_t ev; + jack_midi_event_get(&ev, buf, i); + + if (ev.buffer[0] == 0x90) { + const SPtr<LearnRequest> learn = machine->pending_learn(); + if (learn) { + learn->enter_action()->set_event(ev.size, ev.buffer); + learn->start(_quantization, + TimeStamp(TimeUnit::frames(sample_rate()), + jack_last_frame_time(_client) + + ev.time, 0)); + } + + } else if (ev.buffer[0] == 0x80) { + const SPtr<LearnRequest> learn = machine->pending_learn(); + if (learn && learn->started()) { + learn->exit_action()->set_event(ev.size, ev.buffer); + learn->finish(TimeStamp(TimeUnit::frames(sample_rate()), + jack_last_frame_time(_client) + ev.time, + 0)); + + const uint64_t id = Stateful::next_id(); + write_set(_updates, id, + URIs::instance().rdf_type, + _forge.make_urid(URIs::instance().machina_MidiAction)); + write_set(_updates, learn->node()->id(), + URIs::instance().machina_enter_action, + _forge.make((int32_t)id)); + write_set(_updates, id, + URIs::instance().machina_note_number, + _forge.make((int32_t)ev.buffer[1])); + + machine->clear_pending_learn(); + } + } + } +} + +void +JackDriver::write_event(Raul::TimeStamp time, + size_t size, + const byte* event) +{ + if (!_output_port) { + return; + } + + const Raul::TimeSlice& slice = _context.time(); + + if (slice.beats_to_ticks(time) + slice.offset_ticks() < + slice.start_ticks()) { + std::cerr << "ERROR: Missed event by " + << (slice.start_ticks() + - slice.beats_to_ticks(time) + + slice.offset_ticks()) + << " ticks" + << "\n\tbpm: " << slice.bpm() + << "\n\tev time: " << slice.beats_to_ticks(time) + << "\n\tcycle_start: " << slice.start_ticks() + << "\n\tcycle_end: " << (slice.start_ticks() + + slice.length_ticks()) + << "\n\tcycle_length: " << slice.length_ticks() + << std::endl << std::endl; + return; + } + + const TimeDuration nframes = slice.length_ticks(); + const TimeStamp offset = slice.beats_to_ticks(time) + + slice.offset_ticks() - slice.start_ticks(); + + if (!(offset < slice.offset_ticks() + nframes)) { + std::cerr << "ERROR: Event offset " << offset << " outside cycle " + << "\n\tbpm: " << slice.bpm() + << "\n\tev time: " << slice.beats_to_ticks(time) + << "\n\tcycle_start: " << slice.start_ticks() + << "\n\tcycle_end: " << slice.start_ticks() + + slice.length_ticks() + << "\n\tcycle_length: " << slice.length_ticks() << std::endl; + } else { +#ifdef JACK_MIDI_NEEDS_NFRAMES + jack_midi_event_write( + jack_port_get_buffer(_output_port, nframes), offset, + event, size, nframes); +#else + jack_midi_event_write( + jack_port_get_buffer(_output_port, nframes.ticks()), offset.ticks(), + event, size); +#endif + } +} + +void +JackDriver::on_process(jack_nframes_t nframes) +{ + if (!_is_activated) { + _stop.post(); + return; + } + + _context.time().set_bpm(_bpm); + + assert(_output_port); + jack_midi_clear_buffer(jack_port_get_buffer(_output_port, nframes)); + + TimeStamp length_ticks(TimeStamp(_context.time().ticks_unit(), nframes)); + + _context.time().set_length(length_ticks); + _context.time().set_offset(TimeStamp(_context.time().ticks_unit(), 0, 0)); + + /* Take a reference to machine here and use only it during the process + * cycle so _machine can be switched with set_machine during a cycle. */ + SPtr<Machine> machine = _machine; + + // Machine was switched since last cycle, finalize old machine. + if (machine != _last_machine) { + if (_last_machine) { + assert(!_last_machine.unique()); // Realtime, can't delete + _last_machine->reset(_context.sink(), _last_machine->time()); // Exit all active states + _last_machine.reset(); // Cut our reference + } + _machine_changed.post(); // Signal we're done with it + } + + if (!machine) { + _last_machine = machine; + goto end; + } + + if (_stop_flag) { + machine->reset(_context.sink(), _context.time().start_beats()); + } + + switch (_play_state) { + case PlayState::STOPPED: + break; + case PlayState::PLAYING: + read_input_playing(machine, _context.time()); + break; + case PlayState::RECORDING: + case PlayState::STEP_RECORDING: + read_input_recording(machine, _context.time()); + break; + } + + if (machine->is_empty()) { + goto end; + } + + while (true) { + const uint32_t run_dur_frames = machine->run(_context, _updates); + + if (run_dur_frames == 0) { + // Machine didn't run at all (machine has no initial states) + machine->reset(_context.sink(), machine->time()); // Try again next cycle + _context.time().set_slice(TimeStamp(_frames_unit, 0, 0), + TimeStamp(_frames_unit, 0, 0)); + break; + + } else if (machine->is_finished()) { + // Machine ran for portion of cycle and is finished + machine->reset(_context.sink(), machine->time()); + + _context.time().set_slice(TimeStamp(_frames_unit, 0, 0), + TimeStamp(_frames_unit, nframes + - run_dur_frames, 0)); + _context.time().set_offset(TimeStamp(_frames_unit, run_dur_frames, + 0)); + + } else { + // Machine ran for entire cycle + _context.time().set_slice( + _context.time().start_ticks() + _context.time().length_ticks(), + TimeStamp(_frames_unit, 0, 0)); + break; + } + } + +end: + /* Remember the last machine run, in case a switch happens and + * we need to finalize it next cycle. */ + _last_machine = machine; + + if (_stop_flag) { + _context.time().set_slice(TimeStamp(_frames_unit, 0, 0), + TimeStamp(_frames_unit, 0, 0)); + _stop_flag = false; + _stop.post(); + } +} + +void +JackDriver::set_play_state(PlayState state) +{ + switch (state) { + case PlayState::STOPPED: + switch (_play_state) { + case PlayState::STOPPED: + break; + case PlayState::RECORDING: + case PlayState::STEP_RECORDING: + finish_record(); + // fallthru + case PlayState::PLAYING: + _stop_flag = true; + _stop.wait(); + } + break; + case PlayState::RECORDING: + start_record(false); + break; + case PlayState::STEP_RECORDING: + start_record(true); + break; + case PlayState::PLAYING: + if (_play_state == PlayState::RECORDING || + _play_state == PlayState::STEP_RECORDING) { + finish_record(); + } + } + Driver::set_play_state(state); +} + +void +JackDriver::start_record(bool step) +{ + const double q = (step || _quantize_record) ? _quantization : 0.0; + switch (_play_state) { + case PlayState::STOPPED: + case PlayState::PLAYING: + _recorder = SPtr<Recorder>( + new Recorder(_forge, 1024, _beats_unit, q, step)); + _record_dur = 0; + break; + case PlayState::RECORDING: + case PlayState::STEP_RECORDING: + _recorder->builder()->set_step(true); + break; + } + _play_state = step ? PlayState::STEP_RECORDING : PlayState::RECORDING; +} + +void +JackDriver::finish_record() +{ + _play_state = PlayState::PLAYING; + SPtr<Machine> machine = _recorder->finish(); + _recorder.reset(); + _machine->merge(*machine.get()); +} + +int +JackDriver::jack_process_cb(jack_nframes_t nframes, void* jack_driver) +{ + JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); + me->on_process(nframes); + return 0; +} + +void +JackDriver::jack_shutdown_cb(void* jack_driver) +{ + JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); + me->_client = NULL; +} + +void +JackDriver::jack_error_cb(const char* msg) +{ + cerr << "[JACK] Error: " << msg << endl; +} + +} // namespace machina diff --git a/src/engine/JackDriver.hpp b/src/engine/JackDriver.hpp new file mode 100644 index 0000000..4ebfaa1 --- /dev/null +++ b/src/engine/JackDriver.hpp @@ -0,0 +1,119 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_JACKDRIVER_HPP +#define MACHINA_JACKDRIVER_HPP + +#include <jack/jack.h> +#include <jack/midiport.h> + +#include "raul/Semaphore.hpp" + +#include "machina/Context.hpp" +#include "machina/Driver.hpp" +#include "machina/Machine.hpp" +#include "machina/types.hpp" + +#include "Recorder.hpp" + +namespace machina { + +class MidiAction; +class Node; + +/** Realtime JACK Driver. + * + * "Ticks" are individual frames when running under this driver, and all code + * in the processing context must be realtime safe (non-blocking). + */ +class JackDriver : public machina::Driver +{ +public: + JackDriver(Forge& forge, + SPtr<Machine> machine = SPtr<Machine>()); + ~JackDriver(); + + void attach(const std::string& client_name); + void detach(); + + void activate(); + void deactivate(); + + void set_machine(SPtr<Machine> machine); + + void write_event(Raul::TimeStamp time, + size_t size, + const unsigned char* event); + + void set_play_state(PlayState state); + + void start_transport() { jack_transport_start(_client); } + void stop_transport() { jack_transport_stop(_client); } + + void rewind_transport() { + jack_position_t zero; + zero.frame = 0; + zero.valid = (jack_position_bits_t)0; + jack_transport_reposition(_client, &zero); + } + + bool is_activated() const { return _is_activated; } + bool is_attached() const { return _client != NULL; } + bool is_realtime() const { return _client && jack_is_realtime(_client); } + + jack_nframes_t sample_rate() const { return jack_get_sample_rate(_client); } + jack_client_t* jack_client() const { return _client; } + +private: + void read_input_recording(SPtr<Machine> machine, + const Raul::TimeSlice& time); + + void read_input_playing(SPtr<Machine> machine, + const Raul::TimeSlice& time); + + static void jack_error_cb(const char* msg); + static int jack_process_cb(jack_nframes_t nframes, void* me); + static void jack_shutdown_cb(void* me); + + void start_record(bool step); + void finish_record(); + + void on_process(jack_nframes_t nframes); + + jack_client_t* _client; + + Raul::Semaphore _machine_changed; + SPtr<Machine> _last_machine; + + jack_port_t* _input_port; + jack_port_t* _output_port; + + Context _context; + + Raul::TimeUnit _frames_unit; + Raul::TimeUnit _beats_unit; + + Raul::Semaphore _stop; + bool _stop_flag; + + Raul::TimeDuration _record_dur; + SPtr<Recorder> _recorder; + bool _is_activated; +}; + +} // namespace machina + +#endif // MACHINA_JACKDRIVER_HPP diff --git a/src/engine/LearnRequest.cpp b/src/engine/LearnRequest.cpp new file mode 100644 index 0000000..5756dd1 --- /dev/null +++ b/src/engine/LearnRequest.cpp @@ -0,0 +1,56 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "LearnRequest.hpp" +#include "quantize.hpp" + +namespace machina { + +LearnRequest::LearnRequest(SPtr<Raul::Maid> maid, SPtr<Node> node) + : _started(false) + , _start_time(TimeUnit(TimeUnit::BEATS, 19200), 0, 0) // irrelevant + , _quantization(0) // irrelevant + , _node(node) + , _enter_action(new MidiAction(4, NULL)) + , _exit_action(new MidiAction(4, NULL)) +{ +} + +SPtr<LearnRequest> +LearnRequest::create(SPtr<Raul::Maid> maid, SPtr<Node> node) +{ + return SPtr<LearnRequest>(new LearnRequest(maid, node)); +} + +void +LearnRequest::start(double q, Raul::TimeStamp time) +{ + _started = true; + _start_time = time; + _quantization = q; +} + +/** Add the learned actions to the node */ +void +LearnRequest::finish(TimeStamp time) +{ + _node->set_enter_action(_enter_action); + _node->set_exit_action(_exit_action); + + _node->set_duration(time - _start_time); +} + +} diff --git a/src/engine/LearnRequest.hpp b/src/engine/LearnRequest.hpp new file mode 100644 index 0000000..9cdfc3c --- /dev/null +++ b/src/engine/LearnRequest.hpp @@ -0,0 +1,61 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_LEARNREQUEST_HPP +#define MACHINA_LEARNREQUEST_HPP + +#include "raul/Maid.hpp" + +#include "machina/types.hpp" + +#include "MidiAction.hpp" +#include "Node.hpp" + +namespace machina { + +class Node; +class MidiAction; + +/** A request to MIDI learn a certain node. + */ +class LearnRequest : public Raul::Maid::Disposable +{ +public: + static SPtr<LearnRequest> create(SPtr<Raul::Maid> maid, SPtr<Node> node); + + void start(double q, Raul::TimeStamp time); + void finish(TimeStamp time); + + inline bool started() const { return _started; } + + const SPtr<Node>& node() { return _node; } + const SPtr<MidiAction>& enter_action() { return _enter_action; } + const SPtr<MidiAction>& exit_action() { return _exit_action; } + +private: + LearnRequest(SPtr<Raul::Maid> maid, SPtr<Node> node); + + bool _started; + TimeStamp _start_time; + double _quantization; + SPtr<Node> _node; + SPtr<MidiAction> _enter_action; + SPtr<MidiAction> _exit_action; +}; + +} // namespace machina + +#endif // MACHINA_LEARNREQUEST_HPP diff --git a/src/engine/Loader.cpp b/src/engine/Loader.cpp new file mode 100644 index 0000000..1c662d2 --- /dev/null +++ b/src/engine/Loader.cpp @@ -0,0 +1,193 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> +#include <iostream> +#include <map> + +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "machina/Loader.hpp" +#include "machina/URIs.hpp" +#include "machina/Machine.hpp" + +#include "Edge.hpp" +#include "MidiAction.hpp" +#include "Node.hpp" +#include "SMFDriver.hpp" +#include "machina_config.h" + +using namespace Raul; +using namespace std; + +namespace machina { + +Loader::Loader(Forge& forge, Sord::World& rdf_world) + : _forge(forge) + , _rdf_world(rdf_world) +{} + +static SPtr<Action> +load_action(Sord::Model& model, Sord::Node node) +{ + if (!node.is_valid()) { + return SPtr<Action>(); + } + + Sord::URI rdf_type(model.world(), MACHINA_URI_RDF "type"); + Sord::URI midi_NoteOn(model.world(), LV2_MIDI__NoteOn); + Sord::URI midi_NoteOff(model.world(), LV2_MIDI__NoteOff); + Sord::URI midi_noteNumber(model.world(), LV2_MIDI__noteNumber); + Sord::URI midi_velocity(model.world(), LV2_MIDI__velocity); + + Sord::Node type = model.get(node, rdf_type, Sord::Node()); + uint8_t status = 0; + if (type == midi_NoteOn) { + status = LV2_MIDI_MSG_NOTE_ON; + } else if (type == midi_NoteOff) { + status = LV2_MIDI_MSG_NOTE_OFF; + } else { + return SPtr<Action>(); + } + + Sord::Node num_node = model.get(node, midi_noteNumber, Sord::Node()); + Sord::Node vel_node = model.get(node, midi_velocity, Sord::Node()); + + const uint8_t num = num_node.is_int() ? num_node.to_int() : 64; + const uint8_t vel = vel_node.is_int() ? vel_node.to_int() : 64; + const uint8_t event[3] = { status, num, vel }; + + return SPtr<Action>(new MidiAction(sizeof(event), event)); +} + +/** Load (create) all objects from RDF into the engine. + * + * @param uri URI of machine (resolvable URI to an RDF document). + * @return Loaded Machine. + */ +SPtr<Machine> +Loader::load(const std::string& uri) +{ + std::string document_uri = uri; + + // If "URI" doesn't contain a colon, try to resolve as a filename + if (uri.find(":") == string::npos) { + document_uri = "file://" + document_uri; + } + + cout << "Loading " << document_uri << endl; + + TimeUnit beats(TimeUnit::BEATS, MACHINA_PPQN); + + SPtr<Machine> machine(new Machine(beats)); + + typedef std::map<Sord::Node, SPtr<Node> > Created; + Created created; + + Sord::URI base_uri(_rdf_world, document_uri); + Sord::Model model(_rdf_world, document_uri); + + SerdEnv* env = serd_env_new(base_uri.to_serd_node()); + model.load_file(env, SERD_TURTLE, document_uri); + serd_env_free(env); + + Sord::Node nil; + + Sord::URI machina_SelectorNode(_rdf_world, MACHINA_NS_SelectorNode); + Sord::URI machina_duration(_rdf_world, MACHINA_NS_duration); + Sord::URI machina_edge(_rdf_world, MACHINA_NS_arc); + Sord::URI machina_head(_rdf_world, MACHINA_NS_head); + Sord::URI machina_node(_rdf_world, MACHINA_NS_node); + Sord::URI machina_onEnter(_rdf_world, MACHINA_NS_onEnter); + Sord::URI machina_onExit(_rdf_world, MACHINA_NS_onExit); + Sord::URI machina_probability(_rdf_world, MACHINA_NS_probability); + Sord::URI machina_start(_rdf_world, MACHINA_NS_start); + Sord::URI machina_tail(_rdf_world, MACHINA_NS_tail); + Sord::URI rdf_type(_rdf_world, MACHINA_URI_RDF "type"); + + Sord::Node subject = base_uri; + + // Get start node ID (but re-use existing start node) + Sord::Iter i = model.find(subject, machina_start, nil); + if (i.end()) { + cerr << "error: Machine has no start node" << std::endl; + } + created[i.get_object()] = machine->initial_node(); + + // Get remaining nodes + for (Sord::Iter i = model.find(subject, machina_node, nil); !i.end(); ++i) { + const Sord::Node& id = i.get_object(); + if (created.find(id) != created.end()) { + cerr << "warning: Machine lists the same node twice" << std::endl; + continue; + } + + // Create node + Sord::Iter d = model.find(id, machina_duration, nil); + SPtr<Node> node(new Node(TimeStamp(beats, d.get_object().to_float()))); + machine->add_node(node); + created[id] = node; + + node->set_enter_action( + load_action(model, model.get(id, machina_onEnter, nil))); + node->set_exit_action( + load_action(model, model.get(id, machina_onExit, nil))); + } + + // Get arcs + for (Sord::Iter i = model.find(subject, machina_edge, nil); !i.end(); ++i) { + Sord::Node edge = i.get_object(); + Sord::Iter t = model.find(edge, machina_tail, nil); + Sord::Iter h = model.find(edge, machina_head, nil); + Sord::Iter p = model.find(edge, machina_probability, nil); + + Sord::Node tail = t.get_object(); + Sord::Node head = h.get_object(); + Sord::Node probability = p.get_object(); + + float prob = probability.to_float(); + + Created::iterator tail_i = created.find(tail); + Created::iterator head_i = created.find(head); + + if (tail_i != created.end() && head_i != created.end()) { + const SPtr<Node> tail = tail_i->second; + const SPtr<Node> head = head_i->second; + tail->add_edge(SPtr<Edge>(new Edge(tail, head, prob))); + } else { + cerr << "warning: Ignored edge between unknown nodes " + << tail << " -> " << head << endl; + } + } + + if (machine && !machine->nodes().empty()) { + machine->reset(NULL, machine->time()); + return machine; + } else { + return SPtr<Machine>(); + } +} + +SPtr<Machine> +Loader::load_midi(const std::string& uri, + double q, + Raul::TimeDuration dur) +{ + SPtr<SMFDriver> file_driver(new SMFDriver(_forge, dur.unit())); + return file_driver->learn(uri, q, dur); +} + +} // namespace machina diff --git a/src/engine/MIDISink.hpp b/src/engine/MIDISink.hpp new file mode 100644 index 0000000..983dbba --- /dev/null +++ b/src/engine/MIDISink.hpp @@ -0,0 +1,37 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_MIDI_SINK_HPP +#define MACHINA_MIDI_SINK_HPP + +#include "raul/Deletable.hpp" +#include "raul/TimeStamp.hpp" + +namespace machina { + +/** Pure virtual base for anything you can write MIDI to. */ +class MIDISink + : public Raul::Deletable +{ +public: + virtual void write_event(Raul::TimeStamp time, + size_t ev_size, + const uint8_t* ev) = 0; +}; + +} // namespace machina + +#endif // MACHINA_MIDI_SINK_HPP diff --git a/src/engine/Machine.cpp b/src/engine/Machine.cpp new file mode 100644 index 0000000..b144b6f --- /dev/null +++ b/src/engine/Machine.cpp @@ -0,0 +1,389 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <map> + +#include "sord/sordmm.hpp" + +#include "machina/Atom.hpp" +#include "machina/Context.hpp" +#include "machina/Machine.hpp" +#include "machina/URIs.hpp" +#include "machina/Updates.hpp" +#include "machina/types.hpp" + +#include "Edge.hpp" +#include "Node.hpp" +#include "LearnRequest.hpp" +#include "MidiAction.hpp" + +using namespace std; +using namespace Raul; + +namespace machina { + +Machine::Machine(TimeUnit unit) + : fitness(0.0) + , _initial_node(new Node(TimeStamp(unit, 0, 0), true)) + , _active_nodes(MAX_ACTIVE_NODES, SPtr<Node>()) + , _time(unit, 0, 0) + , _is_finished(false) +{ + _nodes.insert(_initial_node); +} + +void +Machine::assign(const Machine& copy) +{ + std::map< SPtr<Node>, SPtr<Node> > replacements; + + replacements[copy.initial_node()] = _initial_node; + + for (const auto& n : copy._nodes) { + if (!n->is_initial()) { + SPtr<machina::Node> node(new machina::Node(*n.get())); + _nodes.insert(node); + replacements[n] = node; + } + } + + for (const auto& n : _nodes) { + for (const auto& e : n->edges()) { + e->set_tail(n); + e->set_head(replacements[e->head()]); + } + } +} + +Machine::Machine(const Machine& copy) + : Stateful() // don't copy RDF ID + , fitness(0.0) + , _initial_node(new Node(TimeStamp(copy.time().unit(), 0, 0), true)) + , _active_nodes(MAX_ACTIVE_NODES, SPtr<Node>()) + , _time(copy.time()) + , _is_finished(false) +{ + _nodes.insert(_initial_node); + assign(copy); +} + +Machine& +Machine::operator=(const Machine& copy) +{ + if (© == this) { + return *this; + } + + fitness = copy.fitness; + + _active_nodes = std::vector< SPtr<Node> >(MAX_ACTIVE_NODES, SPtr<Node>()); + _is_finished = false; + _time = copy._time; + _pending_learn = SPtr<LearnRequest>(); + + _nodes.clear(); + _nodes.insert(_initial_node); + assign(copy); + + return *this; +} + +bool +Machine::operator==(const Machine& rhs) const +{ + return false; +} + +void +Machine::merge(const Machine& machine) +{ + for (const auto& m : machine.nodes()) { + if (m->is_initial()) { + for (const auto& e : m->edges()) { + e->set_tail(_initial_node); + _initial_node->edges().insert(e); + } + } else { + _nodes.insert(m); + } + } +} + +/** Always returns a node, unless there are none */ +SPtr<Node> +Machine::random_node() +{ + if (_nodes.empty()) { + return SPtr<Node>(); + } + + size_t i = rand() % _nodes.size(); + + // FIXME: O(n) worst case :( + for (Nodes::const_iterator n = _nodes.begin(); n != _nodes.end(); ++n, + --i) { + if (i == 0) { + return *n; + } + } + + return SPtr<Node>(); +} + +/** May return NULL even if edges exist (with low probability) */ +SPtr<Edge> +Machine::random_edge() +{ + SPtr<Node> tail = random_node(); + + for (size_t i = 0; i < _nodes.size() && tail->edges().empty(); ++i) { + tail = random_node(); + } + + return tail ? tail->random_edge() : SPtr<Edge>(); +} + +void +Machine::add_node(SPtr<Node> node) +{ + _nodes.insert(node); +} + +void +Machine::remove_node(SPtr<Node> node) +{ + _nodes.erase(node); + + for (Nodes::const_iterator n = _nodes.begin(); n != _nodes.end(); ++n) { + (*n)->remove_edge_to(node); + } +} + +void +Machine::reset(MIDISink* sink, Raul::TimeStamp time) +{ + if (!_is_finished) { + for (auto& n : _active_nodes) { + if (n) { + n->exit(sink, time); + n.reset(); + } + } + } + + _time = TimeStamp(_time.unit(), 0, 0); + _is_finished = false; +} + +SPtr<Node> +Machine::earliest_node() const +{ + SPtr<Node> earliest; + + for (const auto& n : _active_nodes) { + if (n && (!earliest || n->exit_time() < earliest->exit_time())) { + earliest = n; + } + } + + return earliest; +} + +bool +Machine::enter_node(Context& context, + SPtr<Node> node, + SPtr<Raul::RingBuffer> updates) +{ + assert(!node->is_active()); + assert(_active_nodes.size() == MAX_ACTIVE_NODES); + + /* FIXME: Would be best to use the MIDI note here as a hash key, at least + * while all actions are still MIDI notes... */ + size_t index = (rand() % MAX_ACTIVE_NODES); + for (size_t i = 0; i < MAX_ACTIVE_NODES; ++i) { + if (!_active_nodes[index]) { + node->enter(context.sink(), _time); + _active_nodes[index] = node; + + write_set(updates, + node->id(), + URIs::instance().machina_active, + context.forge().make(true)); + return true; + } + index = (index + 1) % MAX_ACTIVE_NODES; + } + + // If we get here, ran out of active node spots. Don't enter node + return false; +} + +void +Machine::exit_node(Context& context, + SPtr<Node> node, + SPtr<Raul::RingBuffer> updates) +{ + // Exit node + node->exit(context.sink(), _time); + + // Notify UI + write_set(updates, + node->id(), + URIs::instance().machina_active, + context.forge().make(false)); + + // Remove node from _active_nodes + for (auto& n : _active_nodes) { + if (n == node) { + n.reset(); + } + } + + // Activate successors + if (node->is_selector()) { + const double rand_normal = rand() / (double)RAND_MAX; // [0, 1] + double range_min = 0; + + for (const auto& e : node->edges()) { + if (!e->head()->is_active() + && rand_normal > range_min + && rand_normal < range_min + e->probability()) { + + enter_node(context, e->head(), updates); + break; + + } else { + range_min += e->probability(); + } + } + } else { + for (const auto& e : node->edges()) { + const double rand_normal = rand() / (double)RAND_MAX; // [0, 1] + if (rand_normal <= e->probability()) { + SPtr<Node> head = e->head(); + + if (!head->is_active()) { + enter_node(context, head, updates); + } + } + } + } +} + +uint32_t +Machine::run(Context& context, SPtr<Raul::RingBuffer> updates) +{ + if (_is_finished) { + return 0; + } + + const Raul::TimeSlice& time = context.time(); + + const TimeStamp end_frames = (time.start_ticks() + time.length_ticks()); + const TimeStamp end_beats = time.ticks_to_beats(end_frames); + + if (_time.is_zero()) { // Initial run + // Exit any active nodes + for (auto& n : _active_nodes) { + if (n && n->is_active()) { + n->exit(context.sink(), _time); + write_set(updates, + n->id(), + URIs::instance().machina_active, + context.forge().make(false)); + } + n.reset(); + } + + // Enter initial node + enter_node(context, _initial_node, updates); + + if (_initial_node->edges().empty()) { // Nowhere to go, exit + _is_finished = true; + return 0; + } + } + + while (true) { + SPtr<Node> earliest = earliest_node(); + if (!earliest) { + // No more active states, machine is finished + _is_finished = true; + break; + } + + const TimeStamp exit_time = earliest->exit_time(); + if (time.beats_to_ticks(exit_time) < end_frames) { + // Earliest active state ends this cycle, exit it + _time = earliest->exit_time(); + exit_node(context, earliest, updates); + + } else { + // Earliest active state ends in the future, done this cycle + _time = end_beats; + break; + } + + } + + return time.beats_to_ticks(_time).ticks() - time.start_ticks().ticks(); +} + +void +Machine::learn(SPtr<Raul::Maid> maid, SPtr<Node> node) +{ + _pending_learn = LearnRequest::create(maid, node); +} + +void +Machine::write_state(Sord::Model& model) +{ + using namespace Raul; + + model.add_statement(model.base_uri(), + Sord::URI(model.world(), MACHINA_URI_RDF "type"), + Sord::URI(model.world(), MACHINA_NS_Machine)); + + for (Nodes::const_iterator n = _nodes.begin(); n != _nodes.end(); ++n) { + (*n)->write_state(model); + + if ((*n)->is_initial()) { + model.add_statement(model.base_uri(), + Sord::URI(model.world(), MACHINA_NS_start), + (*n)->rdf_id(model.world())); + } else { + model.add_statement(model.base_uri(), + Sord::URI(model.world(), MACHINA_NS_node), + (*n)->rdf_id(model.world())); + } + } + + for (Nodes::const_iterator n = _nodes.begin(); n != _nodes.end(); ++n) { + for (Node::Edges::const_iterator e = (*n)->edges().begin(); + e != (*n)->edges().end(); ++e) { + + (*e)->write_state(model); + + model.add_statement(model.base_uri(), + Sord::URI(model.world(), MACHINA_NS_arc), + (*e)->rdf_id(model.world())); + } + + } +} + +} // namespace machina diff --git a/src/engine/MachineBuilder.cpp b/src/engine/MachineBuilder.cpp new file mode 100644 index 0000000..d812b06 --- /dev/null +++ b/src/engine/MachineBuilder.cpp @@ -0,0 +1,325 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "machina/Machine.hpp" +#include "machina/types.hpp" + +#include "Edge.hpp" +#include "MachineBuilder.hpp" +#include "MidiAction.hpp" +#include "Node.hpp" +#include "quantize.hpp" + +using namespace std; +using namespace Raul; + +namespace machina { + +MachineBuilder::MachineBuilder(SPtr<Machine> machine, double q, bool step) + : _quantization(q) + , _time(machine->time().unit()) // = 0 + , _machine(machine) + , _initial_node(machine->initial_node()) // duration 0 + , _connect_node(_initial_node) + , _connect_node_end_time(_time) // = 0 + , _step_duration(_time.unit(), q) + , _step(step) +{} + +void +MachineBuilder::reset() +{ + _time = TimeStamp(_machine->time().unit()); // = 0 + _connect_node = _initial_node; + _connect_node_end_time = _time; // = 0 +} + +bool +MachineBuilder::is_delay_node(SPtr<Node> node) const +{ + return node != _initial_node && + !node->enter_action() && + !node->exit_action(); +} + +/** Set the duration of a node, with quantization. + */ +void +MachineBuilder::set_node_duration(SPtr<Node> node, + Raul::TimeDuration d) const +{ + if (_step) { + node->set_duration(_step_duration); + return; + } + + Raul::TimeStamp q_dur = quantize(TimeStamp(d.unit(), _quantization), d); + + // Never quantize a note to duration 0 + if (q_dur.is_zero() && (node->enter_action() || node->exit_action())) { + q_dur = _quantization; // Round up + } + node->set_duration(q_dur); +} + +/** Connect two nodes, inserting a delay node between them if necessary. + * + * If a delay node is added to the machine, it is returned. + */ +SPtr<Node> +MachineBuilder::connect_nodes(SPtr<Machine> m, + SPtr<Node> tail, + Raul::TimeStamp tail_end_time, + SPtr<Node> head, + Raul::TimeStamp head_start_time) +{ + SPtr<Node> delay_node; + if (tail == head) { + return delay_node; + } + + if (is_delay_node(tail) && tail->edges().empty()) { + // Tail is a delay node, just accumulate the time difference into it + set_node_duration(tail, + tail->duration() + head_start_time - tail_end_time); + tail->add_edge(SPtr<Edge>(new Edge(tail, head))); + } else if (_step || (head_start_time == tail_end_time)) { + // Connect directly + tail->add_edge(SPtr<Edge>(new Edge(tail, head))); + } else { + // Need to actually create a delay node + delay_node = SPtr<Node>(new Node(head_start_time - tail_end_time)); + tail->add_edge(SPtr<Edge>(new Edge(tail, delay_node))); + delay_node->add_edge(SPtr<Edge>(new Edge(delay_node, head))); + m->add_node(delay_node); + } + + return delay_node; +} + +void +MachineBuilder::note_on(Raul::TimeStamp t, size_t ev_size, uint8_t* buf) +{ + SPtr<Node> node; + if (_step && _poly_nodes.empty() && is_delay_node(_connect_node)) { + /* Stepping and the connect node is the merge node after a polyphonic + group. Re-use it to avoid creating delay nodes in step mode. */ + node = _connect_node; + node->set_duration(_step_duration); + } else { + node = SPtr<Node>(new Node(default_duration())); + } + + node->set_enter_action(SPtr<Action>(new MidiAction(ev_size, buf))); + + if (_step && _poly_nodes.empty()) { + t = _time = _time + _step_duration; // Advance time one step + } + + SPtr<Node> this_connect_node; + Raul::TimeStamp this_connect_node_end_time(t.unit()); + + /* If currently polyphonic, use a poly node with no successors as connect + node, for more sensible patterns like what a human would build. */ + if (!_poly_nodes.empty()) { + for (PolyList::iterator j = _poly_nodes.begin(); + j != _poly_nodes.end(); ++j) { + if (j->second->edges().empty()) { + this_connect_node = j->second; + this_connect_node_end_time = j->first + j->second->duration(); + break; + } + } + } + + /* Currently monophonic, or didn't find a poly node, so use _connect_node + which is maintained below on note off events. */ + if (!this_connect_node) { + this_connect_node = _connect_node; + this_connect_node_end_time = _connect_node_end_time; + } + + SPtr<Node> delay_node = connect_nodes( + _machine, this_connect_node, this_connect_node_end_time, node, t); + + if (delay_node) { + _connect_node = delay_node; + _connect_node_end_time = t; + } + + node->enter(NULL, t); + _active_nodes.push_back(node); +} + +void +MachineBuilder::resolve_note(Raul::TimeStamp time, + size_t ev_size, + uint8_t* buf, + SPtr<Node> resolved) +{ + resolved->set_exit_action(SPtr<Action>(new MidiAction(ev_size, buf))); + + if (_active_nodes.size() == 1) { + if (_step) { + time = _time = _time + _step_duration; + } + + // Last active note + _connect_node_end_time = time; + + if (!_poly_nodes.empty()) { + // Finish a polyphonic section + _connect_node = SPtr<Node>(new Node(TimeStamp(_time.unit(), 0, 0))); + _machine->add_node(_connect_node); + + connect_nodes(_machine, resolved, time, _connect_node, time); + + for (PolyList::iterator j = _poly_nodes.begin(); + j != _poly_nodes.end(); ++j) { + _machine->add_node(j->second); + if (j->second->edges().empty()) { + connect_nodes(_machine, j->second, + j->first + j->second->duration(), + _connect_node, time); + } + } + _poly_nodes.clear(); + + _machine->add_node(resolved); + + } else { + // Just monophonic + if (is_delay_node(_connect_node) + && _connect_node->duration().is_zero() + && (_connect_node->edges().size() == 1) + && ((*_connect_node->edges().begin())->head() == resolved)) { + // Trim useless delay node if possible (after poly sections) + + _connect_node->edges().clear(); + _connect_node->set_enter_action(resolved->enter_action()); + _connect_node->set_exit_action(resolved->exit_action()); + resolved->set_enter_action(SPtr<Action>()); + resolved->set_exit_action(SPtr<Action>()); + set_node_duration(_connect_node, resolved->duration()); + resolved = _connect_node; + _machine->add_node(_connect_node); + + } else { + _connect_node = resolved; + _machine->add_node(resolved); + } + } + + } else { + // Polyphonic, add this node to poly list + _poly_nodes.push_back(make_pair(resolved->enter_time(), resolved)); + _connect_node = resolved; + _connect_node_end_time = _time; + } + + if (resolved->is_active()) { + resolved->exit(NULL, _time); + } +} + +void +MachineBuilder::event(Raul::TimeStamp time, + size_t ev_size, + uint8_t* buf) +{ + if (ev_size == 0) { + return; + } + + if (!_step) { + _time = time; + } + + if ((buf[0] & 0xF0) == LV2_MIDI_MSG_NOTE_ON) { + note_on(time, ev_size, buf); + } else if ((buf[0] & 0xF0) == LV2_MIDI_MSG_NOTE_OFF) { + for (ActiveList::iterator i = _active_nodes.begin(); + i != _active_nodes.end(); ++i) { + SPtr<MidiAction> action = dynamic_ptr_cast<MidiAction>( + (*i)->enter_action()); + if (!action) { + continue; + } + + const size_t ev_size = action->event_size(); + const uint8_t* ev = action->event(); + + if ((ev[0] & 0xF0) == LV2_MIDI_MSG_NOTE_ON && + (ev[0] & 0x0F) == (buf[0] & 0x0F) && + ev[1] == buf[1]) { + // Same channel and note as on event + resolve_note(time, ev_size, buf, *i); + _active_nodes.erase(i); + break; + } + } + } +} + +/** Finish the constructed machine and prepare it for use. + * Resolve any stuck notes, quantize, etc. + */ +void +MachineBuilder::resolve() +{ + // Resolve stuck notes + if (!_active_nodes.empty()) { + for (list<SPtr<Node> >::iterator i = _active_nodes.begin(); + i != _active_nodes.end(); ++i) { + cerr << "WARNING: Resolving stuck note." << endl; + SPtr<MidiAction> action = dynamic_ptr_cast<MidiAction>( + (*i)->enter_action()); + if (!action) { + continue; + } + + const size_t ev_size = action->event_size(); + const uint8_t* ev = action->event(); + if (ev_size == 3 && (ev[0] & 0xF0) == LV2_MIDI_MSG_NOTE_ON) { + uint8_t st((LV2_MIDI_MSG_NOTE_OFF & 0xF0) | (ev[0] & 0x0F)); + const uint8_t note_off[3] = { st, ev[1], 0x40 }; + (*i)->set_exit_action( + SPtr<Action>(new MidiAction(3, note_off))); + set_node_duration((*i), _time - (*i)->enter_time()); + (*i)->exit(NULL, _time); + _machine->add_node((*i)); + } + } + _active_nodes.clear(); + } + + // Add initial node if necessary + if (!_machine->nodes().empty()) { + _machine->add_node(_initial_node); + } +} + +SPtr<Machine> +MachineBuilder::finish() +{ + resolve(); + + return _machine; +} + +} // namespace machina diff --git a/src/engine/MachineBuilder.hpp b/src/engine/MachineBuilder.hpp new file mode 100644 index 0000000..83baf78 --- /dev/null +++ b/src/engine/MachineBuilder.hpp @@ -0,0 +1,87 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_MACHINEBUILDER_HPP +#define MACHINA_MACHINEBUILDER_HPP + +#include <stdint.h> + +#include <list> + +#include "machina/types.hpp" +#include "raul/TimeStamp.hpp" + +namespace machina { + +class Machine; +class Node; + +class MachineBuilder +{ +public: + MachineBuilder(SPtr<Machine> machine, + double quantization, + bool step); + + void event(Raul::TimeStamp time, size_t size, unsigned char* buf); + + void set_step(bool step) { _step = step; } + + void reset(); + void resolve(); + + SPtr<Machine> finish(); + +private: + bool is_delay_node(SPtr<Node> node) const; + void set_node_duration(SPtr<Node> node, Raul::TimeDuration d) const; + + void note_on(Raul::TimeStamp t, size_t ev_size, uint8_t* buf); + + void resolve_note(Raul::TimeStamp t, + size_t ev_size, + uint8_t* buf, + SPtr<Node> resolved); + + SPtr<Node>connect_nodes(SPtr<Machine> m, + SPtr<Node> tail, + Raul::TimeStamp tail_end_time, + SPtr<Node> head, + Raul::TimeStamp head_start_time); + + Raul::TimeStamp default_duration() { + return _step ? _step_duration : Raul::TimeStamp(_time.unit(), 0, 0); + } + + typedef std::list<SPtr<Node> > ActiveList; + ActiveList _active_nodes; + + typedef std::list<std::pair<Raul::TimeStamp, SPtr<Node> > > PolyList; + PolyList _poly_nodes; + + double _quantization; + Raul::TimeStamp _time; + SPtr<Machine> _machine; + SPtr<Node> _initial_node; + SPtr<Node> _connect_node; + Raul::TimeStamp _connect_node_end_time; + Raul::TimeStamp _step_duration; + bool _step; +}; + +} // namespace machina + +#endif // MACHINA_MACHINEBUILDER_HPP diff --git a/src/engine/MidiAction.cpp b/src/engine/MidiAction.cpp new file mode 100644 index 0000000..b5df155 --- /dev/null +++ b/src/engine/MidiAction.cpp @@ -0,0 +1,103 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "machina/Atom.hpp" +#include "machina/URIs.hpp" +#include "machina/types.hpp" + +#include "MIDISink.hpp" +#include "MidiAction.hpp" + +namespace machina { + +/** Create a MIDI action. + * + * Creating a NULL MidiAction is okay, pass event=NULL and the action will + * simply do nothing until a set_event (for MIDI learning). + */ +MidiAction::MidiAction(size_t size, + const byte* event) + : _size(0) + , _max_size(size) +{ + _event = new byte[_max_size]; + set_event(size, event); +} + +MidiAction::~MidiAction() +{ + delete[] _event.load(); +} + +bool +MidiAction::set_event(size_t size, const byte* new_event) +{ + byte* const event = _event.load(); + if (size <= _max_size) { + _event = NULL; + if (size > 0 && new_event) { + memcpy(event, new_event, size); + } + _size = size; + _event = event; + return true; + } else { + return false; + } +} + +void +MidiAction::execute(MIDISink* sink, Raul::TimeStamp time) +{ + const byte* const ev = _event.load(); + if (ev && sink) { + sink->write_event(time, _size, ev); + } +} + +void +MidiAction::write_state(Sord::Model& model) +{ + const uint8_t* ev = event(); + const uint8_t type = (ev[0] & 0xF0); + if (type == LV2_MIDI_MSG_NOTE_ON) { + model.add_statement( + rdf_id(model.world()), + Sord::URI(model.world(), MACHINA_URI_RDF "type"), + Sord::URI(model.world(), LV2_MIDI__NoteOn)); + } else if (type == LV2_MIDI_MSG_NOTE_OFF) { + model.add_statement( + rdf_id(model.world()), + Sord::URI(model.world(), MACHINA_URI_RDF "type"), + Sord::URI(model.world(), LV2_MIDI__NoteOff)); + } else { + std::cerr << "warning: Unable to serialise MIDI event" << std::endl; + } + + model.add_statement( + rdf_id(model.world()), + Sord::URI(model.world(), LV2_MIDI__noteNumber), + Sord::Literal::integer(model.world(), (int)ev[1])); + if (ev[2] != 64) { + model.add_statement( + rdf_id(model.world()), + Sord::URI(model.world(), LV2_MIDI__velocity), + Sord::Literal::integer(model.world(), (int)ev[2])); + } +} + +} // namespace machina diff --git a/src/engine/MidiAction.hpp b/src/engine/MidiAction.hpp new file mode 100644 index 0000000..64a2c65 --- /dev/null +++ b/src/engine/MidiAction.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_MIDIACTION_HPP +#define MACHINA_MIDIACTION_HPP + +#include <atomic> + +#include "raul/TimeSlice.hpp" + +#include "machina/types.hpp" + +#include "Action.hpp" + +namespace machina { + +class MIDISink; + +class MidiAction : public Action +{ +public: + ~MidiAction(); + + MidiAction(size_t size, + const unsigned char* event); + + size_t event_size() { return _size; } + byte* event() { return _event.load(); } + + bool set_event(size_t size, const byte* event); + + void execute(MIDISink* sink, Raul::TimeStamp time); + + virtual void write_state(Sord::Model& model); + +private: + size_t _size; + const size_t _max_size; + std::atomic<byte*> _event; +}; + +} // namespace machina + +#endif // MACHINA_MIDIACTION_HPP diff --git a/src/engine/Mutation.cpp b/src/engine/Mutation.cpp new file mode 100644 index 0000000..fed2ad4 --- /dev/null +++ b/src/engine/Mutation.cpp @@ -0,0 +1,190 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> +#include <cstdlib> + +#include "machina/Machine.hpp" +#include "machina/Mutation.hpp" + +#include "ActionFactory.hpp" +#include "Edge.hpp" +#include "MidiAction.hpp" + +using namespace std; + +namespace machina { +namespace Mutation { + +void +Compress::mutate(Random& rng, Machine& machine) +{ + //cout << "COMPRESS" << endl; + + // Trim disconnected nodes + for (Machine::Nodes::iterator i = machine.nodes().begin(); + i != machine.nodes().end(); ) { + Machine::Nodes::iterator next = i; + ++next; + + if ((*i)->edges().empty()) { + machine.remove_node(*i); + } + + i = next; + } +} + +void +AddNode::mutate(Random& rng, Machine& machine) +{ + //cout << "ADD NODE" << endl; + + // Create random node + SPtr<Node> node(new Node(Raul::TimeDuration{machine.time().unit()})); + node->set_selector(true); + + SPtr<Node> note_node = machine.random_node(); + if (!note_node) { + return; + } + + uint8_t note = rand() % 128; + + SPtr<MidiAction> enter_action = dynamic_ptr_cast<MidiAction>( + note_node->enter_action()); + if (enter_action) { + note = enter_action->event()[1]; + } + + node->set_enter_action(ActionFactory::note_on(note)); + node->set_exit_action(ActionFactory::note_off(note)); + machine.add_node(node); + + // Insert after some node + SPtr<Node> tail = machine.random_node(); + if (tail && (tail != node) /* && !node->connected_to(tail)*/) { + tail->add_edge(SPtr<Edge>(new Edge(tail, node))); + } + + // Insert before some other node + SPtr<Node> head = machine.random_node(); + if (head && (head != node) /* && !head->connected_to(node)*/) { + node->add_edge(SPtr<Edge>(new Edge(node, head))); + } +} + +void +RemoveNode::mutate(Random& rng, Machine& machine) +{ + //cout << "REMOVE NODE" << endl; + + SPtr<Node> node = machine.random_node(); + if (node && !node->is_initial()) { + machine.remove_node(node); + } +} + +void +AdjustNode::mutate(Random& rng, Machine& machine) +{ + //cout << "ADJUST NODE" << endl; + + SPtr<Node> node = machine.random_node(); + if (node) { + SPtr<MidiAction> enter_action = dynamic_ptr_cast<MidiAction>( + node->enter_action()); + SPtr<MidiAction> exit_action = dynamic_ptr_cast<MidiAction>( + node->exit_action()); + if (enter_action && exit_action) { + const uint8_t note = rand() % 128; + enter_action->event()[1] = note; + exit_action->event()[1] = note; + } + node->set_changed(); + } +} + +void +SwapNodes::mutate(Random& rng, Machine& machine) +{ + //cout << "SWAP NODE" << endl; + + if (machine.nodes().size() <= 1) { + return; + } + + SPtr<Node> a = machine.random_node(); + SPtr<Node> b = machine.random_node(); + while (b == a) { + b = machine.random_node(); + } + + SPtr<MidiAction> a_enter = dynamic_ptr_cast<MidiAction>(a->enter_action()); + SPtr<MidiAction> a_exit = dynamic_ptr_cast<MidiAction>(a->exit_action()); + SPtr<MidiAction> b_enter = dynamic_ptr_cast<MidiAction>(b->enter_action()); + SPtr<MidiAction> b_exit = dynamic_ptr_cast<MidiAction>(b->exit_action()); + + uint8_t note_a = a_enter->event()[1]; + uint8_t note_b = b_enter->event()[1]; + + a_enter->event()[1] = note_b; + a_exit->event()[1] = note_b; + b_enter->event()[1] = note_a; + b_exit->event()[1] = note_a; +} + +void +AddEdge::mutate(Random& rng, Machine& machine) +{ + //cout << "ADJUST EDGE" << endl; + + SPtr<Node> tail = machine.random_node(); + SPtr<Node> head = machine.random_node(); + + if (tail && head && tail != head) { + // && !tail->connected_to(head) && !head->connected_to(tail) + SPtr<Edge> edge(new Edge(tail, head)); + edge->set_probability(rand() / (float)RAND_MAX); + tail->add_edge(SPtr<Edge>(new Edge(tail, head))); + } +} + +void +RemoveEdge::mutate(Random& rng, Machine& machine) +{ + //cout << "REMOVE EDGE" << endl; + + SPtr<Node> tail = machine.random_node(); + if (tail && !(tail->is_initial() && tail->edges().size() == 1)) { + tail->remove_edge(tail->random_edge()); + } +} + +void +AdjustEdge::mutate(Random& rng, Machine& machine) +{ + //cout << "ADJUST EDGE" << endl; + + SPtr<Edge> edge = machine.random_edge(); + if (edge) { + edge->set_probability(rand() / (float)RAND_MAX); + edge->tail().lock()->edges_changed(); + } +} + +} // namespace Mutation +} // namespace machina diff --git a/src/engine/Node.cpp b/src/engine/Node.cpp new file mode 100644 index 0000000..7d8a870 --- /dev/null +++ b/src/engine/Node.cpp @@ -0,0 +1,271 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <iostream> + +#include "sord/sordmm.hpp" + +#include "machina/Atom.hpp" +#include "machina/URIs.hpp" + +#include "ActionFactory.hpp" +#include "Edge.hpp" +#include "Node.hpp" + +using namespace Raul; +using namespace std; + +namespace machina { + +Node::Node(TimeDuration duration, bool initial) + : _enter_time(duration.unit()) + , _duration(duration) + , _is_initial(initial) + , _is_selector(false) + , _is_active(false) +{} + +Node::Node(const Node& copy) + : Stateful() // don't copy RDF ID + , _enter_time(copy._enter_time) + , _duration(copy._duration) + , _enter_action(ActionFactory::copy(copy._enter_action)) + , _exit_action(ActionFactory::copy(copy._exit_action)) + , _is_initial(copy._is_initial) + , _is_selector(copy._is_selector) + , _is_active(false) +{ + for (Edges::const_iterator i = copy._edges.begin(); i != copy._edges.end(); + ++i) { + SPtr<Edge> edge(new Edge(*i->get())); + _edges.insert(edge); + } +} + +static inline bool +action_equals(SPtr<const Action> a, SPtr<const Action> b) +{ + return (a == b) || (a && b && *a.get() == *b.get()); +} + +bool +Node::operator==(const Node& rhs) const +{ + return _duration == rhs._duration && + _is_initial == rhs._is_initial && + _is_selector == rhs._is_selector && + _is_active == rhs._is_active && + action_equals(_enter_action, rhs.enter_action()) && + action_equals(_exit_action, rhs.exit_action()); + // TODO: compare edges +} + +/** Always returns an edge, unless there are none */ +SPtr<Edge> +Node::random_edge() +{ + SPtr<Edge> ret; + if (_edges.empty()) { + return ret; + } + + size_t i = rand() % _edges.size(); + + // FIXME: O(n) worst case :( + for (Edges::const_iterator e = _edges.begin(); e != _edges.end(); ++e, + --i) { + if (i == 0) { + ret = *e; + break; + } + } + + return ret; +} + +void +Node::edges_changed() +{ + if (!_is_selector) { + return; + } + + // Normalize edge probabilities if we're a selector + double prob_sum = 0; + + for (Edges::iterator i = _edges.begin(); i != _edges.end(); ++i) { + prob_sum += (*i)->probability(); + } + + for (Edges::iterator i = _edges.begin(); i != _edges.end(); ++i) { + (*i)->set_probability((*i)->probability() / prob_sum); + } + + _changed = true; +} + +void +Node::set_selector(bool yn) +{ + _is_selector = yn; + + if (yn) { + edges_changed(); + } + + _changed = true; +} + +void +Node::set_enter_action(SPtr<Action> action) +{ + _enter_action = action; + _changed = true; +} + +void +Node::set_exit_action(SPtr<Action> action) +{ + _exit_action = action; + _changed = true; +} + +void +Node::enter(MIDISink* sink, TimeStamp time) +{ + if (!_is_active) { + _changed = true; + _is_active = true; + _enter_time = time; + + if (sink && _enter_action) { + _enter_action->execute(sink, time); + } + } +} + +void +Node::exit(MIDISink* sink, TimeStamp time) +{ + if (_is_active) { + if (sink && _exit_action) { + _exit_action->execute(sink, time); + } + + _changed = true; + _is_active = false; + _enter_time = 0; + } +} + +SPtr<Edge> +Node::edge_to(SPtr<Node> head) const +{ + // TODO: Make logarithmic + for (Edges::const_iterator i = _edges.begin(); i != _edges.end(); ++i) { + if ((*i)->head() == head) { + return *i; + } + } + return SPtr<Edge>(); +} + +void +Node::add_edge(SPtr<Edge> edge) +{ + assert(edge->tail().lock().get() == this); + if (edge_to(edge->head())) { + return; + } + + _edges.insert(edge); + edges_changed(); +} + +void +Node::remove_edge(SPtr<Edge> edge) +{ + _edges.erase(edge); + edges_changed(); +} + +bool +Node::connected_to(SPtr<Node> node) +{ + return bool(edge_to(node)); +} + +SPtr<Edge> +Node::remove_edge_to(SPtr<Node> node) +{ + SPtr<Edge> edge = edge_to(node); + if (edge) { + _edges.erase(edge); // TODO: avoid double search + edges_changed(); + } + return edge; +} + +void +Node::set(URIInt key, const Atom& value) +{ + if (key == URIs::instance().machina_initial) { + std::cerr << "error: Attempt to change node initial state" << std::endl; + } else if (key == URIs::instance().machina_selector) { + set_selector(value.get<int32_t>()); + } +} + +void +Node::write_state(Sord::Model& model) +{ + using namespace Raul; + + const Sord::Node& rdf_id = this->rdf_id(model.world()); + + if (_is_selector) + model.add_statement( + rdf_id, + Sord::URI(model.world(), MACHINA_URI_RDF "type"), + Sord::URI(model.world(), MACHINA_NS_SelectorNode)); + else + model.add_statement( + rdf_id, + Sord::URI(model.world(), MACHINA_URI_RDF "type"), + Sord::URI(model.world(), MACHINA_NS_Node)); + + model.add_statement( + rdf_id, + Sord::URI(model.world(), MACHINA_NS_duration), + Sord::Literal::decimal(model.world(), _duration.to_double(), 7)); + + if (_enter_action) { + _enter_action->write_state(model); + model.add_statement(rdf_id, + Sord::URI(model.world(), MACHINA_NS_onEnter), + _enter_action->rdf_id(model.world())); + } + + if (_exit_action) { + _exit_action->write_state(model); + model.add_statement(rdf_id, + Sord::URI(model.world(), MACHINA_NS_onExit), + _exit_action->rdf_id(model.world())); + } +} + +} // namespace machina diff --git a/src/engine/Node.hpp b/src/engine/Node.hpp new file mode 100644 index 0000000..cae75f5 --- /dev/null +++ b/src/engine/Node.hpp @@ -0,0 +1,116 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_NODE_HPP +#define MACHINA_NODE_HPP + +#include <set> + +#include "machina/types.hpp" + +#include "Action.hpp" +#include "MIDISink.hpp" +#include "Schrodinbit.hpp" +#include "Stateful.hpp" + +namespace machina { + +class Edge; +using Raul::TimeDuration; +using Raul::TimeStamp; +using Raul::TimeUnit; + +/** A node is a state (as in a FSM diagram), or "note". + * + * It contains a action, as well as a duration and pointers to its + * successors (states/nodes that (may) follow it). + * + * Initial nodes do not have enter actions (since they are entered at + * an undefined point in time <= 0). + */ +class Node : public Stateful +{ +public: + Node(TimeDuration duration, bool initial=false); + Node(const Node& copy); + + bool operator==(const Node& rhs) const; + + void set_enter_action(SPtr<Action> action); + void set_exit_action(SPtr<Action> action); + + SPtr<const Action> enter_action() const { return _enter_action; } + SPtr<Action> enter_action() { return _enter_action; } + SPtr<const Action> exit_action() const { return _exit_action; } + SPtr<Action> exit_action() { return _exit_action; } + + void enter(MIDISink* sink, TimeStamp time); + void exit(MIDISink* sink, TimeStamp time); + + void edges_changed(); + + void add_edge(SPtr<Edge> edge); + void remove_edge(SPtr<Edge> edge); + SPtr<Edge> remove_edge_to(SPtr<Node> node); + bool connected_to(SPtr<Node> node); + + void set(URIInt key, const Atom& value); + void write_state(Sord::Model& model); + + bool is_initial() const { return _is_initial; } + bool is_active() const { return _is_active; } + TimeStamp enter_time() const { return _enter_time; } + TimeStamp exit_time() const { return _enter_time + _duration; } + TimeDuration duration() const { return _duration; } + void set_duration(TimeDuration d) { _duration = d; } + bool is_selector() const { return _is_selector; } + void set_selector(bool i); + + inline bool changed() { return _changed; } + inline void set_changed() { _changed = true; } + + struct EdgeHeadOrder { + inline bool operator()(const SPtr<const Edge>& a, + const SPtr<const Edge>& b) { + return a.get() < b.get(); + } + }; + + SPtr<Edge> edge_to(SPtr<Node> head) const; + + typedef std::set<SPtr<Edge>, EdgeHeadOrder> Edges; + + Edges& edges() { return _edges; } + + SPtr<Edge> random_edge(); + +private: + Node& operator=(const Node& other); // undefined + + TimeStamp _enter_time; ///< valid iff _is_active + TimeDuration _duration; + SPtr<Action> _enter_action; + SPtr<Action> _exit_action; + Edges _edges; + Schrodinbit _changed; + bool _is_initial; + bool _is_selector; + bool _is_active; +}; + +} // namespace machina + +#endif // MACHINA_NODE_HPP diff --git a/src/engine/Problem.cpp b/src/engine/Problem.cpp new file mode 100644 index 0000000..3b05fed --- /dev/null +++ b/src/engine/Problem.cpp @@ -0,0 +1,383 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#define __STDC_LIMIT_MACROS 1 + +#include <stdint.h> + +#include <set> +#include <vector> +#include <iostream> + +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "eugene/Problem.hpp" + +#include "machina/Machine.hpp" + +#include "ActionFactory.hpp" +#include "Edge.hpp" +#include "MidiAction.hpp" +#include "Problem.hpp" +#include "SMFReader.hpp" +#include "machina_config.h" + +using namespace std; + +namespace machina { + +Problem::Problem(TimeUnit unit, + const std::string& target_midi, + SPtr<Machine> seed) + : _unit(unit) + , _target(*this) + , _seed(new Machine(*seed.get())) +{ + SMFReader smf; + const bool opened = smf.open(target_midi); + assert(opened); + + smf.seek_to_track(2); // FIXME: ? + + uint8_t buf[4]; + uint32_t ev_size; + uint32_t delta_time; + while (smf.read_event(4, buf, &ev_size, &delta_time) >= 0) { + // time ignored + _target.write_event(TimeStamp(_unit, 0.0), ev_size, buf); +#if 0 + //_target._length += delta_time / (double)smf.ppqn(); + if ((buf[0] & 0xF0) == MIDI_CMD_NOTE_ON) { + const uint8_t note = buf[1]; + /*++_target._note_frequency[note]; + ++_target.n_notes();*/ + ++_target._counts[note]; + _target._notes.push_back(note); + } +#endif + } + + cout << "Target notes: " << _target.n_notes() << endl; + + _target.compute(); +} + +float +Problem::evaluate(const Machine& const_machine) const +{ + #if 0 + //cout << "("; + + // kluuudge + Machine& machine = const_cast<Machine&>(const_machine); + + map<Machine*, float>::const_iterator cached = _fitness.find(&machine); + if (cached != _fitness.end()) { + return cached->second; + } + + SPtr<Evaluator> eval(new Evaluator(*this)); + + //machine.reset(); + machine.set_sink(eval); + + // FIXME: timing stuff here isn't right at all... + + static const unsigned ppqn = MACHINA_PPQN; + Raul::TimeSlice time(ppqn, ppqn, 120.0); + time.set_slice(TimeStamp(_unit, 0, 0), TimeDuration(_unit, 2 * ppqn)); + + machine.run(time); + if (eval->n_notes() == 0) { + return 0.0f; // bad dog + } + TimeStamp end(_unit, time.start_ticks().ticks() + 2 * ppqn); + time.set_slice(end, TimeStamp(_unit, 0, 0)); + + while (eval->n_notes() < _target.n_notes()) { + machine.run(time); + if (machine.is_finished()) { + machine.reset(time.start_ticks()); + } + time.set_slice(end, TimeStamp(end.unit(), 0, 0)); + } + + eval->compute(); + + // count +#if 0 + float f = 0; + + for (uint8_t i = 0; i < 128; ++i) { + /*if (eval->_note_frequency[i] <= _target._note_frequency[i]) + f += eval->_note_frequency[i]; + else + f -= _target._note_frequency[i] - eval->_note_frequency[i];*/ + //f -= fabs(eval->_note_frequency[i] - _target._note_frequency[i]); + } +#endif + //cout << ")"; + + // distance + //float f = distance(eval->_notes, _target._notes); + + float f = 0.0; + + for (Evaluator::Patterns::const_iterator i = eval->_patterns.begin(); + i != eval->_patterns.end(); ++i) { + // Reward for matching patterns + if (_target._patterns.find(i->first) != _target._patterns.end()) { + Evaluator::Patterns::const_iterator c = _target._patterns.find( + i->first); + const uint32_t cnt + = (c == _target._patterns.end()) ? 1 : c->second; + f += min(i->second, cnt) * (i->first.length()); + + } else { + // Punish for bad patterns + const uint32_t invlen = (eval->_order - i->first.length() + 1); + f -= (i->second / (float)eval->_patterns.size() + * (float)(invlen * invlen * invlen)) * 4; + } + } + + // Punish for missing patterns + for (Evaluator::Patterns::const_iterator i = _target._patterns.begin(); + i != _target._patterns.end(); ++i) { + if (eval->_patterns.find(i->first) == _target._patterns.end()) { + f -= i->second / (float)_target.n_notes() + * (float)(eval->_order - i->first.length() + 1); + } + } + + //cout << "f = " << f << endl; + + _fitness[&machine] = f; + + return f; + #endif + return 0.0f; +} + +void +Problem::Evaluator::write_event(Raul::TimeStamp time, + size_t ev_size, + const uint8_t* ev) throw (std::logic_error) +{ + if ((ev[0] & 0xF0) == LV2_MIDI_MSG_NOTE_ON) { + + const uint8_t note = ev[1]; + + if (_first_note == 0) { + _first_note = note; + } + + /*++_note_frequency[note]; + ++n_notes();*/ + //_notes.push_back(note); + if (_read.length() == 0) { + _read = note; + return; + } + if (_read.length() == _order) { + _read = _read.substr(1); + } + + _read = _read + (char)note; + + for (size_t i = 0; i < _read.length(); ++i) { + const string pattern = _read.substr(i); + Patterns::iterator p = _patterns.find(pattern); + if (p != _patterns.end()) { + ++(p->second); + } else { + _patterns[pattern] = 1; + } + } + + ++_counts[note]; + ++_n_notes; + + } +} + +void +Problem::Evaluator::compute() +{ + /* + for (uint8_t i=0; i < 128; ++i) { + if (_note_frequency[i] > 0) { + _note_frequency[i] /= (float)n_notes(); + //cout << (int)i << ":\t" << _note_frequency[i] << endl; + } + }*/ +} + +void +Problem::initial_population(eugene::Random& rng, + Population& pop, + size_t gene_size, + size_t pop_size) const +{ + // FIXME: ignores _seed and builds based on MIDI + // evolution of the visible machine would be nice.. + SPtr<Machine> base = SPtr<Machine>(new Machine(_unit)); + for (uint8_t i = 0; i < 128; ++i) { + if (_target._counts[i] > 0) { + //cout << "Initial note: " << (int)i << endl; + SPtr<Node> node(new Node(TimeDuration(_unit, 1 / 2.0))); + node->set_enter_action(ActionFactory::note_on(i)); + node->set_exit_action(ActionFactory::note_off(i)); + node->set_selector(true); + base->add_node(node); + } + } + + for (size_t i = 0; i < pop_size; ++i) { + // FIXME: double copy + Machine m(*base.get()); + + set< SPtr<Node> > unreachable; + + for (Machine::Nodes::iterator i = m.nodes().begin(); i != m.nodes().end(); + ++i) { + if (dynamic_ptr_cast<MidiAction>((*i)->enter_action())->event()[1] == + _target.first_note()) { + m.initial_node()->add_edge( + SPtr<Edge>(new Edge(m.initial_node(), *i))); + } else { + unreachable.insert(*i); + } + } + + SPtr<Node> cur = m.initial_node(); + unreachable.erase(cur); + SPtr<Node> head; + + while (!unreachable.empty()) { + if (rand() % 2) { + head = m.random_node(); + } else { + head = *unreachable.begin(); + } + + if (!head->connected_to(head) ) { + cur->add_edge(SPtr<Edge>(new Edge(cur, head))); + unreachable.erase(head); + cur = head; + } + } + + pop.push_back(m); + + /*cout << "initial # nodes: " << m.nodes().size(); + cout << "initial fitness: " << fitness(m) << endl;*/ + } +} + +/** levenshtein distance (edit distance) */ +size_t +Problem::distance(const std::vector<uint8_t>& source, + const std::vector<uint8_t>& target) const +{ + // Derived from http://www.merriampark.com/ldcpp.htm + + assert(source.size() < UINT16_MAX); + assert(target.size() < UINT16_MAX); + + // Step 1 + + const uint16_t n = source.size(); + const uint16_t m = target.size(); + if (n == 0) { + return m; + } + if (m == 0) { + return n; + } + + _matrix.resize(n + 1); + for (uint16_t i = 0; i <= n; i++) { + _matrix[i].resize(m + 1); + } + + // Step 2 + + for (uint16_t i = 0; i <= n; i++) { + _matrix[i][0] = i; + } + + for (uint16_t j = 0; j <= m; j++) { + _matrix[0][j] = j; + } + + // Step 3 + + for (uint16_t i = 1; i <= n; i++) { + + const uint8_t s_i = source[i - 1]; + + // Step 4 + + for (uint16_t j = 1; j <= m; j++) { + + const uint8_t t_j = target[j - 1]; + + // Step 5 + + uint16_t cost; + if (s_i == t_j) { + cost = 0; + } else { + cost = 1; + } + + // Step 6 + + const uint16_t above = _matrix[i - 1][j]; + const uint16_t left = _matrix[i][j - 1]; + const uint16_t diag = _matrix[i - 1][j - 1]; + uint16_t cell = min(above + 1, min(left + 1, diag + cost)); + + // Step 6A: Cover transposition, in addition to deletion, + // insertion and substitution. This step is taken from: + // Berghel, Hal ; Roach, David : "An Extension of Ukkonen's + // Enhanced Dynamic Programming ASM Algorithm" + // (http://www.acm.org/~hlb/publications/asm/asm.html) + + if (i > 2 && j > 2) { + uint16_t trans = _matrix[i - 2][j - 2] + 1; + if (source[i - 2] != t_j) { + trans++; + } + if (s_i != target[j - 2]) { + trans++; + } + if (cell > trans) { + cell = trans; + } + } + + _matrix[i][j] = cell; + } + } + + // Step 7 + + return _matrix[n][m]; +} + +} // namespace machina diff --git a/src/engine/Problem.hpp b/src/engine/Problem.hpp new file mode 100644 index 0000000..81e67d7 --- /dev/null +++ b/src/engine/Problem.hpp @@ -0,0 +1,135 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_PROBLEM_HPP +#define MACHINA_PROBLEM_HPP + +#include <stdint.h> + +#include <map> +#include <stdexcept> + +#include "machina/Machine.hpp" +#include "eugene/Problem.hpp" + +#include "MIDISink.hpp" + +namespace machina { + +class Problem : public eugene::Problem<Machine> { +public: + Problem(TimeUnit unit, + const std::string& target_midi, + SPtr<Machine> seed = SPtr<Machine>()); + virtual ~Problem() {} + + void seed(SPtr<Machine> parent) { _seed = parent; } + + float evaluate(const Machine& machine) const; + + bool fitness_less_than(float a, float b) const { return a < b; } + + void clear_fitness_cache() { _fitness.clear(); } + + void initial_population(eugene::Random& rng, + Population& pop, + size_t gene_size, + size_t pop_size) const; + +private: + size_t distance(const std::vector<uint8_t>& source, + const std::vector<uint8_t>& target) const; + + // count + /*struct FreqEvaluator : public Raul::MIDISink { + Evaluator(const Problem& problem) + : _problem(problem), _n_notes(0), _length(0) { + for (uint8_t i=0; i < 128; ++i) + _note_frequency[i] = 0; + } + void write_event(Raul::BeatTime time, + size_t ev_size, + const uint8_t* ev) throw (std::logic_error); + void compute(); + const Problem& _problem; + + size_t n_notes() const { return _n_notes; } + + float _note_frequency[128]; + size_t _n_notes; + double _length; + };*/ + + // distance + /*struct Evaluator : public Raul::MIDISink { + Evaluator(const Problem& problem) : _problem(problem) {} + void write_event(Raul::BeatTime time, + size_t ev_size, + const uint8_t* ev) throw (std::logic_error); + void compute(); + const Problem& _problem; + + size_t n_notes() const { return _notes.size(); } + + std::vector<uint8_t> _notes; + float _counts[128]; + };*/ + + struct Evaluator : public MIDISink { + explicit Evaluator(const Problem& problem) + : _problem(problem) + , _order(4) + , _n_notes(0) + , _first_note(0) + { + for (uint8_t i=0; i < 128; ++i) + _counts[i] = 0; + } + void write_event(TimeStamp time, + size_t ev_size, + const uint8_t* ev) throw (std::logic_error); + void compute(); + const Problem& _problem; + + size_t n_notes() const { return _n_notes; } + uint8_t first_note() const { return _first_note; } + + const uint32_t _order; + + std::string _read; + + typedef std::map<std::string, uint32_t> Patterns; + Patterns _patterns; + uint32_t _counts[128]; + size_t _n_notes; + uint8_t _first_note; + }; + + TimeUnit _unit; + + Evaluator _target; + SPtr<Machine> _seed; + + /// for levenshtein distance + mutable std::vector< std::vector<uint16_t> > _matrix; + + /// memoization + mutable std::map<Machine*, float> _fitness; +}; + +} // namespace machina + +#endif // MACHINA_PROBLEM_HPP diff --git a/src/engine/Recorder.cpp b/src/engine/Recorder.cpp new file mode 100644 index 0000000..21dfd4b --- /dev/null +++ b/src/engine/Recorder.cpp @@ -0,0 +1,69 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <ios> +#include <iostream> + +#include "MachineBuilder.hpp" +#include "Recorder.hpp" + +using namespace std; +using namespace Raul; + +namespace machina { + +Recorder::Recorder(Forge& forge, + size_t buffer_size, + TimeUnit unit, + double q, + bool step) + : _unit(unit) + , _record_buffer(buffer_size) + , _builder(new MachineBuilder(SPtr<Machine>(new Machine(unit)), q, step)) +{} + +void +Recorder::_whipped() +{ + TimeStamp t(_unit); + size_t size; + unsigned char buf[4]; + + while (true) { + bool success = _record_buffer.read(sizeof(TimeStamp), (uint8_t*)&t); + if (success) { + success = _record_buffer.read(sizeof(size_t), (uint8_t*)&size); + } + if (success) { + success = _record_buffer.read(size, buf); + } + if (success) { + _builder->event(t, size, buf); + } else { + break; + } + } +} + +SPtr<Machine> +Recorder::finish() +{ + SPtr<Machine> machine = _builder->finish(); + _builder.reset(); + return machine; +} + +} diff --git a/src/engine/Recorder.hpp b/src/engine/Recorder.hpp new file mode 100644 index 0000000..c98c42b --- /dev/null +++ b/src/engine/Recorder.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_RECORDER_HPP +#define MACHINA_RECORDER_HPP + +#include "raul/RingBuffer.hpp" + +#include "machina/Machine.hpp" +#include "machina/types.hpp" + +#include "Slave.hpp" + +namespace machina { + +class MachineBuilder; + +class Recorder + : public Slave +{ +public: + Recorder(Forge& forge, + size_t buffer_size, + TimeUnit unit, + double q, + bool step); + + inline void write(Raul::TimeStamp time, size_t size, + const unsigned char* buf) { + if (_record_buffer.write_space() < + (sizeof(TimeStamp) + sizeof(size_t) + size)) { + std::cerr << "Record buffer overflow" << std::endl; + return; + } else { + _record_buffer.write(sizeof(TimeStamp), (uint8_t*)&time); + _record_buffer.write(sizeof(size_t), (uint8_t*)&size); + _record_buffer.write(size, buf); + } + } + + SPtr<MachineBuilder> builder() { return _builder; } + + SPtr<Machine> finish(); + +private: + virtual void _whipped(); + + TimeUnit _unit; + Raul::RingBuffer _record_buffer; + SPtr<MachineBuilder> _builder; +}; + +} // namespace machina + +#endif // MACHINA_RECORDER_HPP diff --git a/src/engine/SMFDriver.cpp b/src/engine/SMFDriver.cpp new file mode 100644 index 0000000..3dc51e2 --- /dev/null +++ b/src/engine/SMFDriver.cpp @@ -0,0 +1,162 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> +#include <list> + +#include "machina/Context.hpp" +#include "machina/Machine.hpp" +#include "machina/types.hpp" + +#include "Edge.hpp" +#include "SMFDriver.hpp" +#include "SMFReader.hpp" +#include "SMFWriter.hpp" +#include "quantize.hpp" + +using namespace std; + +namespace machina { + +SMFDriver::SMFDriver(Forge& forge, Raul::TimeUnit unit) + : Driver(forge, SPtr<Machine>()) +{ + _writer = SPtr<SMFWriter>(new SMFWriter(unit)); +} + +/** Learn a single track from the MIDI file at `uri` + * + * @param track The track of the MIDI file to import, starting from 1. + * + * Currently only file:// URIs are supported. + * @return the resulting machine. + */ +SPtr<Machine> +SMFDriver::learn(const string& filename, + unsigned track, + double q, + Raul::TimeDuration max_duration) +{ + //assert(q.unit() == max_duration.unit()); + SPtr<Machine> m(new Machine(max_duration.unit())); + SPtr<MachineBuilder> builder = SPtr<MachineBuilder>( + new MachineBuilder(m, q, false)); + SMFReader reader; + + if (!reader.open(filename)) { + cerr << "Unable to open MIDI file " << filename << endl; + return SPtr<Machine>(); + } + + if (track > reader.num_tracks()) { + return SPtr<Machine>(); + } else { + learn_track(builder, reader, track, q, max_duration); + } + + m->reset(NULL, m->time()); + + if (m->nodes().size() > 1) { + return m; + } else { + return SPtr<Machine>(); + } +} + +/** Learn all tracks from a MIDI file into a single machine. + * + * This will result in one disjoint subgraph in the machine for each track. + */ +SPtr<Machine> +SMFDriver::learn(const string& filename, double q, Raul::TimeStamp max_duration) +{ + SPtr<Machine> m(new Machine(max_duration.unit())); + SPtr<MachineBuilder> builder = SPtr<MachineBuilder>( + new MachineBuilder(m, q, false)); + SMFReader reader; + + if (!reader.open(filename)) { + cerr << "Unable to open MIDI file " << filename << endl; + return SPtr<Machine>(); + } + + for (unsigned t = 1; t <= reader.num_tracks(); ++t) { + builder->reset(); + learn_track(builder, reader, t, q, max_duration); + } + + m->reset(NULL, m->time()); + + if (m->nodes().size() > 1) { + return m; + } else { + return SPtr<Machine>(); + } +} + +void +SMFDriver::learn_track(SPtr<MachineBuilder> builder, + SMFReader& reader, + unsigned track, + double q, + Raul::TimeDuration max_duration) +{ + const bool found_track = reader.seek_to_track(track); + if (!found_track) { + return; + } + + uint8_t buf[4]; + uint32_t ev_size; + uint32_t ev_delta_time; + + Raul::TimeUnit unit = Raul::TimeUnit(TimeUnit::BEATS, MACHINA_PPQN); + + uint64_t t = 0; + while (reader.read_event(4, buf, &ev_size, &ev_delta_time) >= 0) { + t += ev_delta_time; + + const uint32_t beats = t / (uint32_t)reader.ppqn(); + const uint32_t smf_ticks = t % (uint32_t)reader.ppqn(); + const double frac = smf_ticks / (double)reader.ppqn(); + const uint32_t ticks = frac * MACHINA_PPQN; + + if (!max_duration.is_zero() && t > max_duration.to_double()) { + break; + } + + if (ev_size > 0) { + // TODO: quantize + builder->event(TimeStamp(unit, beats, ticks), ev_size, buf); + } + } + + builder->resolve(); +} + +void +SMFDriver::run(SPtr<Machine> machine, Raul::TimeStamp max_time) +{ + // FIXME: unit kludge (tempo only) + Context context(_forge, machine->time().unit().ppt(), + _writer->unit().ppt(), 120.0); + context.set_sink(this); + context.time().set_slice(TimeStamp(max_time.unit(), 0, 0), + context.time().beats_to_ticks(max_time)); + machine->run(context, SPtr<Raul::RingBuffer>()); +} + +} // namespace machina diff --git a/src/engine/SMFDriver.hpp b/src/engine/SMFDriver.hpp new file mode 100644 index 0000000..e8b9a4b --- /dev/null +++ b/src/engine/SMFDriver.hpp @@ -0,0 +1,67 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_SMFDRIVER_HPP +#define MACHINA_SMFDRIVER_HPP + +#include "machina/Driver.hpp" +#include "machina/types.hpp" + +#include "MachineBuilder.hpp" +#include "SMFReader.hpp" +#include "SMFWriter.hpp" + +namespace machina { + +class Node; +class Machine; + +class SMFDriver : public Driver +{ +public: + SMFDriver(Forge& forge, Raul::TimeUnit unit); + + SPtr<Machine> learn(const std::string& filename, + double q, + Raul::TimeDuration max_duration); + + SPtr<Machine> learn(const std::string& filename, + unsigned track, + double q, + Raul::TimeDuration max_duration); + + void run(SPtr<Machine> machine, Raul::TimeStamp max_time); + + void write_event(Raul::TimeStamp time, + size_t ev_size, + const unsigned char* ev) + { _writer->write_event(time, ev_size, ev); } + + SPtr<SMFWriter> writer() { return _writer; } + +private: + SPtr<SMFWriter> _writer; + + void learn_track(SPtr<MachineBuilder> builder, + SMFReader& reader, + unsigned track, + double q, + Raul::TimeDuration max_duration); +}; + +} // namespace machina + +#endif // MACHINA_SMFDRIVER_HPP diff --git a/src/engine/SMFReader.cpp b/src/engine/SMFReader.cpp new file mode 100644 index 0000000..5976f75 --- /dev/null +++ b/src/engine/SMFReader.cpp @@ -0,0 +1,323 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Raul 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 3 of the License, or any later version. + + Raul 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 Raul. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <arpa/inet.h> +#include <cassert> +#include <cstdio> +#include <cstring> +#include <string> + +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "SMFReader.hpp" + +using std::endl; + +namespace machina { + +/** Return the size of the given event NOT including the status byte, + * or -1 if unknown (eg sysex) + */ +static int +midi_event_size(unsigned char status) +{ + if (status >= 0x80 && status <= 0xE0) { + status &= 0xF0; // mask off the channel + } + + switch (status) { + case LV2_MIDI_MSG_NOTE_OFF: + case LV2_MIDI_MSG_NOTE_ON: + case LV2_MIDI_MSG_NOTE_PRESSURE: + case LV2_MIDI_MSG_CONTROLLER: + case LV2_MIDI_MSG_BENDER: + case LV2_MIDI_MSG_SONG_POS: + return 2; + + case LV2_MIDI_MSG_PGM_CHANGE: + case LV2_MIDI_MSG_CHANNEL_PRESSURE: + case LV2_MIDI_MSG_MTC_QUARTER: + case LV2_MIDI_MSG_SONG_SELECT: + return 1; + + case LV2_MIDI_MSG_TUNE_REQUEST: + case LV2_MIDI_MSG_CLOCK: + case LV2_MIDI_MSG_START: + case LV2_MIDI_MSG_CONTINUE: + case LV2_MIDI_MSG_STOP: + case LV2_MIDI_MSG_ACTIVE_SENSE: + case LV2_MIDI_MSG_RESET: + return 0; + + case LV2_MIDI_MSG_SYSTEM_EXCLUSIVE: + return -1; + } + + return -1; +} + +SMFReader::SMFReader(const std::string filename) + : _fd(NULL) + , _ppqn(0) + , _track(0) + , _track_size(0) +{ + if (filename.length() > 0) { + open(filename); + } +} + +SMFReader::~SMFReader() +{ + if (_fd) { + close(); + } +} + +bool +SMFReader::open(const std::string& filename) +{ + if (_fd) { + throw std::logic_error( + "Attempt to start new read while write in progress."); + } + + std::cout << "Opening SMF file " << filename << " for reading." << endl; + + _fd = fopen(filename.c_str(), "r+"); + + if (_fd) { + // Read type (bytes 8..9) + fseek(_fd, 0, SEEK_SET); + char mthd[5]; + mthd[4] = '\0'; + fread(mthd, 1, 4, _fd); + if (strcmp(mthd, "MThd")) { + std::cerr << filename << " is not an SMF file, aborting." << endl; + fclose(_fd); + _fd = NULL; + return false; + } + + // Read type (bytes 8..9) + fseek(_fd, 8, SEEK_SET); + uint16_t type_be = 0; + fread(&type_be, 2, 1, _fd); + _type = ntohs(type_be); + + // Read number of tracks (bytes 10..11) + uint16_t num_tracks_be = 0; + fread(&num_tracks_be, 2, 1, _fd); + _num_tracks = ntohs(num_tracks_be); + + // Read PPQN (bytes 12..13) + uint16_t ppqn_be = 0; + fread(&ppqn_be, 2, 1, _fd); + _ppqn = ntohs(ppqn_be); + + // TODO: Absolute (SMPTE seconds) time support + if ((_ppqn & 0x8000) != 0) { + throw UnsupportedTime(); + } + + seek_to_track(1); + + return true; + } else { + return false; + } +} + +/** Seek to the start of a given track, starting from 1. + * Returns true if specified track was found. + */ +bool +SMFReader::seek_to_track(unsigned track) +{ + if (track == 0) { + throw std::logic_error("Seek to track 0 out of range (must be >= 1)"); + } + + if (!_fd) { + throw std::logic_error("Attempt to seek to track on unopened SMF file."); + } + + unsigned track_pos = 0; + + fseek(_fd, 14, SEEK_SET); + char id[5]; + id[4] = '\0'; + uint32_t chunk_size = 0; + + while (!feof(_fd)) { + fread(id, 1, 4, _fd); + + if (!strcmp(id, "MTrk")) { + ++track_pos; + } else { + std::cerr << "Unknown chunk ID " << id << endl; + } + + uint32_t chunk_size_be; + fread(&chunk_size_be, 4, 1, _fd); + chunk_size = ntohl(chunk_size_be); + + if (track_pos == track) { + break; + } + + fseek(_fd, chunk_size, SEEK_CUR); + } + + if (!feof(_fd) && track_pos == track) { + _track = track; + _track_size = chunk_size; + return true; + } else { + return false; + } +} + +/** Read an event from the current position in file. + * + * File position MUST be at the beginning of a delta time, or this will die very messily. + * ev.buffer must be of size ev.size, and large enough for the event. The returned event + * will have it's time field set to it's delta time (so it's the caller's responsibility + * to keep track of delta time, even for ignored events). + * + * Returns event length (including status byte) on success, 0 if event was + * skipped (eg a meta event), or -1 on EOF (or end of track). + * + * If `buf` is not large enough to hold the event, 0 will be returned, but ev_size + * set to the actual size of the event. + */ +int +SMFReader::read_event(size_t buf_len, + uint8_t* buf, + uint32_t* ev_size, + uint32_t* delta_time) +{ + if (_track == 0) { + throw std::logic_error("Attempt to read from unopened SMF file"); + } + + if (!_fd || feof(_fd)) { + return -1; + } + + assert(buf_len > 0); + assert(buf); + assert(ev_size); + assert(delta_time); + + // Running status state + static uint8_t last_status = 0; + static uint32_t last_size = 0; + + *delta_time = read_var_len(_fd); + int status = fgetc(_fd); + if (status == EOF) { + throw PrematureEOF(); + } else if (status > 0xFF) { + throw CorruptFile(); + } + + if (status < 0x80) { + if (last_status == 0) { + throw CorruptFile(); + } + status = last_status; + *ev_size = last_size; + fseek(_fd, -1, SEEK_CUR); + } else { + last_status = status; + *ev_size = midi_event_size(status) + 1; + last_size = *ev_size; + } + + buf[0] = static_cast<uint8_t>(status); + + if (status == 0xFF) { + *ev_size = 0; + if (feof(_fd)) { + throw PrematureEOF(); + } + uint8_t type = fgetc(_fd); + const uint32_t size = read_var_len(_fd); + + if (type == 0x2F) { + return -1; // we hit the logical EOF anyway... + } else { + fseek(_fd, size, SEEK_CUR); + return 0; + } + } + + if ((*ev_size > buf_len) || (*ev_size == 0) || feof(_fd)) { + // Skip event, return 0 + fseek(_fd, *ev_size - 1, SEEK_CUR); + return 0; + } else { + // Read event, return size + if (ferror(_fd)) { + throw CorruptFile(); + } + + fread(buf + 1, 1, *ev_size - 1, _fd); + + if (((buf[0] & 0xF0) == 0x90) && (buf[2] == 0) ) { + buf[0] = (0x80 | (buf[0] & 0x0F)); + buf[2] = 0x40; + } + + return *ev_size; + } +} + +void +SMFReader::close() +{ + if (_fd) { + fclose(_fd); + } + + _fd = NULL; +} + +uint32_t +SMFReader::read_var_len(FILE* fd) +{ + if (feof(fd)) { + throw PrematureEOF(); + } + + uint32_t value; + uint8_t c; + + if ((value = getc(fd)) & 0x80) { + value &= 0x7F; + do { + if (feof(fd)) { + throw PrematureEOF(); + } + value = (value << 7) + ((c = getc(fd)) & 0x7F); + } while (c & 0x80); + } + + return value; +} + +} // namespace machina diff --git a/src/engine/SMFReader.hpp b/src/engine/SMFReader.hpp new file mode 100644 index 0000000..7147f38 --- /dev/null +++ b/src/engine/SMFReader.hpp @@ -0,0 +1,87 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_SMF_READER_HPP +#define MACHINA_SMF_READER_HPP + +#include <stdexcept> +#include <string> +#include <inttypes.h> +#include "raul/TimeStamp.hpp" + +namespace machina { + +/** Standard Midi File (Type 0) Reader + * + * Currently this only reads SMF files with tempo-based timing. + * \ingroup raul + */ +class SMFReader +{ +public: + class PrematureEOF + : public std::exception + { + const char* what() const throw() { return "Unexpected end of file"; } + }; + class CorruptFile + : public std::exception + { + const char* what() const throw() { return "Corrupted file"; } + }; + class UnsupportedTime + : public std::exception + { + const char* what() const throw() { return + "Unsupported time stamp type (SMPTE)"; } + }; + + explicit SMFReader(const std::string filename = ""); + ~SMFReader(); + + bool open(const std::string& filename); + + bool seek_to_track(unsigned track); + + uint16_t type() const { return _type; } + uint16_t ppqn() const { return _ppqn; } + size_t num_tracks() { return _num_tracks; } + + int read_event(size_t buf_len, + uint8_t* buf, + uint32_t* ev_size, + uint32_t* ev_delta_time); + + void close(); + + static uint32_t read_var_len(FILE* fd); + +protected: + /** size of SMF header, including MTrk chunk header */ + static const uint32_t HEADER_SIZE = 22; + + std::string _filename; + FILE* _fd; + uint16_t _type; + uint16_t _ppqn; + uint16_t _num_tracks; + uint32_t _track; + uint32_t _track_size; +}; + +} // namespace machina + +#endif // MACHINA_SMF_READER_HPP diff --git a/src/engine/SMFWriter.cpp b/src/engine/SMFWriter.cpp new file mode 100644 index 0000000..25d25c9 --- /dev/null +++ b/src/engine/SMFWriter.cpp @@ -0,0 +1,241 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <arpa/inet.h> +#include <stdint.h> +#include <stdio.h> + +#include <cassert> +#include <cstdio> +#include <cstring> +#include <limits> +#include <string> + +#include "SMFWriter.hpp" + +using std::endl; + +namespace machina { + +/** Create a new SMF writer. + * + * @param unit Must match the time stamp of ALL events passed to write, or + * terrible things will happen. + * + * *** NOTE: Only beat time is implemented currently. + */ +SMFWriter::SMFWriter(Raul::TimeUnit unit) + : _fd(NULL) + , _unit(unit) + , _start_time(unit, 0, 0) + , _last_ev_time(unit, 0, 0) + , _track_size(0) + , _header_size(0) +{ + if (unit.type() == Raul::TimeUnit::BEATS) { + assert(unit.ppt() < std::numeric_limits<uint16_t>::max()); + } +} + +SMFWriter::~SMFWriter() +{ + if (_fd) { + finish(); + } +} + +/** Start a write to an SMF file. + * + * @param filename Filename to write to. + * @param start_time Beat time corresponding to t=0 in the file (timestamps + * passed to write_event will have this value subtracted before writing). + */ +bool +SMFWriter::start(const std::string& filename, + Raul::TimeStamp start_time) +{ + if (_fd) { + throw std::logic_error( + "Attempt to start new write while write in progress."); + } + + std::cout << "Opening SMF file " << filename << " for writing." << endl; + + _fd = fopen(filename.c_str(), "w+"); + + if (_fd) { + _track_size = 0; + _filename = filename; + _start_time = start_time; + _last_ev_time = 0; + // write a tentative header to pad file out so writing starts at the right offset + write_header(); + } + + return (_fd == 0) ? false : true; +} + +/** Write an event at the end of the file. + * + * @param time The absolute time of the event, relative to the start of the + * file (the start_time parameter to start). Must be monotonically increasing + * on successive calls to this method. + */ +void +SMFWriter::write_event(Raul::TimeStamp time, + size_t ev_size, + const unsigned char* ev) +{ + if (time < _start_time) { + throw std::logic_error("Event time is before file start time"); + } else if (time < _last_ev_time) { + throw std::logic_error("Event time not monotonically increasing"); + } else if (time.unit() != _unit) { + throw std::logic_error("Event has unexpected time unit"); + } + + Raul::TimeStamp delta_time = time; + delta_time -= _start_time; + + fseek(_fd, 0, SEEK_END); + + uint64_t delta_ticks = delta_time.ticks() * _unit.ppt() + + delta_time.subticks(); + size_t stamp_size = 0; + + /* If delta time is too long (i.e. overflows), write out empty + * "proprietary" events to reach the desired time. + * Any SMF reading application should interpret this correctly + * (by accumulating the delta time and ignoring the event) */ + while (delta_ticks > VAR_LEN_MAX) { + static unsigned char null_event[] = { 0xFF, 0x7F, 0x0 }; + stamp_size = write_var_len(VAR_LEN_MAX); + fwrite(null_event, 1, 3, _fd); + _track_size += stamp_size + 3; + delta_ticks -= VAR_LEN_MAX; + } + + assert(delta_ticks <= VAR_LEN_MAX); + stamp_size = write_var_len(static_cast<uint32_t>(delta_ticks)); + fwrite(ev, 1, ev_size, _fd); + + _last_ev_time = time; + _track_size += stamp_size + ev_size; +} + +void +SMFWriter::flush() +{ + if (_fd) { + fflush(_fd); + } +} + +void +SMFWriter::finish() +{ + if (!_fd) { + throw std::logic_error( + "Attempt to finish write with no write in progress."); + } + + write_footer(); + fclose(_fd); + _fd = NULL; +} + +void +SMFWriter::write_header() +{ + std::cout << "SMF Flushing header\n"; + + const uint16_t type = htons(0); // SMF Type 0 (single track) + const uint16_t ntracks = htons(1); // Number of tracks (always 1 for Type 0) + const uint16_t division = htons(_unit.ppt()); // PPQN + + char data[6]; + memcpy(data, &type, 2); + memcpy(data + 2, &ntracks, 2); + memcpy(data + 4, &division, 2); + //data[4] = _ppqn & 0xF0; + //data[5] = _ppqn & 0x0F; + + _fd = freopen(_filename.c_str(), "r+", _fd); + assert(_fd); + fseek(_fd, 0, 0); + write_chunk("MThd", 6, data); + write_chunk_header("MTrk", _track_size); +} + +void +SMFWriter::write_footer() +{ + std::cout << "Writing EOT\n"; + + fseek(_fd, 0, SEEK_END); + write_var_len(1); // whatever... + static const unsigned char eot[4] = { 0xFF, 0x2F, 0x00 }; // end-of-track meta-event + fwrite(eot, 1, 4, _fd); +} + +void +SMFWriter::write_chunk_header(const char id[4], uint32_t length) +{ + const uint32_t length_be = htonl(length); + + fwrite(id, 1, 4, _fd); + fwrite(&length_be, 4, 1, _fd); +} + +void +SMFWriter::write_chunk(const char id[4], uint32_t length, void* data) +{ + write_chunk_header(id, length); + + fwrite(data, 1, length, _fd); +} + +/** Write an SMF variable length value. + * + * @return size (in bytes) of the value written. + */ +size_t +SMFWriter::write_var_len(uint32_t value) +{ + size_t ret = 0; + + uint32_t buffer = value & 0x7F; + + while ((value >>= 7)) { + buffer <<= 8; + buffer |= ((value & 0x7F) | 0x80); + } + + while (true) { + //printf("Writing var len byte %X\n", (unsigned char)buffer); + ++ret; + fputc(buffer, _fd); + if (buffer & 0x80) { + buffer >>= 8; + } else { + break; + } + } + + return ret; +} + +} // namespace machina diff --git a/src/engine/SMFWriter.hpp b/src/engine/SMFWriter.hpp new file mode 100644 index 0000000..18bf3ec --- /dev/null +++ b/src/engine/SMFWriter.hpp @@ -0,0 +1,73 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_SMF_WRITER_HPP +#define MACHINA_SMF_WRITER_HPP + +#include <stdexcept> +#include <string> + +#include "raul/TimeStamp.hpp" + +#include "MIDISink.hpp" + +namespace machina { + +/** Standard Midi File (Type 0) Writer + * \ingroup raul + */ +class SMFWriter + : public MIDISink +{ +public: + explicit SMFWriter(Raul::TimeUnit unit); + ~SMFWriter(); + + bool start(const std::string& filename, + Raul::TimeStamp start_time); + + Raul::TimeUnit unit() const { return _unit; } + + void write_event(Raul::TimeStamp time, + size_t ev_size, + const unsigned char* ev); + + void flush(); + + void finish(); + +protected: + static const uint32_t VAR_LEN_MAX = 0x0FFFFFFF; + + void write_header(); + void write_footer(); + + void write_chunk_header(const char id[4], uint32_t length); + void write_chunk(const char id[4], uint32_t length, void* data); + size_t write_var_len(uint32_t val); + + std::string _filename; + FILE* _fd; + Raul::TimeUnit _unit; + Raul::TimeStamp _start_time; + Raul::TimeStamp _last_ev_time; ///< Time last event was written relative to _start_time + uint32_t _track_size; + uint32_t _header_size; ///< size of SMF header, including MTrk chunk header +}; + +} // namespace machina + +#endif // MACHINA_SMF_WRITER_HPP diff --git a/src/engine/Schrodinbit.hpp b/src/engine/Schrodinbit.hpp new file mode 100644 index 0000000..0721eb9 --- /dev/null +++ b/src/engine/Schrodinbit.hpp @@ -0,0 +1,42 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef SCHRODINBIT_HPP +#define SCHRODINBIT_HPP + +/** A flag which becomes false when its value is observed + */ +class Schrodinbit +{ +public: + Schrodinbit() : _flag(false) {} + + inline operator bool() { + const bool ret = _flag; + _flag = false; + return ret; + } + + inline bool operator=(bool flag) { + _flag = flag; + return flag; + } + +private: + bool _flag; +}; + +#endif // SCHRODINBIT_HPP diff --git a/src/engine/Slave.hpp b/src/engine/Slave.hpp new file mode 100644 index 0000000..74ec06b --- /dev/null +++ b/src/engine/Slave.hpp @@ -0,0 +1,73 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_SLAVE_HPP +#define MACHINA_SLAVE_HPP + +#include <thread> + +#include "raul/Semaphore.hpp" + +namespace machina { + +/** Thread driven by (realtime safe) signals. + * + * Use this to perform some task in a separate thread you want to 'drive' + * from a realtime (or otherwise) thread. + */ +class Slave +{ +public: + Slave() + : _whip(0) + , _exit_flag(false) + , _thread(&Slave::_run, this) + {} + + virtual ~Slave() { + _exit_flag = true; + _whip.post(); + _thread.join(); + } + + /** Tell the slave to do whatever work it does. Realtime safe. */ + inline void whip() { _whip.post(); } + +protected: + /** Worker method. + * + * This is called once from this thread every time whip() is called. + * Implementations likely want to put a single (non loop) chunk of code + * here, e.g. to process an event. + */ + virtual void _whipped() = 0; + + Raul::Semaphore _whip; + +private: + inline void _run() { + while (_whip.wait() && !_exit_flag) { + _whipped(); + } + } + + bool _exit_flag; + std::thread _thread; +}; + +} // namespace machina + +#endif // MACHINA_SLAVE_HPP diff --git a/src/engine/Stateful.cpp b/src/engine/Stateful.cpp new file mode 100644 index 0000000..7608a06 --- /dev/null +++ b/src/engine/Stateful.cpp @@ -0,0 +1,39 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Stateful.hpp" + +namespace machina { + +uint64_t Stateful::_next_id = 1; + +Stateful::Stateful() + : _id(next_id()) +{} + +const Sord::Node& +Stateful::rdf_id(Sord::World& world) const +{ + if (!_rdf_id.is_valid()) { + std::ostringstream ss; + ss << "b" << _id; + _rdf_id = Sord::Node(world, Sord::Node::BLANK, ss.str()); + } + + return _rdf_id; +} + +} // namespace machina diff --git a/src/engine/Stateful.hpp b/src/engine/Stateful.hpp new file mode 100644 index 0000000..b15d863 --- /dev/null +++ b/src/engine/Stateful.hpp @@ -0,0 +1,70 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_STATEFUL_HPP +#define MACHINA_STATEFUL_HPP + +#include <stdint.h> + +#include "sord/sordmm.hpp" + +#include "machina/Atom.hpp" +#include "machina/types.hpp" + +namespace Raul { +class Atom; +} + +namespace machina { + +class URIs; + +class Stateful +{ +public: + Stateful(); + + virtual ~Stateful() {} + + virtual void set(URIInt key, const Atom& value) {} + virtual void write_state(Sord::Model& model) = 0; + + uint64_t id() const { return _id; } + const Sord::Node& rdf_id(Sord::World& world) const; + + static uint64_t next_id() { return _next_id++; } + +protected: + explicit Stateful(uint64_t id) : _id(id) {} + +private: + static uint64_t _next_id; + + uint64_t _id; + mutable Sord::Node _rdf_id; +}; + +class StatefulKey : public Stateful +{ +public: + explicit StatefulKey(uint64_t id) : Stateful(id) {} + + void write_state(Sord::Model& model) {} +}; + +} // namespace machina + +#endif // MACHINA_STATEFUL_HPP diff --git a/src/engine/URIs.cpp b/src/engine/URIs.cpp new file mode 100644 index 0000000..2b43b81 --- /dev/null +++ b/src/engine/URIs.cpp @@ -0,0 +1,23 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "machina/URIs.hpp" + +namespace machina { + +URIs* URIs::_instance = NULL; + +} // namespace machina diff --git a/src/engine/Updates.cpp b/src/engine/Updates.cpp new file mode 100644 index 0000000..ce655e4 --- /dev/null +++ b/src/engine/Updates.cpp @@ -0,0 +1,66 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" + +#include "machina/Atom.hpp" +#include "machina/Updates.hpp" +#include "machina/types.hpp" +#include "raul/RingBuffer.hpp" + +namespace machina { + +void +write_set(SPtr<Raul::RingBuffer> buf, + uint64_t subject, + URIInt key, + const Atom& value) +{ + const uint32_t update_type = UPDATE_SET; + buf->write(sizeof(update_type), &update_type); + buf->write(sizeof(subject), &subject); + buf->write(sizeof(key), &key); + + const LV2_Atom atom = { value.size(), value.type() }; + buf->write(sizeof(LV2_Atom), &atom); + buf->write(value.size(), value.get_body()); +} + +uint32_t +read_set(SPtr<Raul::RingBuffer> buf, + uint64_t* subject, + URIInt* key, + Atom* value) +{ + uint32_t update_type = 0; + buf->read(sizeof(update_type), &update_type); + if (update_type != UPDATE_SET) { + return 0; + } + + buf->read(sizeof(*subject), subject); + buf->read(sizeof(*key), key); + + LV2_Atom atom = { 0, 0 }; + buf->read(sizeof(LV2_Atom), &atom); + *value = Atom(atom.size, atom.type, NULL); + buf->read(atom.size, value->get_body()); + + return sizeof(update_type) + sizeof(*subject) + sizeof(*key) + + sizeof(LV2_Atom) + atom.size; +} + +} diff --git a/src/engine/machina/Atom.hpp b/src/engine/machina/Atom.hpp new file mode 100644 index 0000000..710cc26 --- /dev/null +++ b/src/engine/machina/Atom.hpp @@ -0,0 +1,217 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Raul 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 3 of the License, or any later version. + + Raul 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 Raul. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_ATOM_HPP +#define MACHINA_ATOM_HPP + +#include <stdint.h> + +#include <cassert> +#include <cstdlib> +#include <cstring> +#include <string> + +namespace machina { + +class Forge; + +/** + A generic typed data container. + + An Atom holds a value with some type and size, both specified by a uint32_t. + Values with size less than sizeof(void*) are stored inline: no dynamic + allocation occurs so Atoms may be created in hard real-time threads. + Otherwise, if the size is larger than sizeof(void*), the value will be + dynamically allocated in a separate chunk of memory. +*/ +class Atom { +public: + Atom() : _size(0), _type(0) { _val._blob = NULL; } + ~Atom() { dealloc(); } + + typedef uint32_t TypeID; + + /** Contruct a raw atom. + * + * Typically this is not used directly, use Forge methods to make atoms. + */ + Atom(uint32_t size, TypeID type, const void* body) + : _size(size) + , _type(type) + { + if (is_reference()) { + _val._blob = malloc(size); + } + if (body) { + memcpy(get_body(), body, size); + } + } + + Atom(const Atom& copy) + : _size(copy._size) + , _type(copy._type) + { + if (is_reference()) { + _val._blob = malloc(_size); + memcpy(_val._blob, copy._val._blob, _size); + } else { + memcpy(&_val, ©._val, _size); + } + } + + Atom& operator=(const Atom& other) { + if (&other == this) { + return *this; + } + dealloc(); + _size = other._size; + _type = other._type; + if (is_reference()) { + _val._blob = malloc(_size); + memcpy(_val._blob, other._val._blob, _size); + } else { + memcpy(&_val, &other._val, _size); + } + return *this; + } + + inline bool operator==(const Atom& other) const { + if (_type == other.type() && _size == other.size()) { + if (is_reference()) { + return !memcmp(_val._blob, other._val._blob, _size); + } else { + return !memcmp(&_val, &other._val, _size); + } + } + return false; + } + + inline bool operator!=(const Atom& other) const { + return !operator==(other); + } + + inline bool operator<(const Atom& other) const { + if (_type == other.type()) { + if (is_reference()) { + return memcmp(_val._blob, other._val._blob, _size) < 0; + } else { + return memcmp(&_val, &other._val, _size) < 0; + } + } + return _type < other.type(); + } + + inline uint32_t size() const { return _size; } + inline bool is_valid() const { return _type; } + inline TypeID type() const { return _type; } + + inline const void* get_body() const { + return is_reference() ? _val._blob : &_val; + } + + inline void* get_body() { + return is_reference() ? _val._blob : &_val; + } + + template <typename T> const T& get() const { + assert(size() == sizeof(T)); + return *static_cast<const T*>(get_body()); + } + + template <typename T> const T* ptr() const { + return static_cast<const T*>(get_body()); + } + +private: + friend class Forge; + + /** Free dynamically allocated value, if applicable. */ + inline void dealloc() { + if (is_reference()) { + free(_val._blob); + } + } + + /** Return true iff this value is dynamically allocated. */ + inline bool is_reference() const { + return _size > sizeof(_val); + } + + uint32_t _size; + TypeID _type; + + union { + intptr_t _val; + void* _blob; + } _val; +}; + +class Forge { +public: + Forge() + : Int(1) + , Float(2) + , Bool(3) + , URI(4) + , URID(5) + , String(6) + {} + + virtual ~Forge() {} + + Atom make() { return Atom(); } + Atom make(int32_t v) { return Atom(sizeof(v), Int, &v); } + Atom make(float v) { return Atom(sizeof(v), Float, &v); } + Atom make(bool v) { + const int32_t iv = v ? 1 : 0; + return Atom(sizeof(int32_t), Bool, &iv); + } + + Atom make_urid(int32_t v) { return Atom(sizeof(int32_t), URID, &v); } + + Atom alloc(uint32_t size, uint32_t type, const void* val) { + return Atom(size, type, val); + } + + Atom alloc(const char* v) { + const size_t len = strlen(v); + return Atom(len + 1, String, v); + } + + Atom alloc(const std::string& v) { + return Atom(v.length() + 1, String, v.c_str()); + } + + Atom alloc_uri(const char* v) { + const size_t len = strlen(v); + return Atom(len + 1, URI, v); + } + + Atom alloc_uri(const std::string& v) { + return Atom(v.length() + 1, URI, v.c_str()); + } + + Atom::TypeID Int; + Atom::TypeID Float; + Atom::TypeID Bool; + Atom::TypeID URI; + Atom::TypeID URID; + Atom::TypeID String; +}; + +} // namespace machina + +#endif // MACHINA_ATOM_HPP diff --git a/src/engine/machina/Context.hpp b/src/engine/machina/Context.hpp new file mode 100644 index 0000000..766975b --- /dev/null +++ b/src/engine/machina/Context.hpp @@ -0,0 +1,51 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_CONTEXT_HPP +#define MACHINA_CONTEXT_HPP + +#include "machina/Atom.hpp" +#include "machina/types.hpp" +#include "raul/TimeSlice.hpp" + +namespace machina { + +class MIDISink; + +class Context +{ +public: + Context(Forge& forge, uint32_t rate, uint32_t ppqn, double bpm) + : _forge(forge) + , _time(rate, ppqn, bpm) + {} + + void set_sink(MIDISink* sink) { _sink = sink; } + + Forge& forge() { return _forge; } + const Raul::TimeSlice& time() const { return _time; } + Raul::TimeSlice& time() { return _time; } + MIDISink* sink() { return _sink; } + +private: + Forge& _forge; + Raul::TimeSlice _time; + MIDISink* _sink; +}; + +} // namespace machina + +#endif // MACHINA_CONTEXT_HPP diff --git a/src/engine/machina/Controller.hpp b/src/engine/machina/Controller.hpp new file mode 100644 index 0000000..833b5b2 --- /dev/null +++ b/src/engine/machina/Controller.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_CONTROLLER_HPP +#define MACHINA_CONTROLLER_HPP + +#include <stdint.h> + +#include <set> + +#include "raul/RingBuffer.hpp" +#include "raul/Maid.hpp" + +#include "machina/Model.hpp" +#include "machina/URIs.hpp" +#include "machina/types.hpp" + +#include "Stateful.hpp" + +namespace Raul { +class Atom; +} + +namespace machina { + +class Engine; +class Machine; + +class Controller +{ +public: + Controller(SPtr<Engine> engine, Model& model); + + uint64_t create(const Properties& properties); + uint64_t connect(uint64_t tail_id, uint64_t head_id); + + void set_property(uint64_t object_id, URIInt key, const Atom& value); + + void learn(SPtr<Raul::Maid> maid, uint64_t node_id); + void disconnect(uint64_t tail_id, uint64_t head_id); + void erase(uint64_t id); + + void announce(SPtr<Machine> machine); + + void process_updates(); + +private: + SPtr<Stateful> find(uint64_t id); + + struct StatefulComparator { + inline bool operator()(SPtr<Stateful> a, SPtr<Stateful> b) const { + return a->id() < b->id(); + } + }; + + typedef std::set<SPtr<Stateful>, StatefulComparator> Objects; + Objects _objects; + + SPtr<Engine> _engine; + Model& _model; + + SPtr<Raul::RingBuffer> _updates; +}; + +} + +#endif // MACHINA_CONTROLLER_HPP diff --git a/src/engine/machina/Driver.hpp b/src/engine/machina/Driver.hpp new file mode 100644 index 0000000..546c4d6 --- /dev/null +++ b/src/engine/machina/Driver.hpp @@ -0,0 +1,86 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_DRIVER_HPP +#define MACHINA_DRIVER_HPP + +#include "raul/RingBuffer.hpp" + +#include "machina/types.hpp" + +#include "MIDISink.hpp" + +namespace machina { + +class Machine; + +class Driver : public MIDISink +{ +public: + Driver(Forge& forge, SPtr<Machine> machine) + : _forge(forge) + , _machine(machine) + , _play_state(PlayState::STOPPED) + , _bpm(120.0) + , _quantization(0.125) + , _quantize_record(0) + {} + + enum class PlayState { + STOPPED, + PLAYING, + RECORDING, + STEP_RECORDING + }; + + virtual ~Driver() {} + + SPtr<Machine> machine() { return _machine; } + + virtual void set_machine(SPtr<Machine> machine) { + _machine = machine; + } + + SPtr<Raul::RingBuffer> update_sink() { return _updates; } + + void set_update_sink(SPtr<Raul::RingBuffer> b) { + _updates = b; + } + + virtual void set_bpm(double bpm) { _bpm = bpm; } + virtual void set_quantization(double q) { _quantization = q; } + virtual void set_quantize_record(bool q) { _quantize_record = q; } + virtual void set_play_state(PlayState state) { _play_state = state; } + + virtual bool is_activated() const { return false; } + virtual void activate() {} + virtual void deactivate() {} + + PlayState play_state() const { return _play_state; } + +protected: + Forge& _forge; + SPtr<Machine> _machine; + SPtr<Raul::RingBuffer> _updates; + PlayState _play_state; + double _bpm; + double _quantization; + bool _quantize_record; +}; + +} // namespace machina + +#endif // MACHINA_JACKDRIVER_HPP diff --git a/src/engine/machina/Engine.hpp b/src/engine/machina/Engine.hpp new file mode 100644 index 0000000..dd40f42 --- /dev/null +++ b/src/engine/machina/Engine.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_ENGINE_HPP +#define MACHINA_ENGINE_HPP + +#include <string> + +#include "machina/Atom.hpp" +#include "machina/Driver.hpp" +#include "machina/Loader.hpp" +#include "machina/types.hpp" + +namespace machina { + +class Machine; + +class Engine +{ +public: + Engine(Forge& forge, + SPtr<Driver> driver, + Sord::World& rdf_world); + + Sord::World& rdf_world() { return _rdf_world; } + + static SPtr<Driver> new_driver(Forge& forge, + const std::string& name, + SPtr<Machine> machine); + + SPtr<Driver> driver() { return _driver; } + SPtr<Machine> machine() { return _driver->machine(); } + Forge& forge() { return _forge; } + + SPtr<Machine> load_machine(const std::string& uri); + SPtr<Machine> load_machine_midi(const std::string& uri, + double q, + Raul::TimeDuration dur); + + void export_midi(const std::string& filename, + Raul::TimeDuration dur); + + void set_bpm(double bpm); + void set_quantization(double beat_fraction); + +private: + SPtr<Driver> _driver; + Sord::World& _rdf_world; + Loader _loader; + Forge _forge; +}; + +} // namespace machina + +#endif // MACHINA_ENGINE_HPP diff --git a/src/engine/machina/Evolver.hpp b/src/engine/machina/Evolver.hpp new file mode 100644 index 0000000..400c177 --- /dev/null +++ b/src/engine/machina/Evolver.hpp @@ -0,0 +1,72 @@ +/* + This file is part of Machina. + Copyright 2007-2017 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_EVOLVER_HPP +#define MACHINA_EVOLVER_HPP + +#include <atomic> +#include <memory> +#include <thread> + +#include "eugene/GA.hpp" +#include "eugene/Random.hpp" +#include "machina/types.hpp" +#include "raul/TimeStamp.hpp" + +#include "Machine.hpp" +#include "Schrodinbit.hpp" + +namespace eugene { +template<typename G> class HybridMutation; +} + +namespace machina { + +class Problem; + +class Evolver +{ +public: + Evolver(Raul::TimeUnit unit, + const std::string& target_midi, + SPtr<Machine> seed); + + void seed(SPtr<Machine> parent); + bool improvement() { return _improvement; } + + void start(); + void join(); + + const Machine& best() { return _ga->best(); } + + typedef eugene::GA<Machine> MachinaGA; + +private: + void run(); + + eugene::Random _rng; + SPtr<MachinaGA> _ga; + SPtr<Problem> _problem; + float _seed_fitness; + Schrodinbit _improvement; + std::atomic<bool> _exit_flag; + + std::unique_ptr<std::thread> _thread; +}; + +} // namespace machina + +#endif // MACHINA_EVOLVER_HPP diff --git a/src/engine/machina/Loader.hpp b/src/engine/machina/Loader.hpp new file mode 100644 index 0000000..3fa66ff --- /dev/null +++ b/src/engine/machina/Loader.hpp @@ -0,0 +1,49 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_LOADER_HPP +#define MACHINA_LOADER_HPP + +#include "machina/Atom.hpp" +#include "machina/types.hpp" +#include "raul/TimeStamp.hpp" +#include "sord/sordmm.hpp" + +using Sord::Namespaces; + +namespace machina { + +class Machine; + +class Loader +{ +public: + Loader(Forge& forge, Sord::World& rdf_world); + + SPtr<Machine> load(const std::string& filename); + + SPtr<Machine> load_midi(const std::string& filename, + double q, + Raul::TimeDuration dur); + +private: + Forge& _forge; + Sord::World& _rdf_world; +}; + +} // namespace machina + +#endif // MACHINA_LOADER_HPP diff --git a/src/engine/machina/Machine.hpp b/src/engine/machina/Machine.hpp new file mode 100644 index 0000000..7c9855d --- /dev/null +++ b/src/engine/machina/Machine.hpp @@ -0,0 +1,132 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_MACHINE_HPP +#define MACHINA_MACHINE_HPP + +#include <vector> +#include <set> + +#include "machina/types.hpp" +#include "machina/Atom.hpp" +#include "raul/RingBuffer.hpp" +#include "raul/TimeSlice.hpp" +#include "sord/sordmm.hpp" + +#include "types.hpp" +#include "Node.hpp" + +namespace machina { + +class Context; +class LearnRequest; + +/** A (Finite State) Machine. + */ +class Machine : public Stateful +{ +public: + explicit Machine(TimeUnit unit); + + /** Copy a Machine. + * + * Creates a deep copy which is the 'same' machine, but with fresh state, + * i.e. all nodes are inactive and time is at zero. + */ + Machine(const Machine& copy); + + /** Completely replace this machine's contents with a deep copy. */ + Machine& operator=(const Machine& copy); + + bool operator==(const Machine& rhs) const; + + /** Merge another machine into this machine. */ + void merge(const Machine& machine); + + bool is_empty() { return _nodes.empty(); } + bool is_finished() { return _is_finished; } + + void add_node(SPtr<Node> node); + void remove_node(SPtr<Node> node); + void learn(SPtr<Raul::Maid> maid, SPtr<Node> node); + + void write_state(Sord::Model& model); + + /** Exit all active nodes and reset time to 0. */ + void reset(MIDISink* sink, Raul::TimeStamp time); + + /** Run the machine for a (real) time slice. + * + * Returns the duration of time the machine actually ran in frames. + * + * Caller can check is_finished() to determine if the machine still has any + * active nodes. If not, time() will return the exact time stamp the + * machine actually finished on (so it can be restarted immediately + * with sample accuracy if necessary). + */ + uint32_t run(Context& context, SPtr<Raul::RingBuffer> updates); + + // Any context + inline Raul::TimeStamp time() const { return _time; } + + SPtr<LearnRequest> pending_learn() { return _pending_learn; } + void clear_pending_learn() { _pending_learn.reset(); } + + typedef std::set< SPtr<Node> > Nodes; + Nodes& nodes() { return _nodes; } + const Nodes& nodes() const { return _nodes; } + + SPtr<Node> initial_node() const { return _initial_node; } + + SPtr<Node> random_node(); + SPtr<Edge> random_edge(); + + float fitness; // For GA + +private: + /** Return the active Node with the earliest exit time. */ + SPtr<Node> earliest_node() const; + + void assign(const Machine& other); + + /** Enter a node at the current time (called by run()). + * + * @return true if node was entered, otherwise voics are exhausted. + */ + bool enter_node(Context& context, + SPtr<Node> node, + SPtr<Raul::RingBuffer> updates); + + /** Exit a node at the current time (called by run()). */ + void exit_node(Context& context, + SPtr<Node> node, + SPtr<Raul::RingBuffer> updates); + + static const size_t MAX_ACTIVE_NODES = 128; + + SPtr<Node> _initial_node; + std::vector< SPtr<Node> > _active_nodes; + + SPtr<LearnRequest> _pending_learn; + Nodes _nodes; + Raul::TimeStamp _time; + + bool _is_finished; +}; + +} // namespace machina + +#endif // MACHINA_MACHINE_HPP diff --git a/src/engine/machina/Model.hpp b/src/engine/machina/Model.hpp new file mode 100644 index 0000000..32a332e --- /dev/null +++ b/src/engine/machina/Model.hpp @@ -0,0 +1,44 @@ +/* + This file is part of Machina. + Copyright 2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_MODEL_HPP +#define MACHINA_MODEL_HPP + +#include <stdint.h> + +#include <map> + +#include "machina/Atom.hpp" +#include "machina/types.hpp" + +namespace machina { + +class Model +{ +public: + virtual ~Model() {} + + virtual void new_object(uint64_t id, const Properties& properties) = 0; + + virtual void erase_object(uint64_t id) = 0; + + virtual void set(uint64_t id, URIInt key, const Atom& value) = 0; + virtual const Atom& get(uint64_t id, URIInt key) const = 0; +}; + +} // namespace machina + +#endif // MACHINA_MODEL_HPP diff --git a/src/engine/machina/Mutation.hpp b/src/engine/machina/Mutation.hpp new file mode 100644 index 0000000..69f5ee4 --- /dev/null +++ b/src/engine/machina/Mutation.hpp @@ -0,0 +1,60 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_MACHINE_MUTATION_HPP +#define MACHINA_MACHINE_MUTATION_HPP + +#include "machina_config.h" + +#ifdef HAVE_EUGENE +# include "eugene/Mutation.hpp" +# define SUPER : public eugene::Mutation<Machine> +#else +# define SUPER : public Mutation +#endif + +namespace machina { + +#ifdef HAVE_EUGENE +typedef eugene::Random Random; +#else +struct Random {}; +#endif + +class Machine; + +namespace Mutation { + +struct Mutation { + virtual ~Mutation() {} + + virtual void mutate(Random& rng, Machine& machine) = 0; +}; + +struct Compress SUPER { void mutate(Random& rng, Machine& machine); }; +struct AddNode SUPER { void mutate(Random& rng, Machine& machine); }; +struct RemoveNode SUPER { void mutate(Random& rng, Machine& machine); }; +struct AdjustNode SUPER { void mutate(Random& rng, Machine& machine); }; +struct SwapNodes SUPER { void mutate(Random& rng, Machine& machine); }; +struct AddEdge SUPER { void mutate(Random& rng, Machine& machine); }; +struct RemoveEdge SUPER { void mutate(Random& rng, Machine& machine); }; +struct AdjustEdge SUPER { void mutate(Random& rng, Machine& machine); }; + +} // namespace Mutation + +} // namespace machina + +#endif // MACHINA_MACHINE_MUTATION_HPP diff --git a/src/engine/machina/URIs.hpp b/src/engine/machina/URIs.hpp new file mode 100644 index 0000000..f770723 --- /dev/null +++ b/src/engine/machina/URIs.hpp @@ -0,0 +1,93 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_URIS_HPP +#define MACHINA_URIS_HPP + +#include <stdint.h> + +#include "machina/Atom.hpp" +#include "machina/types.hpp" + +#define MACHINA_URI_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + +#define MACHINA_NS "http://drobilla.net/ns/machina#" + +#define MACHINA_NS_Machine MACHINA_NS "Machine" +#define MACHINA_NS_Node MACHINA_NS "Node" +#define MACHINA_NS_SelectorNode MACHINA_NS "SelectorNode" +#define MACHINA_NS_arc MACHINA_NS "arc" +#define MACHINA_NS_duration MACHINA_NS "duration" +#define MACHINA_NS_head MACHINA_NS "head" +#define MACHINA_NS_node MACHINA_NS "node" +#define MACHINA_NS_onEnter MACHINA_NS "onEnter" +#define MACHINA_NS_onExit MACHINA_NS "onExit" +#define MACHINA_NS_probability MACHINA_NS "probability" +#define MACHINA_NS_start MACHINA_NS "start" +#define MACHINA_NS_tail MACHINA_NS "tail" + +namespace machina { + +class URIs +{ +public: + static void init() { _instance = new URIs(); } + + static inline const URIs& instance() { assert(_instance); return *_instance; } + + URIInt machina_Edge; + URIInt machina_MidiAction; + URIInt machina_Node; + URIInt machina_active; + URIInt machina_canvas_x; + URIInt machina_canvas_y; + URIInt machina_duration; + URIInt machina_enter_action; + URIInt machina_exit_action; + URIInt machina_head_id; + URIInt machina_initial; + URIInt machina_note_number; + URIInt machina_probability; + URIInt machina_selector; + URIInt machina_tail_id; + URIInt rdf_type; + +private: + URIs() + : machina_Edge(100) + , machina_MidiAction(101) + , machina_Node(102) + , machina_active(1) + , machina_canvas_x(2) + , machina_canvas_y(3) + , machina_duration(4) + , machina_enter_action(11) + , machina_exit_action(12) + , machina_head_id(5) + , machina_initial(6) + , machina_note_number(13) + , machina_probability(7) + , machina_selector(8) + , machina_tail_id(9) + , rdf_type(10) + {} + + static URIs* _instance; +}; + +} // namespace machina + +#endif // MACHINA_URIS_HPP diff --git a/src/engine/machina/Updates.hpp b/src/engine/machina/Updates.hpp new file mode 100644 index 0000000..ff09af9 --- /dev/null +++ b/src/engine/machina/Updates.hpp @@ -0,0 +1,46 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_UPDATES_HPP +#define MACHINA_UPDATES_HPP + +#include <stdint.h> + +#include "machina/Atom.hpp" +#include "machina/types.hpp" +#include "raul/RingBuffer.hpp" + +namespace machina { + +enum UpdateType { + UPDATE_SET = 1 +}; + +void +write_set(SPtr<Raul::RingBuffer> buf, + uint64_t subject, + URIInt key, + const Atom& value); + +uint32_t +read_set(SPtr<Raul::RingBuffer> buf, + uint64_t* subject, + URIInt* key, + Atom* value); + +} // namespace machina + +#endif // MACHINA_UPDATES_HPP diff --git a/src/engine/machina/types.hpp b/src/engine/machina/types.hpp new file mode 100644 index 0000000..61137b7 --- /dev/null +++ b/src/engine/machina/types.hpp @@ -0,0 +1,69 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_TYPES_HPP +#define MACHINA_TYPES_HPP + +#include <map> +#include <memory> + +#include "raul/Maid.hpp" + +namespace machina { + +typedef unsigned char byte; + +typedef uint32_t URIInt; + +class Atom; +typedef std::map<URIInt, Atom> Properties; + +#if __cplusplus >= 201103L +template <class T> +using SPtr = std::shared_ptr<T>; + +template <class T> +using WPtr = std::weak_ptr<T>; + +template <class T> +using MPtr = Raul::managed_ptr<T>; +#else +#define SPtr std::shared_ptr +#define WPtr std::weak_ptr +#define MPtr Raul::managed_ptr +#endif + +template <class T> +void NullDeleter(T* ptr) {} + +template<class T, class U> +SPtr<T> static_ptr_cast(const SPtr<U>& r) { + return std::static_pointer_cast<T>(r); +} + +template<class T, class U> +SPtr<T> dynamic_ptr_cast(const SPtr<U>& r) { + return std::dynamic_pointer_cast<T>(r); +} + +template<class T, class U> +SPtr<T> const_ptr_cast(const SPtr<U>& r) { + return std::const_pointer_cast<T>(r); +} + +} // namespace machina + +#endif // MACHINA_TYPES_HPP diff --git a/src/engine/quantize.hpp b/src/engine/quantize.hpp new file mode 100644 index 0000000..9cf939a --- /dev/null +++ b/src/engine/quantize.hpp @@ -0,0 +1,46 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_QUANTIZE_HPP +#define MACHINA_QUANTIZE_HPP + +#include <cmath> + +#include "raul/TimeStamp.hpp" + +namespace machina { + +inline TimeStamp +quantize(TimeStamp q, TimeStamp t) +{ + assert(q.unit() == t.unit()); + // FIXME: Precision problem? Should probably stay in discrete domain + const double qd = q.to_double(); + const double td = t.to_double(); + return TimeStamp(t.unit(), (qd > 0) ? lrint(td / qd) * qd : td); +} + +inline double +quantize(double q, double t) +{ + return (q > 0) + ? lrint(t / q) * q + : t; +} + +} // namespace machina + +#endif // MACHINA_QUANTIZE_HPP diff --git a/src/engine/quantize_test.cpp b/src/engine/quantize_test.cpp new file mode 100644 index 0000000..5410489 --- /dev/null +++ b/src/engine/quantize_test.cpp @@ -0,0 +1,41 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> + +#include "quantize.hpp" + +using namespace std; +using namespace machina; + +int +main() +{ + TimeStamp q(TimeUnit(TimeUnit::BEATS, 19200), 0.25); + + for (double in = 0.0; in < 32; in += 0.23) { + TimeStamp beats(TimeUnit(TimeUnit::BEATS, 19200), in); + + /*cout << "Q(" << in << ", 1/4) = " + << quantize(q, beats) << endl;*/ + + if (quantize(q, beats).subticks() % (19200 / 4) != 0) { + return 1; + } + } + + return 0; +} diff --git a/src/engine/smf_test.cpp b/src/engine/smf_test.cpp new file mode 100644 index 0000000..d98a696 --- /dev/null +++ b/src/engine/smf_test.cpp @@ -0,0 +1,84 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> +#include <string> +#include "raul/log.hpp" +#include "raul/SMFReader.hpp" +#include "raul/SMFWriter.hpp" + +using namespace std; +using namespace machina; + +int +main(int argc, char** argv) +{ +#define CHECK(cond) \ + do { if (!(cond)) { \ + error << "Test at " << __FILE__ << ":" << __LINE__ \ + << " failed: " << __STRING(cond) << endl; \ + return 1; \ + } \ + } \ + while (0) + + static const uint16_t ppqn = 19200; + + const char* filename = NULL; + + if (argc < 2) { + filename = "./test.mid"; + SMFWriter writer(TimeUnit(TimeUnit::BEATS, ppqn)); + writer.start(string(filename), TimeStamp(writer.unit(), 0, 0)); + writer.finish(); + } else { + filename = argv[1]; + } + + SMFReader reader; + bool opened = reader.open(filename); + + if (!opened) { + cerr << "Unable to open SMF file " << filename << endl; + return -1; + } + + CHECK(reader.type() == 0); + CHECK(reader.num_tracks() == 1); + CHECK(reader.ppqn() == ppqn); + + for (unsigned t = 1; t <= reader.num_tracks(); ++t) { + reader.seek_to_track(t); + + unsigned char buf[4]; + uint32_t ev_size; + uint32_t ev_delta_time; + while (reader.read_event(4, buf, &ev_size, &ev_delta_time) >= 0) { + + cout << t << ": Event, size = " << ev_size << ", time = " + << ev_delta_time; + cout << ":\t"; + cout.flags(ios::hex); + for (uint32_t i = 0; i < ev_size; ++i) { + cout << "0x" << static_cast<int>(buf[i]) << " "; + } + cout.flags(ios::dec); + cout << endl; + } + } + + return 0; +} diff --git a/src/engine/wscript b/src/engine/wscript new file mode 100644 index 0000000..1ec63a2 --- /dev/null +++ b/src/engine/wscript @@ -0,0 +1,45 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + core_source = ''' + ActionFactory.cpp + Controller.cpp + Edge.cpp + Engine.cpp + JackDriver.cpp + LearnRequest.cpp + Loader.cpp + Machine.cpp + MachineBuilder.cpp + MidiAction.cpp + Mutation.cpp + Node.cpp + Recorder.cpp + SMFDriver.cpp + SMFReader.cpp + SMFWriter.cpp + Stateful.cpp + Updates.cpp + URIs.cpp + ''' + if bld.env.HAVE_EUGENE: + core_source += ''' + Evolver.cpp + Problem.cpp + ''' + obj = bld(features = 'cxx cxxshlib') + obj.source = core_source + obj.export_includes = ['.'] + obj.includes = ['.', '..', '../..'] + obj.name = 'libmachina_engine' + obj.target = 'machina_engine' + obj.cxxflags = '-pthread' + if bld.env.CXX_NAME != 'clang': + obj.linkflags = '-pthread' + core_libs = 'RAUL SERD SORD JACK LV2' + if bld.env.HAVE_EUGENE: + core_libs += ' EUGENE ' + autowaf.use_lib(bld, obj, core_libs) + + bld.add_post_fun(autowaf.run_ldconfig) diff --git a/src/gui/EdgeView.cpp b/src/gui/EdgeView.cpp new file mode 100644 index 0000000..b4c6099 --- /dev/null +++ b/src/gui/EdgeView.cpp @@ -0,0 +1,142 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ganv/Canvas.hpp" + +#include "machina/Controller.hpp" +#include "machina/types.hpp" + +#include "EdgeView.hpp" +#include "MachinaCanvas.hpp" +#include "MachinaGUI.hpp" +#include "NodeView.hpp" + +namespace machina { +namespace gui { + +/* probability colour stuff */ + +#define RGB_TO_UINT(r, g, b) ((((guint)(r)) << 16) | (((guint)(g)) << 8) | ((guint)(b))) +#define RGB_TO_RGBA(x, a) (((x) << 8) | ((((guint)a) & 0xff))) +#define RGBA_TO_UINT(r, g, b, a) RGB_TO_RGBA(RGB_TO_UINT(r, g, b), a) + +#define UINT_RGBA_R(x) (((uint32_t)(x)) >> 24) +#define UINT_RGBA_G(x) ((((uint32_t)(x)) >> 16) & 0xff) +#define UINT_RGBA_B(x) ((((uint32_t)(x)) >> 8) & 0xff) +#define UINT_RGBA_A(x) (((uint32_t)(x)) & 0xff) + +#define MONO_INTERPOLATE(v1, v2, t) ((int)rint((v2) * (t) + (v1) * (1 - (t)))) + +#define UINT_INTERPOLATE(c1, c2, t) \ + RGBA_TO_UINT(MONO_INTERPOLATE(UINT_RGBA_R(c1), UINT_RGBA_R(c2), t), \ + MONO_INTERPOLATE(UINT_RGBA_G(c1), UINT_RGBA_G(c2), t), \ + MONO_INTERPOLATE(UINT_RGBA_B(c1), UINT_RGBA_B(c2), t), \ + MONO_INTERPOLATE(UINT_RGBA_A(c1), UINT_RGBA_A(c2), t) ) + +inline static uint32_t edge_color(float prob) +{ + static const uint32_t min = 0xFF4444FF; + static const uint32_t mid = 0xFFFF44FF; + static const uint32_t max = 0x44FF44FF; + + if (prob <= 0.5) { + return UINT_INTERPOLATE(min, mid, prob * 2.0); + } else { + return UINT_INTERPOLATE(mid, max, (prob - 0.5) * 2.0); + } +} + +/* end probability colour stuff */ + +using namespace Ganv; + +EdgeView::EdgeView(Canvas& canvas, + NodeView* src, + NodeView* dst, + SPtr<machina::client::ClientObject> edge) + : Ganv::Edge(canvas, src, dst, 0x9FA0A0FF, true, false) + , _edge(edge) +{ + set_color(edge_color(probability())); + + edge->signal_property.connect( + sigc::mem_fun(this, &EdgeView::on_property)); + + signal_event().connect( + sigc::mem_fun(this, &EdgeView::on_event)); +} + +EdgeView::~EdgeView() +{ + _edge->set_view(NULL); +} + +float +EdgeView::probability() const +{ + return _edge->get(URIs::instance().machina_probability).get<float>(); +} + +double +EdgeView::length_hint() const +{ + NodeView* tail = dynamic_cast<NodeView*>(get_tail()); + return tail->node()->get(URIs::instance().machina_duration).get<float>() + * 10.0; +} + +void +EdgeView::show_label(bool show) +{ + set_color(edge_color(probability())); +} + +bool +EdgeView::on_event(GdkEvent* ev) +{ + MachinaCanvas* canvas = dynamic_cast<MachinaCanvas*>(this->canvas()); + Forge& forge = canvas->app()->forge(); + + if (ev->type == GDK_BUTTON_PRESS) { + if (ev->button.state & GDK_CONTROL_MASK) { + if (ev->button.button == 1) { + canvas->app()->controller()->set_property( + _edge->id(), + URIs::instance().machina_probability, + forge.make(float(probability() - 0.1f))); + return true; + } else if (ev->button.button == 3) { + canvas->app()->controller()->set_property( + _edge->id(), + URIs::instance().machina_probability, + forge.make(float(probability() + 0.1f))); + return true; + } + } + } + return false; +} + +void +EdgeView::on_property(machina::URIInt key, const Atom& value) +{ + if (key == URIs::instance().machina_probability) { + set_color(edge_color(value.get<float>())); + } +} + +} // namespace machina +} // namespace gui diff --git a/src/gui/EdgeView.hpp b/src/gui/EdgeView.hpp new file mode 100644 index 0000000..1ad2dd0 --- /dev/null +++ b/src/gui/EdgeView.hpp @@ -0,0 +1,59 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_EDGEVIEW_HPP +#define MACHINA_EDGEVIEW_HPP + +#include "ganv/Edge.hpp" + +#include "client/ClientObject.hpp" + +#include "machina/types.hpp" + +namespace machina { +namespace gui { + +class NodeView; + +class EdgeView + : public Ganv::Edge + , public machina::client::ClientObject::View +{ +public: + EdgeView(Ganv::Canvas& canvas, + NodeView* src, + NodeView* dst, + SPtr<machina::client::ClientObject> edge); + + ~EdgeView(); + + void show_label(bool show); + + virtual double length_hint() const; + +private: + bool on_event(GdkEvent* ev); + void on_property(machina::URIInt key, const Atom& value); + + float probability() const; + + SPtr<machina::client::ClientObject> _edge; +}; + +} // namespace machina +} // namespace gui + +#endif // MACHINA_EDGEVIEW_HPP diff --git a/src/gui/MachinaCanvas.cpp b/src/gui/MachinaCanvas.cpp new file mode 100644 index 0000000..a9e51c4 --- /dev/null +++ b/src/gui/MachinaCanvas.cpp @@ -0,0 +1,211 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <map> + +#include "raul/TimeStamp.hpp" + +#include "client/ClientModel.hpp" +#include "client/ClientObject.hpp" +#include "machina/Controller.hpp" +#include "machina/Engine.hpp" +#include "machina/types.hpp" + +#include "EdgeView.hpp" +#include "MachinaCanvas.hpp" +#include "MachinaGUI.hpp" +#include "NodeView.hpp" + +using namespace Raul; +using namespace Ganv; + +namespace machina { +namespace gui { + +MachinaCanvas::MachinaCanvas(MachinaGUI* app, int width, int height) + : Canvas(width, height) + , _app(app) + , _connect_node(NULL) + , _did_connect(false) +{ + widget().grab_focus(); + + signal_event.connect(sigc::mem_fun(this, &MachinaCanvas::on_event)); +} + +void +MachinaCanvas::connect_nodes(GanvNode* node, void* data) +{ + MachinaCanvas* canvas = (MachinaCanvas*)data; + NodeView* view = dynamic_cast<NodeView*>(Glib::wrap(node)); + if (!view || view == canvas->_connect_node) { + return; + } + if (canvas->get_edge(view, canvas->_connect_node)) { + canvas->action_disconnect(view, canvas->_connect_node); + canvas->_did_connect = true; + } else if (!canvas->get_edge(canvas->_connect_node, view)) { + canvas->action_connect(view, canvas->_connect_node); + canvas->_did_connect = true; + } +} + +bool +MachinaCanvas::node_clicked(NodeView* node, GdkEventButton* event) +{ + if (event->state & (GDK_CONTROL_MASK|GDK_SHIFT_MASK)) { + return false; + + } else if (event->button == 2) { + // Middle click: learn + _app->controller()->learn(_app->maid(), node->node()->id()); + return false; + + } else if (event->button == 1) { + // Left click: connect/disconnect + _connect_node = node; + for_each_selected_node(connect_nodes, this); + const bool handled = _did_connect; + _connect_node = NULL; + _did_connect = false; + if (_app->chain_mode()) { + return false; // Cause Ganv to select as usual + } else { + return handled; // If we did something, stop event here + } + } + + return false; +} + +bool +MachinaCanvas::on_event(GdkEvent* event) +{ + if (event->type == GDK_BUTTON_RELEASE + && event->button.button == 3 + && !(event->button.state & (GDK_CONTROL_MASK))) { + + action_create_node(event->button.x, event->button.y); + return true; + } + return false; +} + +void +MachinaCanvas::on_new_object(SPtr<client::ClientObject> object) +{ + const machina::URIs& uris = URIs::instance(); + const Atom& type = object->get(uris.rdf_type); + if (!type.is_valid()) { + return; + } + + if (type.get<URIInt>() == uris.machina_Node) { + const Atom& node_x = object->get(uris.machina_canvas_x); + const Atom& node_y = object->get(uris.machina_canvas_y); + float x, y; + if (node_x.type() == _app->forge().Float && + node_y.type() == _app->forge().Float) { + x = node_x.get<float>(); + y = node_y.get<float>(); + } else { + int scroll_x, scroll_y; + get_scroll_offsets(scroll_x, scroll_y); + x = scroll_x + 128.0; + y = scroll_y + 128.0; + } + + NodeView* view = new NodeView(_app->window(), *this, object, x, y); + + //if ( ! node->enter_action() && ! node->exit_action() ) + // view->set_base_color(0x101010FF); + + view->signal_clicked().connect( + sigc::bind<0>(sigc::mem_fun(this, &MachinaCanvas::node_clicked), + view)); + + object->set_view(view); + + } else if (type.get<URIInt>() == uris.machina_Edge) { + SPtr<machina::client::ClientObject> tail = _app->client_model()->find( + object->get(uris.machina_tail_id).get<int32_t>()); + SPtr<machina::client::ClientObject> head = _app->client_model()->find( + object->get(uris.machina_head_id).get<int32_t>()); + + if (!tail || !head) { + std::cerr << "Invalid arc " + << object->get(uris.machina_tail_id).get<int32_t>() + << " => " + << object->get(uris.machina_head_id).get<int32_t>() + << std::endl; + return; + } + + NodeView* tail_view = dynamic_cast<NodeView*>(tail->view()); + NodeView* head_view = dynamic_cast<NodeView*>(head->view()); + + object->set_view(new EdgeView(*this, tail_view, head_view, object)); + + } else { + std::cerr << "Unknown object type " << type.get<URIInt>() << std::endl; + } +} + +void +MachinaCanvas::on_erase_object(SPtr<client::ClientObject> object) +{ + const Atom& type = object->get(URIs::instance().rdf_type); + if (type.get<URIInt>() == URIs::instance().machina_Node) { + delete object->view(); + object->set_view(NULL); + } else if (type.get<URIInt>() == URIs::instance().machina_Edge) { + remove_edge(dynamic_cast<Ganv::Edge*>(object->view())); + object->set_view(NULL); + } else { + std::cerr << "Unknown object type" << std::endl; + } +} + +void +MachinaCanvas::action_create_node(double x, double y) +{ + const Properties props = { + { URIs::instance().rdf_type, + _app->forge().make_urid(URIs::instance().machina_Node) }, + { URIs::instance().machina_canvas_x, + _app->forge().make((float)x) }, + { URIs::instance().machina_canvas_y, + _app->forge().make((float)y) }, + { URIs::instance().machina_duration, + _app->forge().make((float)_app->default_length()) } }; + + _app->controller()->create(props); +} + +void +MachinaCanvas::action_connect(NodeView* tail, NodeView* head) +{ + _app->controller()->connect(tail->node()->id(), head->node()->id()); +} + +void +MachinaCanvas::action_disconnect(NodeView* tail, NodeView* head) +{ + _app->controller()->disconnect(tail->node()->id(), head->node()->id()); +} + +} // namespace machina +} // namespace gui diff --git a/src/gui/MachinaCanvas.hpp b/src/gui/MachinaCanvas.hpp new file mode 100644 index 0000000..f32e6cb --- /dev/null +++ b/src/gui/MachinaCanvas.hpp @@ -0,0 +1,66 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_CANVAS_HPP_HPP +#define MACHINA_CANVAS_HPP_HPP + +#include <string> + +#include "ganv/Canvas.hpp" +#include "machina/types.hpp" + +using namespace Ganv; + +namespace machina { + +namespace client { class ClientObject; } + +namespace gui { + +class MachinaGUI; +class NodeView; + +class MachinaCanvas : public Canvas +{ +public: + MachinaCanvas(MachinaGUI* app, int width, int height); + + void on_new_object(SPtr<machina::client::ClientObject> object); + void on_erase_object(SPtr<machina::client::ClientObject> object); + + MachinaGUI* app() { return _app; } + +protected: + bool on_event(GdkEvent* event); + + bool node_clicked(NodeView* node, GdkEventButton* ev); + +private: + void action_create_node(double x, double y); + void action_connect(NodeView* tail, NodeView* head); + void action_disconnect(NodeView* tail, NodeView* head); + + static void connect_nodes(GanvNode* node, void* data); + + MachinaGUI* _app; + NodeView* _connect_node; + bool _did_connect; +}; + +} // namespace machina +} // namespace gui + +#endif // MACHINA_CANVAS_HPP_HPP diff --git a/src/gui/MachinaGUI.cpp b/src/gui/MachinaGUI.cpp new file mode 100644 index 0000000..ff71ca7 --- /dev/null +++ b/src/gui/MachinaGUI.cpp @@ -0,0 +1,750 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "machina_config.h" + +#include <cmath> +#include <fstream> +#include <limits.h> +#include <pthread.h> +#include "sord/sordmm.hpp" +#include "machina/Controller.hpp" +#include "machina/Engine.hpp" +#include "machina/Machine.hpp" +#include "machina/Mutation.hpp" +#include "machina/Updates.hpp" +#include "client/ClientModel.hpp" +#include "WidgetFactory.hpp" +#include "MachinaGUI.hpp" +#include "MachinaCanvas.hpp" +#include "NodeView.hpp" +#include "EdgeView.hpp" + +#ifdef HAVE_EUGENE +#include "machina/Evolver.hpp" +#endif + +namespace machina { +namespace gui { + +MachinaGUI::MachinaGUI(SPtr<machina::Engine> engine) + : _unit(TimeUnit::BEATS, 19200) + , _engine(engine) + , _client_model(new machina::client::ClientModel()) + , _controller(new machina::Controller(_engine, *_client_model.get())) + , _maid(new Raul::Maid()) + , _refresh(false) + , _evolve(false) + , _chain_mode(true) +{ + _canvas = SPtr<MachinaCanvas>(new MachinaCanvas(this, 1600*2, 1200*2)); + + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create(); + + xml->get_widget("machina_win", _main_window); + xml->get_widget("about_win", _about_window); + xml->get_widget("help_dialog", _help_dialog); + xml->get_widget("toolbar", _toolbar); + xml->get_widget("open_menuitem", _menu_file_open); + xml->get_widget("save_menuitem", _menu_file_save); + xml->get_widget("save_as_menuitem", _menu_file_save_as); + xml->get_widget("quit_menuitem", _menu_file_quit); + xml->get_widget("zoom_in_menuitem", _menu_zoom_in); + xml->get_widget("zoom_out_menuitem", _menu_zoom_out); + xml->get_widget("zoom_normal_menuitem", _menu_zoom_normal); + xml->get_widget("arrange_menuitem", _menu_view_arrange); + xml->get_widget("import_midi_menuitem", _menu_import_midi); + xml->get_widget("export_midi_menuitem", _menu_export_midi); + xml->get_widget("export_graphviz_menuitem", _menu_export_graphviz); + xml->get_widget("view_toolbar_menuitem", _menu_view_toolbar); + xml->get_widget("view_labels_menuitem", _menu_view_labels); + xml->get_widget("help_about_menuitem", _menu_help_about); + xml->get_widget("help_help_menuitem", _menu_help_help); + xml->get_widget("canvas_scrolledwindow", _canvas_scrolledwindow); + xml->get_widget("bpm_spinbutton", _bpm_spinbutton); + xml->get_widget("quantize_checkbutton", _quantize_checkbutton); + xml->get_widget("quantize_spinbutton", _quantize_spinbutton); + xml->get_widget("stop_but", _stop_button); + xml->get_widget("play_but", _play_button); + xml->get_widget("record_but", _record_button); + xml->get_widget("step_record_but", _step_record_button); + xml->get_widget("chain_but", _chain_button); + xml->get_widget("fan_but", _fan_button); + xml->get_widget("load_target_but", _load_target_button); + xml->get_widget("evolve_toolbar", _evolve_toolbar); + xml->get_widget("evolve_but", _evolve_button); + xml->get_widget("mutate_but", _mutate_button); + xml->get_widget("compress_but", _compress_button); + xml->get_widget("add_node_but", _add_node_button); + xml->get_widget("remove_node_but", _remove_node_button); + xml->get_widget("adjust_node_but", _adjust_node_button); + xml->get_widget("add_edge_but", _add_edge_button); + xml->get_widget("remove_edge_but", _remove_edge_button); + xml->get_widget("adjust_edge_but", _adjust_edge_button); + + _canvas_scrolledwindow->add(_canvas->widget()); + _canvas_scrolledwindow->signal_event().connect(sigc::mem_fun(this, + &MachinaGUI::scrolled_window_event)); + + _canvas_scrolledwindow->property_hadjustment().get_value()->set_step_increment(10); + _canvas_scrolledwindow->property_vadjustment().get_value()->set_step_increment(10); + + _stop_button->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::stop_toggled)); + _play_button->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::play_toggled)); + _record_button->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::record_toggled)); + _step_record_button->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::step_record_toggled)); + + _chain_button->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::chain_toggled)); + _fan_button->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::fan_toggled)); + + _menu_file_open->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_file_open)); + _menu_file_save->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_file_save)); + _menu_file_save_as->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_file_save_as)); + _menu_file_quit->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_file_quit)); + _menu_zoom_in->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::on_zoom_in)); + _menu_zoom_out->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::on_zoom_out)); + _menu_zoom_normal->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::on_zoom_normal)); + _menu_view_arrange->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::arrange)); + _menu_import_midi->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_import_midi)); + _menu_export_midi->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_export_midi)); + _menu_export_graphviz->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_export_graphviz)); + _menu_view_toolbar->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::show_toolbar_toggled)); + _menu_view_labels->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::show_labels_toggled)); + _menu_help_about->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_help_about)); + _menu_help_help->signal_activate().connect( + sigc::mem_fun(this, &MachinaGUI::menu_help_help)); + _bpm_spinbutton->signal_changed().connect( + sigc::mem_fun(this, &MachinaGUI::tempo_changed)); + _quantize_checkbutton->signal_toggled().connect( + sigc::mem_fun(this, &MachinaGUI::quantize_record_changed)); + _quantize_spinbutton->signal_changed().connect( + sigc::mem_fun(this, &MachinaGUI::quantize_changed)); + + _mutate_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &MachinaGUI::random_mutation), + SPtr<Machine>())); + _compress_button->signal_clicked().connect( + sigc::hide_return(sigc::bind(sigc::mem_fun(this, &MachinaGUI::mutate), + SPtr<Machine>(), 0))); + _add_node_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &MachinaGUI::mutate), + SPtr<Machine>(), 1)); + _remove_node_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &MachinaGUI::mutate), + SPtr<Machine>(), 2)); + _adjust_node_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &MachinaGUI::mutate), + SPtr<Machine>(), 3)); + _add_edge_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &MachinaGUI::mutate), + SPtr<Machine>(), 4)); + _remove_edge_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &MachinaGUI::mutate), + SPtr<Machine>(), 5)); + _adjust_edge_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &MachinaGUI::mutate), + SPtr<Machine>(), 6)); + + _canvas->widget().show(); + + _main_window->present(); + + _quantize_checkbutton->set_active(false); + update_toolbar(); + + // Idle callback to drive the maid (collect garbage) + Glib::signal_timeout().connect( + sigc::bind_return(sigc::mem_fun(_maid.get(), &Raul::Maid::cleanup), + true), + 1000); + + // Idle callback to update node states + Glib::signal_timeout().connect( + sigc::mem_fun(this, &MachinaGUI::idle_callback), 100); + +#ifdef HAVE_EUGENE + _load_target_button->signal_clicked().connect( + sigc::mem_fun(this, &MachinaGUI::load_target_clicked)); + _evolve_button->signal_clicked().connect( + sigc::mem_fun(this, &MachinaGUI::evolve_toggled)); + Glib::signal_timeout().connect( + sigc::mem_fun(this, &MachinaGUI::evolve_callback), 1000); +#else + _evolve_toolbar->hide(); +#endif + + _client_model->signal_new_object.connect( + sigc::mem_fun(this, &MachinaGUI::on_new_object)); + _client_model->signal_erase_object.connect( + sigc::mem_fun(this, &MachinaGUI::on_erase_object)); + + rebuild_canvas(); +} + +MachinaGUI::~MachinaGUI() +{ +} + +#ifdef HAVE_EUGENE +bool +MachinaGUI::evolve_callback() +{ + if (_evolve && _evolver->improvement()) { + _engine->driver()->set_machine( + SPtr<Machine>(new Machine(_evolver->best()))); + _controller->announce(_engine->machine()); + } + + return true; +} +#endif + +bool +MachinaGUI::idle_callback() +{ + _controller->process_updates(); + return true; +} + +static void +destroy_edge(GanvEdge* edge, void* data) +{ + MachinaGUI* gui = (MachinaGUI*)data; + EdgeView* view = dynamic_cast<EdgeView*>(Glib::wrap(edge)); + if (view) { + NodeView* tail = dynamic_cast<NodeView*>(view->get_tail()); + NodeView* head = dynamic_cast<NodeView*>(view->get_head()); + gui->controller()->disconnect(tail->node()->id(), head->node()->id()); + } +} + +static void +destroy_node(GanvNode* node, void* data) +{ + MachinaGUI* gui = (MachinaGUI*)data; + NodeView* view = dynamic_cast<NodeView*>(Glib::wrap(GANV_NODE(node))); + if (view) { + const SPtr<client::ClientObject> node = view->node(); + gui->canvas()->for_each_edge_on( + GANV_NODE(view->gobj()), destroy_edge, gui); + gui->controller()->erase(node->id()); + } +} + +bool +MachinaGUI::scrolled_window_event(GdkEvent* event) +{ + if (event->type == GDK_KEY_PRESS) { + if (event->key.keyval == GDK_Delete) { + _canvas->for_each_selected_node(destroy_node, this); + return true; + } + } + + return false; +} + +void +MachinaGUI::arrange() +{ + _canvas->arrange(); +} + +void +MachinaGUI::load_target_clicked() +{ + Gtk::FileChooserDialog dialog(*_main_window, + "Load MIDI file for evolution", Gtk::FILE_CHOOSER_ACTION_OPEN); + dialog.set_local_only(false); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::OPEN, Gtk::RESPONSE_OK); + + Gtk::FileFilter filt; + filt.add_pattern("*.mid"); + filt.set_name("MIDI Files"); + dialog.set_filter(filt); + + const int result = dialog.run(); + + if (result == Gtk::RESPONSE_OK) + _target_filename = dialog.get_filename(); +} + +#ifdef HAVE_EUGENE +void +MachinaGUI::evolve_toggled() +{ + if (_evolve_button->get_active()) { + _evolver = SPtr<Evolver>( + new Evolver(_unit, _target_filename, _engine->machine())); + _evolve = true; + stop_toggled(); + _engine->driver()->set_machine(SPtr<Machine>()); + _evolver->start(); + } else { + _evolver->join(); + _evolve = false; + SPtr<Machine> new_machine = SPtr<Machine>( + new Machine(_evolver->best())); + _engine->driver()->set_machine(new_machine); + _controller->announce(_engine->machine()); + _engine->driver()->activate(); + } +} +#endif + +void +MachinaGUI::random_mutation(SPtr<Machine> machine) +{ + if (!machine) + machine = _engine->machine(); + + mutate(machine, machine->nodes().size() < 2 ? 1 : rand() % 7); +} + +void +MachinaGUI::mutate(SPtr<Machine> machine, unsigned mutation) +{ + #if 0 + if (!machine) + machine = _engine->machine(); + + using namespace Mutation; + + switch (mutation) { + case 0: + Compress().mutate(*machine.get()); + _canvas->build(machine, _menu_view_labels->get_active()); + break; + case 1: + AddNode().mutate(*machine.get()); + _canvas->build(machine, _menu_view_labels->get_active()); + break; + case 2: + RemoveNode().mutate(*machine.get()); + _canvas->build(machine, _menu_view_labels->get_active()); + break; + case 3: + AdjustNode().mutate(*machine.get()); + idle_callback(); // update nodes + break; + case 4: + AddEdge().mutate(*machine.get()); + _canvas->build(machine, _menu_view_labels->get_active()); + break; + case 5: + RemoveEdge().mutate(*machine.get()); + _canvas->build(machine, _menu_view_labels->get_active()); + break; + case 6: + AdjustEdge().mutate(*machine.get()); + _canvas->update_edges(); + break; + default: throw; + } + #endif +} + +void +MachinaGUI::update_toolbar() +{ + const Driver::PlayState state = _engine->driver()->play_state(); + _record_button->set_active(state == Driver::PlayState::RECORDING); + _step_record_button->set_active(state == Driver::PlayState::STEP_RECORDING); + _play_button->set_active(state == Driver::PlayState::PLAYING); +} + +void +MachinaGUI::rebuild_canvas() +{ + _controller->announce(_engine->machine()); + _canvas->arrange(); +} + +void +MachinaGUI::quantize_record_changed() +{ + _engine->driver()->set_quantize_record(_quantize_checkbutton->get_active()); +} + +void +MachinaGUI::quantize_changed() +{ + _engine->set_quantization(1.0 / _quantize_spinbutton->get_value()); +} + +void +MachinaGUI::tempo_changed() +{ + _engine->set_bpm(_bpm_spinbutton->get_value_as_int()); +} + +void +MachinaGUI::menu_file_quit() +{ + _main_window->hide(); +} + +void +MachinaGUI::menu_file_open() +{ + Gtk::FileChooserDialog dialog(*_main_window, "Open Machine", Gtk::FILE_CHOOSER_ACTION_OPEN); + dialog.set_local_only(false); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::OPEN, Gtk::RESPONSE_OK); + + Gtk::FileFilter filt; + filt.add_pattern("*.machina.ttl"); + filt.set_name("Machina Machines (Turtle/RDF)"); + dialog.set_filter(filt); + + const int result = dialog.run(); + + if (result == Gtk::RESPONSE_OK) { + SPtr<machina::Machine> new_machine = _engine->load_machine(dialog.get_uri()); + if (new_machine) { + rebuild_canvas(); + _save_uri = dialog.get_uri(); + } + } +} + +void +MachinaGUI::menu_file_save() +{ + if (_save_uri == "" || _save_uri.substr(0, 5) != "file:") { + menu_file_save_as(); + } else { + if (_save_uri.substr(0, 5) != "file:") + menu_file_save_as(); + + Sord::Model model(_engine->rdf_world(), _save_uri); + _engine->machine()->write_state(model); + model.write_to_file(_save_uri, SERD_TURTLE); + } +} + +void +MachinaGUI::menu_file_save_as() +{ + Gtk::FileChooserDialog dialog(*_main_window, "Save Machine", + Gtk::FILE_CHOOSER_ACTION_SAVE); + + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + + if (_save_uri.length() > 0) + dialog.set_uri(_save_uri); + + const int result = dialog.run(); + + assert(result == Gtk::RESPONSE_OK + || result == Gtk::RESPONSE_CANCEL + || result == Gtk::RESPONSE_NONE); + + if (result == Gtk::RESPONSE_OK) { + string filename = dialog.get_filename(); + + if (filename.length() < 13 || filename.substr(filename.length()-12) != ".machina.ttl") + filename += ".machina.ttl"; + + const std::string uri = Glib::filename_to_uri(filename); + + bool confirm = false; + std::fstream fin; + fin.open(filename.c_str(), std::ios::in); + if (fin.is_open()) { // File exists + string msg = "A file named \""; + msg += filename + "\" already exists.\n\nDo you want to replace it?"; + Gtk::MessageDialog confirm_dialog(dialog, + msg, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); + if (confirm_dialog.run() == Gtk::RESPONSE_YES) + confirm = true; + else + confirm = false; + } else { // File doesn't exist + confirm = true; + } + fin.close(); + + if (confirm) { + _save_uri = uri; + Sord::Model model(_engine->rdf_world(), _save_uri); + _engine->machine()->write_state(model); + model.write_to_file(_save_uri, SERD_TURTLE); + } + } +} + +void +MachinaGUI::menu_import_midi() +{ + Gtk::FileChooserDialog dialog(*_main_window, "Learn from MIDI file", + Gtk::FILE_CHOOSER_ACTION_OPEN); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::OPEN, Gtk::RESPONSE_OK); + + Gtk::FileFilter filt; + filt.add_pattern("*.mid"); + filt.set_name("MIDI Files"); + dialog.set_filter(filt); + + Gtk::HBox* extra_widget = Gtk::manage(new Gtk::HBox()); + Gtk::SpinButton* length_sb = Gtk::manage(new Gtk::SpinButton()); + length_sb->set_increments(1, 10); + length_sb->set_range(0, INT_MAX); + length_sb->set_value(0); + extra_widget->pack_start(*Gtk::manage(new Gtk::Label("")), true, true); + extra_widget->pack_start(*Gtk::manage(new Gtk::Label("Maximum Length (0 = unlimited): ")), false, false); + extra_widget->pack_start(*length_sb, false, false); + dialog.set_extra_widget(*extra_widget); + extra_widget->show_all(); + + const int result = dialog.run(); + + if (result == Gtk::RESPONSE_OK) { + const double length_dbl = length_sb->get_value_as_int(); + const Raul::TimeStamp length(_unit, length_dbl); + + SPtr<machina::Machine> machine = _engine->load_machine_midi( + dialog.get_filename(), 0.0, length); + + if (machine) { + dialog.hide(); + machine->reset(NULL, machine->time()); + _engine->driver()->set_machine(machine); + _canvas->clear(); + rebuild_canvas(); + } else { + Gtk::MessageDialog msg_dialog(dialog, "Error loading MIDI file", + false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + msg_dialog.run(); + } + } +} + +void +MachinaGUI::menu_export_midi() +{ + Gtk::FileChooserDialog dialog(*_main_window, "Export to a MIDI file", + Gtk::FILE_CHOOSER_ACTION_SAVE); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + + Gtk::FileFilter filt; + filt.add_pattern("*.mid"); + filt.set_name("MIDI Files"); + dialog.set_filter(filt); + + Gtk::HBox* extra_widget = Gtk::manage(new Gtk::HBox()); + Gtk::SpinButton* dur_sb = Gtk::manage(new Gtk::SpinButton()); + dur_sb->set_increments(1, 10); + dur_sb->set_range(0, INT_MAX); + dur_sb->set_value(0); + extra_widget->pack_start(*Gtk::manage(new Gtk::Label("")), true, true); + extra_widget->pack_start(*Gtk::manage(new Gtk::Label("Duration (beats): ")), false, false); + extra_widget->pack_start(*dur_sb, false, false); + dialog.set_extra_widget(*extra_widget); + extra_widget->show_all(); + + const int result = dialog.run(); + + if (result == Gtk::RESPONSE_OK) { + const double dur_dbl = dur_sb->get_value_as_int(); + const Raul::TimeStamp dur(_unit, dur_dbl); + _engine->export_midi(dialog.get_filename(), dur); + } +} + +void +MachinaGUI::menu_export_graphviz() +{ + Gtk::FileChooserDialog dialog(*_main_window, "Export to a GraphViz DOT file", + Gtk::FILE_CHOOSER_ACTION_SAVE); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + + const int result = dialog.run(); + + if (result == Gtk::RESPONSE_OK) + _canvas->export_dot(dialog.get_filename().c_str()); +} + +void +MachinaGUI::show_toolbar_toggled() +{ + if (_menu_view_toolbar->get_active()) + _toolbar->show(); + else + _toolbar->hide(); +} + +void +MachinaGUI::on_zoom_in() +{ + _canvas->set_font_size(_canvas->get_font_size() + 1.0); +} + +void +MachinaGUI::on_zoom_out() +{ + _canvas->set_font_size(_canvas->get_font_size() - 1.0); +} + +void +MachinaGUI::on_zoom_normal() +{ + _canvas->set_zoom(1.0); +} + +static void +show_node_label(GanvNode* node, void* data) +{ + bool show = *(bool*)data; + Ganv::Node* nodemm = Glib::wrap(node); + NodeView* const nv = dynamic_cast<NodeView*>(nodemm); + if (nv) { + nv->show_label(show); + } +} + +static void +show_edge_label(GanvEdge* edge, void* data) +{ + bool show = *(bool*)data; + Ganv::Edge* edgemm = Glib::wrap(edge); + EdgeView* const ev = dynamic_cast<EdgeView*>(edgemm); + if (ev) { + ev->show_label(show); + } +} + +void +MachinaGUI::show_labels_toggled() +{ + bool show = _menu_view_labels->get_active(); + + _canvas->for_each_node(show_node_label, &show); + _canvas->for_each_edge(show_edge_label, &show); +} + +void +MachinaGUI::menu_help_about() +{ + _about_window->set_transient_for(*_main_window); + _about_window->show(); +} + +void +MachinaGUI::menu_help_help() +{ + _help_dialog->set_transient_for(*_main_window); + _help_dialog->run(); + _help_dialog->hide(); +} + +void +MachinaGUI::stop_toggled() +{ + if (_stop_button->get_active()) { + const Driver::PlayState old_state = _engine->driver()->play_state(); + _engine->driver()->set_play_state(Driver::PlayState::STOPPED); + if (old_state == Driver::PlayState::RECORDING || + old_state == Driver::PlayState::STEP_RECORDING) { + rebuild_canvas(); + } + } +} + +void +MachinaGUI::play_toggled() +{ + if (_play_button->get_active()) { + const Driver::PlayState old_state = _engine->driver()->play_state(); + _engine->driver()->set_play_state(Driver::PlayState::PLAYING); + if (old_state == Driver::PlayState::RECORDING || + old_state == Driver::PlayState::STEP_RECORDING) { + rebuild_canvas(); + } + } +} + +void +MachinaGUI::record_toggled() +{ + if (_record_button->get_active()) { + _engine->driver()->set_play_state(Driver::PlayState::RECORDING); + } +} + +void +MachinaGUI::step_record_toggled() +{ + if (_step_record_button->get_active()) { + _engine->driver()->set_play_state(Driver::PlayState::STEP_RECORDING); + } +} + +void +MachinaGUI::chain_toggled() +{ + if (_chain_button->get_active()) { + _chain_mode = true; + } +} + +void +MachinaGUI::fan_toggled() +{ + if (_fan_button->get_active()) { + _chain_mode = false; + } +} + +void +MachinaGUI::on_new_object(SPtr<client::ClientObject> object) +{ + _canvas->on_new_object(object); +} + +void +MachinaGUI::on_erase_object(SPtr<client::ClientObject> object) +{ + _canvas->on_erase_object(object); +} + +} // namespace machina +} // namespace gui diff --git a/src/gui/MachinaGUI.hpp b/src/gui/MachinaGUI.hpp new file mode 100644 index 0000000..bb111a2 --- /dev/null +++ b/src/gui/MachinaGUI.hpp @@ -0,0 +1,190 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_GUI_HPP +#define MACHINA_GUI_HPP + +#include <string> + +#include <gtkmm.h> + +#include "raul/Maid.hpp" +#include "raul/TimeStamp.hpp" + +#include "machina/types.hpp" +#include "machina_config.h" + +using namespace std; + +namespace machina { + +class Machine; +class Engine; +class Evolver; +class Controller; + +namespace client { +class ClientModel; +class ClientObject; +} + +namespace gui { + +class MachinaCanvas; + +class MachinaGUI +{ +public: + explicit MachinaGUI(SPtr<machina::Engine> engine); + ~MachinaGUI(); + + SPtr<MachinaCanvas> canvas() { return _canvas; } + SPtr<machina::Engine> engine() { return _engine; } + SPtr<machina::Controller> controller() { return _controller; } + Forge& forge() { return _forge; } + SPtr<Raul::Maid> maid() { return _maid; } + Gtk::Window* window() { return _main_window; } + + void attach(); + void quit() { _main_window->hide(); } + + bool chain_mode() const { return _chain_mode; } + + double default_length() const { + return 1 / (double)_quantize_spinbutton->get_value(); + } + + inline void queue_refresh() { _refresh = true; } + + void on_new_object(SPtr<machina::client::ClientObject> object); + void on_erase_object(SPtr<machina::client::ClientObject> object); + + SPtr<machina::client::ClientModel> client_model() { + return _client_model; + } + +protected: + void menu_file_quit(); + void menu_file_open(); + void menu_file_save(); + void menu_file_save_as(); + void menu_import_midi(); + void menu_export_midi(); + void menu_export_graphviz(); + void on_zoom_in(); + void on_zoom_out(); + void on_zoom_normal(); + void show_toolbar_toggled(); + void show_labels_toggled(); + void menu_help_about(); + void menu_help_help(); + void arrange(); + void load_target_clicked(); + + void random_mutation(SPtr<machina::Machine> machine); + void mutate(SPtr<machina::Machine> machine, unsigned mutation); + void update_toolbar(); + void rebuild_canvas(); + + bool scrolled_window_event(GdkEvent* ev); + bool idle_callback(); + +#ifdef HAVE_EUGENE + void evolve_toggled(); + bool evolve_callback(); +#endif + + void stop_toggled(); + void play_toggled(); + void record_toggled(); + void step_record_toggled(); + + void chain_toggled(); + void fan_toggled(); + + void quantize_record_changed(); + void quantize_changed(); + void tempo_changed(); + + string _save_uri; + string _target_filename; + + Raul::TimeUnit _unit; + + SPtr<MachinaCanvas> _canvas; + SPtr<machina::Engine> _engine; + SPtr<machina::client::ClientModel> _client_model; + SPtr<machina::Controller> _controller; + + SPtr<Raul::Maid> _maid; + SPtr<machina::Evolver> _evolver; + + Forge _forge; + + Gtk::Main* _gtk_main; + + Gtk::Window* _main_window; + Gtk::Dialog* _help_dialog; + Gtk::AboutDialog* _about_window; + Gtk::Toolbar* _toolbar; + Gtk::MenuItem* _menu_file_open; + Gtk::MenuItem* _menu_file_save; + Gtk::MenuItem* _menu_file_save_as; + Gtk::MenuItem* _menu_file_quit; + Gtk::MenuItem* _menu_zoom_in; + Gtk::MenuItem* _menu_zoom_out; + Gtk::MenuItem* _menu_zoom_normal; + Gtk::MenuItem* _menu_view_arrange; + Gtk::MenuItem* _menu_import_midi; + Gtk::MenuItem* _menu_export_midi; + Gtk::MenuItem* _menu_export_graphviz; + Gtk::MenuItem* _menu_help_about; + Gtk::CheckMenuItem* _menu_view_labels; + Gtk::CheckMenuItem* _menu_view_toolbar; + Gtk::MenuItem* _menu_help_help; + Gtk::ScrolledWindow* _canvas_scrolledwindow; + Gtk::TextView* _status_text; + Gtk::Expander* _messages_expander; + Gtk::SpinButton* _bpm_spinbutton; + Gtk::CheckButton* _quantize_checkbutton; + Gtk::SpinButton* _quantize_spinbutton; + Gtk::ToggleToolButton* _stop_button; + Gtk::ToggleToolButton* _play_button; + Gtk::ToggleToolButton* _record_button; + Gtk::ToggleToolButton* _step_record_button; + Gtk::RadioButton* _chain_button; + Gtk::RadioButton* _fan_button; + Gtk::ToolButton* _load_target_button; + Gtk::Toolbar* _evolve_toolbar; + Gtk::ToggleToolButton* _evolve_button; + Gtk::ToolButton* _mutate_button; + Gtk::ToolButton* _compress_button; + Gtk::ToolButton* _add_node_button; + Gtk::ToolButton* _remove_node_button; + Gtk::ToolButton* _adjust_node_button; + Gtk::ToolButton* _add_edge_button; + Gtk::ToolButton* _remove_edge_button; + Gtk::ToolButton* _adjust_edge_button; + + bool _refresh; + bool _evolve; + bool _chain_mode; +}; + +} // namespace machina +} // namespace gui + +#endif // MACHINA_GUI_HPP diff --git a/src/gui/NodePropertiesWindow.cpp b/src/gui/NodePropertiesWindow.cpp new file mode 100644 index 0000000..f22eb57 --- /dev/null +++ b/src/gui/NodePropertiesWindow.cpp @@ -0,0 +1,136 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "client/ClientObject.hpp" +#include "machina/URIs.hpp" +#include "raul/TimeStamp.hpp" + +#include "MachinaGUI.hpp" +#include "NodePropertiesWindow.hpp" +#include "WidgetFactory.hpp" + +using namespace std; + +namespace machina { +namespace gui { + +NodePropertiesWindow* NodePropertiesWindow::_instance = NULL; + +NodePropertiesWindow::NodePropertiesWindow( + BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::Dialog(cobject) + , _gui(NULL) +{ + property_visible() = false; + + xml->get_widget("node_properties_note_spinbutton", _note_spinbutton); + xml->get_widget("node_properties_duration_spinbutton", _duration_spinbutton); + xml->get_widget("node_properties_apply_button", _apply_button); + xml->get_widget("node_properties_cancel_button", _cancel_button); + xml->get_widget("node_properties_ok_button", _ok_button); + + _apply_button->signal_clicked().connect( + sigc::mem_fun(this, &NodePropertiesWindow::apply_clicked)); + _cancel_button->signal_clicked().connect( + sigc::mem_fun(this, &NodePropertiesWindow::cancel_clicked)); + _ok_button->signal_clicked().connect( + sigc::mem_fun(this, &NodePropertiesWindow::ok_clicked)); +} + +NodePropertiesWindow::~NodePropertiesWindow() +{} + +void +NodePropertiesWindow::apply_clicked() +{ +#if 0 + const uint8_t note = _note_spinbutton->get_value(); + if (!_node->enter_action()) { + _node->set_enter_action(ActionFactory::note_on(note)); + _node->set_exit_action(ActionFactory::note_off(note)); + } else { + SPtr<MidiAction> action = dynamic_ptr_cast<MidiAction>(_node->enter_action()); + action->event()[1] = note; + action = dynamic_ptr_cast<MidiAction>(_node->exit_action()); + action->event()[1] = note; + } +#endif + _node->set(URIs::instance().machina_duration, + _gui->forge().make(float(_duration_spinbutton->get_value()))); +} + +void +NodePropertiesWindow::cancel_clicked() +{ + assert(this == _instance); + delete _instance; + _instance = NULL; +} + +void +NodePropertiesWindow::ok_clicked() +{ + apply_clicked(); + cancel_clicked(); +} + +void +NodePropertiesWindow::set_node(MachinaGUI* gui, + SPtr<machina::client::ClientObject> node) +{ + _gui = gui; + _node = node; + #if 0 + SPtr<MidiAction> enter_action = dynamic_ptr_cast<MidiAction>(node->enter_action()); + if (enter_action && ( enter_action->event_size() > 1) + && ( (enter_action->event()[0] & 0xF0) == 0x90) ) { + _note_spinbutton->set_value(enter_action->event()[1]); + _note_spinbutton->show(); + } else if (!enter_action) { + _note_spinbutton->set_value(60); + _note_spinbutton->show(); + } else { + _note_spinbutton->hide(); + } + #endif + _duration_spinbutton->set_value( + node->get(URIs::instance().machina_duration).get<float>()); +} + +void +NodePropertiesWindow::present(MachinaGUI* gui, + Gtk::Window* parent, + SPtr<machina::client::ClientObject> node) +{ + if (!_instance) { + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create(); + + xml->get_widget_derived("node_properties_dialog", _instance); + + if (parent) { + _instance->set_transient_for(*parent); + } + } + + _instance->set_node(gui, node); + _instance->show(); +} + +} // namespace machina +} // namespace gui diff --git a/src/gui/NodePropertiesWindow.hpp b/src/gui/NodePropertiesWindow.hpp new file mode 100644 index 0000000..a999004 --- /dev/null +++ b/src/gui/NodePropertiesWindow.hpp @@ -0,0 +1,67 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NODEPROPERTIESWINDOW_HPP +#define NODEPROPERTIESWINDOW_HPP + +#include <gtkmm.h> + +#include "machina/types.hpp" + +namespace machina { + +namespace client { class ClientObject; } + +namespace gui { + +class MachinaGUI; + +class NodePropertiesWindow : public Gtk::Dialog +{ +public: + NodePropertiesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + ~NodePropertiesWindow(); + + static void present(MachinaGUI* gui, + Gtk::Window* parent, + SPtr<machina::client::ClientObject> node); + +private: + void set_node(MachinaGUI* gui, + SPtr<machina::client::ClientObject> node); + + void apply_clicked(); + void cancel_clicked(); + void ok_clicked(); + + static NodePropertiesWindow* _instance; + + MachinaGUI* _gui; + SPtr<machina::client::ClientObject> _node; + + Gtk::SpinButton* _note_spinbutton; + Gtk::SpinButton* _duration_spinbutton; + Gtk::Button* _apply_button; + Gtk::Button* _cancel_button; + Gtk::Button* _ok_button; +}; + +} // namespace machina +} // namespace gui + +#endif // NODEPROPERTIESWINDOW_HPP diff --git a/src/gui/NodeView.cpp b/src/gui/NodeView.cpp new file mode 100644 index 0000000..a1b96e4 --- /dev/null +++ b/src/gui/NodeView.cpp @@ -0,0 +1,203 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "machina/Controller.hpp" +#include "machina/URIs.hpp" +#include "machina/types.hpp" + +#include "client/ClientModel.hpp" + +#include "MachinaCanvas.hpp" +#include "MachinaGUI.hpp" +#include "NodePropertiesWindow.hpp" +#include "NodeView.hpp" + +using namespace std; + +namespace machina { +namespace gui { + +NodeView::NodeView(Gtk::Window* window, + Ganv::Canvas& canvas, + SPtr<machina::client::ClientObject> node, + double x, + double y) + : Ganv::Circle(canvas, "", x, y) + , _window(window) + , _node(node) + , _default_border_color(get_border_color()) + , _default_fill_color(get_fill_color()) +{ + set_fit_label(false); + set_radius_ems(1.25); + + signal_event().connect(sigc::mem_fun(this, &NodeView::on_event)); + + MachinaCanvas* mcanvas = dynamic_cast<MachinaCanvas*>(&canvas); + if (is(mcanvas->app()->forge(), URIs::instance().machina_initial)) { + set_border_width(4.0); + set_is_source(true); + const uint8_t alpha[] = { 0xCE, 0xB1, 0 }; + set_label((const char*)alpha); + } + + node->signal_property.connect(sigc::mem_fun(this, &NodeView::on_property)); + + for (const auto& p : node->properties()) { + on_property(p.first, p.second); + } +} + +NodeView::~NodeView() +{ + _node->set_view(NULL); +} + +bool +NodeView::on_double_click(GdkEventButton*) +{ + MachinaCanvas* canvas = dynamic_cast<MachinaCanvas*>(this->canvas()); + NodePropertiesWindow::present(canvas->app(), _window, _node); + return true; +} + +bool +NodeView::is(Forge& forge, machina::URIInt key) +{ + const Atom& value = _node->get(key); + return value.type() == forge.Bool && value.get<int32_t>(); +} + +bool +NodeView::on_event(GdkEvent* event) +{ + MachinaCanvas* canvas = dynamic_cast<MachinaCanvas*>(this->canvas()); + Forge& forge = canvas->app()->forge(); + if (event->type == GDK_BUTTON_PRESS) { + if (event->button.state & GDK_CONTROL_MASK) { + if (event->button.button == 1) { + canvas->app()->controller()->set_property( + _node->id(), + URIs::instance().machina_selector, + forge.make(!is(forge, URIs::instance().machina_selector))); + return true; + } + } else { + return _signal_clicked.emit(&event->button); + } + } else if (event->type == GDK_2BUTTON_PRESS) { + return on_double_click(&event->button); + } + return false; +} + +static void +midi_note_name(uint8_t num, uint8_t buf[8]) +{ + static const char* notes = "CCDDEFFGGAAB"; + static const bool is_sharp[] = { 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0 }; + + const uint8_t octave = num / 12; + const uint8_t id = num - octave * 12; + const uint8_t sub[] = { 0xE2, 0x82, uint8_t(0x80 + octave) }; + const uint8_t sharp[] = { 0xE2, 0x99, 0xAF }; + + int b = 0; + buf[b++] = notes[id]; + if (is_sharp[id]) { + for (unsigned s = 0; s < sizeof(sharp); ++s) { + buf[b++] = sharp[s]; + } + } + for (unsigned s = 0; s < sizeof(sub); ++s) { + buf[b++] = sub[s]; + } + buf[b++] = 0; +} + +void +NodeView::show_label(bool show) +{ + if (show && _enter_action) { + Atom note_number = _enter_action->get( + URIs::instance().machina_note_number); + if (note_number.is_valid()) { + uint8_t buf[8]; + midi_note_name(note_number.get<int32_t>(), buf); + set_label((const char*)buf); + return; + } + } + + set_label(""); +} + +void +NodeView::on_property(machina::URIInt key, const Atom& value) +{ + static const uint32_t active_color = 0x408040FF; + static const uint32_t active_border_color = 0x00FF00FF; + + if (key == URIs::instance().machina_selector) { + if (value.get<int32_t>()) { + set_dash_length(4.0); + } else { + set_dash_length(0.0); + } + } else if (key == URIs::instance().machina_initial) { + set_border_width(value.get<int32_t>() ? 4.0 : 1.0); + set_is_source(value.get<int32_t>()); + } else if (key == URIs::instance().machina_active) { + if (value.get<int32_t>()) { + if (get_fill_color() != active_color) { + set_fill_color(active_color); + set_border_color(active_border_color); + } + } else if (get_fill_color() == active_color) { + set_default_colors(); + } + } else if (key == URIs::instance().machina_enter_action) { + const uint64_t action_id = value.get<int32_t>(); + MachinaCanvas* canvas = dynamic_cast<MachinaCanvas*>(this->canvas()); + _enter_action_connection.disconnect(); + _enter_action = canvas->app()->client_model()->find(action_id); + if (_enter_action) { + _enter_action_connection = _enter_action->signal_property.connect( + sigc::mem_fun(this, &NodeView::on_action_property)); + for (auto i : _enter_action->properties()) { + on_action_property(i.first, i.second); + } + } + } +} + +void +NodeView::on_action_property(machina::URIInt key, const Atom& value) +{ + if (key == URIs::instance().machina_note_number) { + show_label(true); + } +} + +void +NodeView::set_default_colors() +{ + set_fill_color(_default_fill_color); + set_border_color(_default_border_color); +} + +} // namespace machina +} // namespace gui diff --git a/src/gui/NodeView.hpp b/src/gui/NodeView.hpp new file mode 100644 index 0000000..6e0681c --- /dev/null +++ b/src/gui/NodeView.hpp @@ -0,0 +1,76 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef MACHINA_NODEVIEW_HPP +#define MACHINA_NODEVIEW_HPP + +#include "ganv/Circle.hpp" + +#include "client/ClientObject.hpp" + +#include "machina/types.hpp" + +namespace machina { +namespace gui { + +class NodeView + : public Ganv::Circle + , public machina::client::ClientObject::View +{ +public: + NodeView(Gtk::Window* window, + Canvas& canvas, + SPtr<machina::client::ClientObject> node, + double x, + double y); + + ~NodeView(); + + SPtr<machina::client::ClientObject> node() { return _node; } + + void show_label(bool show); + + void update_state(bool show_labels); + + void set_default_colors(); + + sigc::signal<bool, GdkEventButton*>& signal_clicked() { + return _signal_clicked; + } + +private: + bool on_event(GdkEvent* ev); + bool on_double_click(GdkEventButton* ev); + void on_property(machina::URIInt key, const Atom& value); + void on_action_property(machina::URIInt key, const Atom& value); + + bool is(Forge& forge, machina::URIInt key); + + Gtk::Window* _window; + SPtr<machina::client::ClientObject> _node; + uint32_t _default_border_color; + uint32_t _default_fill_color; + + SPtr<machina::client::ClientObject> _enter_action; + sigc::connection _enter_action_connection; + + sigc::signal<bool, GdkEventButton*> _signal_clicked; +}; + +} // namespace machina +} // namespace gui + +#endif // MACHINA_NODEVIEW_HPP diff --git a/src/gui/WidgetFactory.hpp b/src/gui/WidgetFactory.hpp new file mode 100644 index 0000000..08118dc --- /dev/null +++ b/src/gui/WidgetFactory.hpp @@ -0,0 +1,65 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <fstream> +#include <iostream> +#include <string> + +#include <gtkmm.h> + +#include "machina_config.h" + +namespace machina { +namespace gui { + +class WidgetFactory +{ +public: + static Glib::RefPtr<Gtk::Builder> create() { + Glib::RefPtr<Gtk::Builder> xml; + + // Check for the .ui file in current directory + std::string ui_filename = "./machina.ui"; + std::ifstream fs(ui_filename.c_str()); + if (fs.fail()) { + // didn't find it, check MACHINA_DATA_DIR + fs.clear(); + ui_filename = MACHINA_DATA_DIR; + ui_filename += "/machina.ui"; + + fs.open(ui_filename.c_str()); + if (fs.fail()) { + std::cerr << "No machina.ui in current directory or " + << MACHINA_DATA_DIR << "." << std::endl; + exit(EXIT_FAILURE); + } + fs.close(); + } + + try { + xml = Gtk::Builder::create_from_file(ui_filename); + } catch (const Gtk::BuilderError& ex) { + std::cerr << ex.what() << std::endl; + throw ex; + } + + return xml; + } + +}; + +} // namespace machina +} // namespace gui diff --git a/src/gui/machina.gladep b/src/gui/machina.gladep new file mode 100644 index 0000000..c27832f --- /dev/null +++ b/src/gui/machina.gladep @@ -0,0 +1,9 @@ +<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*--> +<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd"> + +<glade-project> + <name>Machina</name> + <program_name>machina</program_name> + <language>C++</language> + <gnome_support>FALSE</gnome_support> +</glade-project> diff --git a/src/gui/machina.svg b/src/gui/machina.svg new file mode 100644 index 0000000..39f7be0 --- /dev/null +++ b/src/gui/machina.svg @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.45pre1" + width="256" + height="256" + version="1.0" + sodipodi:docbase="/home/dave/code/lad/machina/src/gui" + sodipodi:docname="machina-icon.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:modified="true"> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs5" /> + <sodipodi:namedview + inkscape:window-height="771" + inkscape:window-width="1183" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + guidetolerance="10.0" + gridtolerance="10.0" + objecttolerance="10.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" + inkscape:zoom="1.4142136" + inkscape:cx="310.87907" + inkscape:cy="40.263308" + inkscape:window-x="293" + inkscape:window-y="52" + inkscape:current-layer="svg2" + width="256px" + height="256px" /> + <path + style="fill:#000000" + id="path2189" + d="M 9.0278551,221.40628 C 22.572047,206.65805 31.269455,188.04472 39.259152,169.8919 C 51.644822,140.58208 56.041288,109.13092 58.849953,77.700978 C 59.962909,56.867589 61.443535,35.864275 59.495794,15.031194 C 63.699947,13.959444 67.938586,10.617016 72.108252,11.815945 C 77.429885,13.346107 71.833885,22.901101 72.179608,28.42755 C 73.082013,42.852664 75.819667,56.648771 78.944682,70.727057 C 85.857753,99.135712 98.546656,125.08352 122.5541,142.3916 C 149.02904,155.16169 159.76491,132.36389 173.41824,112.97967 C 189.89026,87.286307 201.67487,59.107084 212.79124,30.80732 C 238.81023,-0.42720544 213.47911,64.070958 211.76716,70.33904 C 202.71922,108.04829 200.75405,146.89462 201.60034,185.52025 C 202.59785,202.30805 203.03716,219.51338 208.40658,235.60545 C 209.82998,239.98783 211.97855,244.0493 214.19967,248.06447 L 201.80123,255.17231 C 199.63995,251.03059 197.23968,246.9206 196.00991,242.38503 C 191.48082,225.58243 192.01187,207.89788 191.55783,190.64995 C 191.61755,152.05096 193.33386,113.23415 201.28376,75.349455 C 205.86472,56.717664 207.3011,28.642568 227.34088,20.082503 C 229.12544,19.320222 225.62336,23.562881 224.76461,25.303071 C 211.8352,53.436232 199.91298,82.147117 183.57547,108.55509 C 167.9424,131.6754 143.08989,165.0319 111.95557,149.10157 C 88.367319,131.04024 76.047527,104.6256 68.72284,76.231983 C 66.537387,67.180737 52.831088,26.99333 59.36089,18.303914 C 62.551525,14.058035 67.78841,11.838043 72.002169,8.6051067 C 71.542368,29.947073 70.679719,51.279977 69.135855,72.573834 C 66.386515,103.98497 62.160375,135.44301 50.178851,164.87632 C 42.654028,182.74324 34.458191,200.83743 22.235303,216.04826 L 9.0278551,221.40628 z " /> + <path + style="fill:#000000" + id="path2191" + d="M 177.4044,227.49356 C 186.73664,226.83386 194.85497,230.46984 202.63542,235.17331 C 208.34182,239.12994 205.55279,237.07748 211.00405,241.32859 L 199.24569,248.73751 C 193.86468,244.57093 196.63478,246.5644 190.93236,242.7613 C 183.22589,238.37114 175.24676,235.06127 166.16497,235.78874 L 177.4044,227.49356 z " /> + <path + style="fill:#000000" + id="path2193" + d="M 202.904,253.28805 C 206.25526,244.218 211.55226,236.16787 216.96397,228.23361 C 217.73013,227.17184 218.49629,226.11007 219.26244,225.0483 L 232.24972,219.44712 C 231.4278,220.51566 230.60588,221.58419 229.78396,222.65273 C 224.30649,230.41756 219.07262,238.3307 215.72758,247.29568 L 202.904,253.28805 z " /> +</svg> diff --git a/src/gui/machina.ui b/src/gui/machina.ui new file mode 100644 index 0000000..89404fc --- /dev/null +++ b/src/gui/machina.ui @@ -0,0 +1,1128 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkRadioAction" id="play_action"> + <property name="stock_id">gtk-media-play</property> + <property name="draw_as_radio">True</property> + <property name="group">record_action</property> + </object> + <object class="GtkRadioAction" id="step_record_action"> + <property name="label" translatable="yes">Step record</property> + <property name="stock_id">gtk-add</property> + <property name="draw_as_radio">True</property> + <property name="group">record_action</property> + </object> + <object class="GtkRadioAction" id="stop_action"> + <property name="stock_id">gtk-media-stop</property> + <property name="draw_as_radio">True</property> + <property name="group">record_action</property> + </object> + <object class="GtkAboutDialog" id="about_win"> + <property name="can_focus">False</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">normal</property> + <property name="program_name">Machina</property> + <property name="copyright" translatable="yes">© 2013 David Robillard <http://drobilla.net></property> + <property name="comments" translatable="yes">A MIDI sequencer based on + probabilistic finite-state automata</property> + <property name="website">http://drobilla.net/software/machina</property> + <property name="license" translatable="yes">Machina 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. + +Machina 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 Machina; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +</property> + <property name="authors">David Robillard <d@drobilla.net></property> + <property name="translator_credits" translatable="yes" comments="TRANSLATORS: Replace this string with your names, one name per line.">translator-credits</property> + <property name="logo">machina.svg</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="can_focus">False</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkAdjustment" id="bpm_adjustment"> + <property name="lower">1</property> + <property name="upper">480</property> + <property name="value">120</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="duration_adjustment"> + <property name="upper">64</property> + <property name="value">0.25</property> + <property name="step_increment">1</property> + <property name="page_increment">4</property> + </object> + <object class="GtkDialog" id="help_dialog"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Machina Help</property> + <property name="resizable">False</property> + <property name="window_position">center-on-parent</property> + <property name="icon_name">gtk-help</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="closebutton1"> + <property name="label">gtk-close</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes"><big><b>Nodes</b></big> +Nodes represent notes, which have a pitch and duration. +When a node is highlighted green, it is playing. + +Play begins at the initial node α. Whenever all nodes become +inactive or stop is pressed, play returns to the initial node. + +Nodes with dashed borders are selectors. Only one successor is +played after a selector, i.e. only one outgoing arc is traversed. + + • Right click the canvas to create a new node + • Middle click a node to learn a MIDI note + • Double click a node to edit its properties + • Ctrl+Left click a node to make it a selector + +<big><b>Arcs</b></big> +When a node is finished playing, play travels along outgoing +arcs, depending on their probability. The colour of an arc +indicates its probability, where green is high and red is low. + + • Ctrl+Left click an arc to decrease its probability + • Ctrl+Right click an arc to increase its probability + +<big><b>Recording</b></big> +A machine can be built by recording MIDI input. To record, press +the record button and play some MIDI notes. To finish recording, +press stop or play and the new nodes will be added to the machine. + +Normal recording inserts delay nodes to reproduce the timing of +the input. To avoid this, use step recording which directly connects +nodes to their successors with no delays in-between. + +<big><b>Connecting</b></big> +Connecting nodes is based on the selection. If there is a selection, +clicking a node will connect the selection to that node. + +There are two modes: chain and fan. In chain mode, the selection is +moved to the clicked node for quickly connecting long chains of nodes. +In fan mode, the selection is unchanged for quickly connecting the +selection to many nodes.</property> + <property name="use_markup">True</property> + <property name="justify">fill</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">8</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-7">closebutton1</action-widget> + </action-widgets> + </object> + <object class="GtkAdjustment" id="length_adjustment"> + <property name="lower">1</property> + <property name="upper">256</property> + <property name="value">8</property> + <property name="step_increment">1</property> + <property name="page_increment">8</property> + </object> + <object class="GtkWindow" id="machina_win"> + <property name="can_focus">False</property> + <property name="border_width">1</property> + <property name="title" translatable="yes">Machina</property> + <property name="default_width">640</property> + <property name="default_height">480</property> + <property name="icon">machina.svg</property> + <child> + <object class="GtkVBox" id="main_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuBar" id="menubar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="file_menu"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="file_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="open_menuitem"> + <property name="label">gtk-open</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_open_session_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="save_menuitem"> + <property name="label">gtk-save</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_save_session_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="save_as_menuitem"> + <property name="label">gtk-save-as</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_save_session_as_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="import_midi_menuitem"> + <property name="label">_Import MIDI...</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="I" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_learn_midi_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="export_midi_menuitem"> + <property name="label">_Export MIDI...</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="E" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_export_midi_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="export_graphviz_menuitem"> + <property name="label">Export _GraphViz...</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="d" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_export_graphviz_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="quit_menuitem"> + <property name="label">gtk-quit</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_quit1_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="view_menu"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_View</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="view_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckMenuItem" id="view_labels_menuitem"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Labels</property> + <property name="use_underline">True</property> + <accelerator key="L" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_view_edge_labels_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="view_toolbar_menuitem"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Toolbar</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="T" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_toolbar2_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem3"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="zoom_in_menuitem"> + <property name="label">gtk-zoom-in</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="plus" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <accelerator key="equal" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="zoom_out_menuitem"> + <property name="label">gtk-zoom-out</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="minus" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="zoom_normal_menuitem"> + <property name="label">gtk-zoom-100</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="0" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem1"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="arrange_menuitem"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Arrange</property> + <property name="use_underline">True</property> + <accelerator key="g" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="help_menu"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Help</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="help_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="help_help_menuitem"> + <property name="label">gtk-help</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_help_about_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="help_about_menuitem"> + <property name="label">gtk-about</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_about1_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkToolbar" id="toolbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="toolbar_style">icons</property> + <property name="show_arrow">False</property> + <property name="icon_size">2</property> + <property name="icon_size_set">True</property> + <child> + <object class="GtkToggleToolButton" id="stop_but"> + <property name="related_action">stop_action</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Stop</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="play_but"> + <property name="related_action">play_action</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Play</property> + <property name="active">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="record_but"> + <property name="related_action">record_action</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Record</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="step_record_but"> + <property name="related_action">step_record_action</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="tooltip_text" translatable="yes">Step record</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separatortoolitem1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="toolitem1"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="bpm_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">Playback tempo</property> + <property name="max_length">3</property> + <property name="invisible_char">•</property> + <property name="width_chars">3</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="adjustment">bpm_adjustment</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"> BPM</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separatortoolitem2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="toolitem3"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">4</property> + <child> + <object class="GtkCheckButton" id="quantize_checkbutton"> + <property name="label" translatable="yes">Quantize 1/</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Quantize recorded notes</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="quantize_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">Note type for quantization</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="adjustment">quantize_adjustment</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separatortoolitem3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="toolitem2"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">4</property> + <child> + <object class="GtkRadioButton" id="chain_but"> + <property name="label" translatable="yes">Chain</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Move selection to head node after connection</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="fan_but"> + <property name="label" translatable="yes">Fan</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Keep selection on tail node after connection</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <property name="group">chain_but</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToolbar" id="evolve_toolbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="toolbar_style">icons</property> + <property name="show_arrow">False</property> + <property name="icon_size">2</property> + <property name="icon_size_set">True</property> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="load_target_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="stock_id">gtk-open</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="evolve_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Evolve machine (towards target MIDI)</property> + <property name="stock_id">gtk-execute</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="mutate_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="stock_id">gtk-dialog-warning</property> + <accelerator key="m" signal="clicked" modifiers="GDK_CONTROL_MASK"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="compress_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="stock_id">gtk-convert</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="add_node_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Add Node</property> + <property name="stock_id">gtk-new</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="remove_node_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Delete Node</property> + <property name="stock_id">gtk-delete</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="adjust_node_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Adjust Node</property> + <property name="stock_id">gtk-edit</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="add_edge_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Add Edge</property> + <property name="stock_id">gtk-connect</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="remove_edge_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Remove Edge</property> + <property name="stock_id">gtk-disconnect</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="adjust_edge_but"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Adjust Edge</property> + <property name="stock_id">gtk-select-color</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="canvas_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">2</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkAdjustment" id="note_num_adjustment"> + <property name="upper">127</property> + <property name="value">64</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkRadioAction" id="record_action"> + <property name="stock_id">gtk-media-record</property> + <property name="draw_as_radio">True</property> + </object> + <object class="GtkDialog" id="node_properties_dialog"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">dialog1</property> + <property name="resizable">False</property> + <property name="icon">machina.svg</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <property name="skip_pager_hint">True</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">8</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="node_properties_apply_button"> + <property name="label">gtk-apply</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="node_properties_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="node_properties_ok_button"> + <property name="label">gtk-ok</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <property name="column_spacing">4</property> + <property name="row_spacing">8</property> + <child> + <object class="GtkSpinButton" id="node_properties_duration_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="adjustment">duration_adjustment</property> + <property name="digits">3</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Duration: </property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Note: </property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="node_properties_note_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="adjustment">note_num_adjustment</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-10">node_properties_apply_button</action-widget> + <action-widget response="-6">node_properties_cancel_button</action-widget> + <action-widget response="-5">node_properties_ok_button</action-widget> + </action-widgets> + </object> + <object class="GtkAdjustment" id="quantize_adjustment"> + <property name="lower">1</property> + <property name="upper">256</property> + <property name="value">1</property> + <property name="step_increment">1</property> + <property name="page_increment">8</property> + </object> +</interface> diff --git a/src/gui/main.cpp b/src/gui/main.cpp new file mode 100644 index 0000000..547966f --- /dev/null +++ b/src/gui/main.cpp @@ -0,0 +1,100 @@ +/* + This file is part of Machina. + Copyright 2007-2014 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <signal.h> + +#include <iostream> +#include <string> + +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "machina/Engine.hpp" +#include "machina/Loader.hpp" +#include "machina/Machine.hpp" +#include "machina/URIs.hpp" +#include "machina_config.h" +#include "sord/sordmm.hpp" + +#include "MachinaGUI.hpp" + +using namespace std; +using namespace machina; + +int +main(int argc, char** argv) +{ + Glib::thread_init(); + machina::URIs::init(); + + Sord::World rdf_world; + rdf_world.add_prefix("", MACHINA_NS); + rdf_world.add_prefix("midi", LV2_MIDI_PREFIX); + + Forge forge; + SPtr<machina::Machine> machine; + + Raul::TimeUnit beats(TimeUnit::BEATS, MACHINA_PPQN); + + // Load machine, if given + if (argc >= 2) { + const string filename = argv[1]; + const string ext = filename.substr(filename.length() - 4); + + if (ext == ".ttl") { + cout << "Loading machine from " << filename << endl; + machine = Loader(forge, rdf_world).load(filename); + + } else if (ext == ".mid") { + cout << "Building machine from MIDI file " << filename << endl; + + double q = 0.0; + if (argc >= 3) { + q = strtod(argv[2], NULL); + cout << "Quantization: " << q << endl; + } + + machine = Loader(forge, rdf_world).load_midi( + filename, q, Raul::TimeDuration(beats, 0, 0)); + } + + if (!machine) { + cerr << "Failed to load machine, exiting" << std::endl; + return 1; + } + } + + + if (!machine) { + machine = SPtr<Machine>(new Machine(beats)); + } + + // Create driver + SPtr<Driver> driver(Engine::new_driver(forge, "jack", machine)); + if (!driver) { + cerr << "warning: Failed to create Jack driver, using SMF" << endl; + driver = SPtr<Driver>(Engine::new_driver(forge, "smf", machine)); + } + + SPtr<Engine> engine(new Engine(forge, driver, rdf_world)); + + Gtk::Main app(argc, argv); + + driver->activate(); + gui::MachinaGUI gui(engine); + + app.run(*gui.window()); + + return 0; +} diff --git a/src/gui/wscript b/src/gui/wscript new file mode 100644 index 0000000..0eef312 --- /dev/null +++ b/src/gui/wscript @@ -0,0 +1,42 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + obj = bld(features = 'cxx cxxshlib') + obj.source = ''' + EdgeView.cpp + MachinaCanvas.cpp + MachinaGUI.cpp + NodePropertiesWindow.cpp + NodeView.cpp + ''' + + obj.includes = ['.', '..', '../..', '../engine'] + obj.export_includes = ['.'] + obj.name = 'libmachina_gui' + obj.target = 'machina_gui' + obj.use = 'libmachina_engine libmachina_client' + autowaf.use_lib(bld, obj, ''' + GANV + GLADEMM + GLIBMM + GNOMECANVAS + GTKMM + RAUL + SORD + SIGCPP + EUGENE + LV2 + ''') + + # GUI runtime files + bld.install_files('${DATADIR}/machina', 'machina.ui') + bld.install_files('${DATADIR}/machina', 'machina.svg') + + # Executable + obj = bld(features = 'cxx cxxprogram') + obj.target = 'machina_gui' + obj.source = 'main.cpp' + obj.includes = ['.', '../..', '../engine'] + obj.use = 'libmachina_engine libmachina_gui' + autowaf.use_lib(bld, obj, 'GTHREAD GLIBMM GTKMM SORD RAUL MACHINA EUGENE GANV LV2') diff --git a/src/machina.cpp b/src/machina.cpp new file mode 100644 index 0000000..32eab05 --- /dev/null +++ b/src/machina.cpp @@ -0,0 +1,86 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "machina_config.h" + +#include <iostream> +#include <signal.h> +#include "machina/Action.hpp" +#include "machina/Edge.hpp" +#include "machina/Engine.hpp" +#include "machina/Machine.hpp" +#include "machina/MidiAction.hpp" +#ifdef HAVE_EUGENE +# include "machina/Problem.hpp" +#endif + +#include "JackDriver.hpp" + +using namespace std; +using namespace machina; + +bool quit = false; + +void +catch_int(int) +{ + signal(SIGINT, catch_int); + signal(SIGTERM, catch_int); + + std::cout << "Interrupted" << std::endl; + + quit = true; +} + +int +main(int argc, char** argv) +{ + if (argc != 2) { + cout << "Usage: " << argv[0] << " FILE" << endl; + return -1; + } + + if (!Glib::thread_supported()) { + Glib::thread_init(); + } + + SPtr<JackDriver> driver(new JackDriver()); + + Sord::World rdf_world; + Engine engine(driver, rdf_world); + + /* FIXME: Would be nice if this could take URIs on the cmd line + char* uri = (char*)calloc(6 + strlen(argv[1]), sizeof(char)); + strcpy(uri, "file:"); + strcat(uri, argv[1]); + engine.load_machine(uri); + free(uri); + */ + engine.load_machine(argv[1]); + + driver->attach("machina"); + + signal(SIGINT, catch_int); + signal(SIGTERM, catch_int); + + while (!quit) { + sleep(1); + } + + driver->detach(); + + return 0; +} diff --git a/src/midi2machina.cpp b/src/midi2machina.cpp new file mode 100644 index 0000000..fdff2f7 --- /dev/null +++ b/src/midi2machina.cpp @@ -0,0 +1,81 @@ +/* + This file is part of Machina. + Copyright 2007-2013 David Robillard <http://drobilla.net> + + Machina 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 3 of the License, or any later version. + + Machina 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 Machina. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <iostream> +#include <signal.h> +#include "sord/sordmm.hpp" +#include "machina/Engine.hpp" +#include "machina/Machine.hpp" +#include "machina/Action.hpp" +#include "machina/Edge.hpp" +#include "machina/SMFDriver.hpp" +#include "machina/MidiAction.hpp" + +using namespace std; +using namespace machina; + +bool quit = false; + +void +catch_int(int) +{ + signal(SIGINT, catch_int); + signal(SIGTERM, catch_int); + + std::cout << "Interrupted" << std::endl; + + quit = true; +} + +int +main(int argc, char** argv) +{ + if (argc != 3) { + cout << "Usage: midi2machina QUANTIZATION FILE" << endl; + cout << "Specify quantization in beats, e.g. 1.0, or 0 for none" + << endl; + return -1; + } + + SPtr<SMFDriver> driver(new SMFDriver()); + + SPtr<Machine> machine = driver->learn(argv[2], strtof(argv[1], NULL)); + + if (!machine) { + cout << "Failed to load MIDI file." << endl; + return -1; + } + + /* + string out_filename = argv[1]; + if (out_filename.find_last_of("/") != string::npos) + out_filename = out_filename.substr(out_filename.find_last_of("/")+1); + + const string::size_type last_dot = out_filename.find_last_of("."); + if (last_dot != string::npos && last_dot != 0) + out_filename = out_filename.substr(0, last_dot); + out_filename += ".machina"; + + cout << "Writing output to " << out_filename << endl; + */ + + Sord::World world; + Sord::Model model(world); + machine->write_state(model); + model.serialise_to_file_handle(stdout); + + return 0; +} |