Add Sampler classed and the vidi-player.py and vidi-sampler.py programs
[vidi-player.git] / vidi / Sampler.py
diff --git a/vidi/Sampler.py b/vidi/Sampler.py
new file mode 100755 (executable)
index 0000000..645e39e
--- /dev/null
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+#
+# Sampler - play video samples interactively according to midi messages
+#
+# Copyright (C) 2016  Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import mido
+
+import gi
+gi.require_version('Gst', '1.0')
+from gi.repository import Gst
+Gst.init(None)
+
+from gi.repository import GObject
+GObject.threads_init()
+
+import vidi
+
+class Sampler(vidi.Player):
+    def __init__(self, videofont_path):
+        playbin = Gst.ElementFactory.make("playbin", "player")
+        vidi.Player.__init__(self, playbin)
+
+        self.videofont_path = videofont_path
+        self.last_note = None
+
+        self.uri = Gst.filename_to_uri(self.get_sample_path(self.last_note))
+        self.pipeline.set_property("uri", self.uri)
+        self.pipeline.connect("about-to-finish", self.on_about_to_finish)
+
+    def on_about_to_finish(self, element):
+        element.set_property("uri", self.uri)
+
+    def get_sample_path(self, note):
+        if note is None:
+            sample_name = "rest"
+        else:
+            sample_name = vidi.MidiNote(note).name
+
+        return "%s/sample_%s.webm" % (self.videofont_path, sample_name)
+
+    def midi_message_cb(self, msg):
+        if vidi.is_note(msg):
+            new_note = msg.note
+
+            # The logic is as follows:
+            #  - Always play a new note on;
+            #  - only play silence if the note off is the same one that was
+            #    played last;
+            #  - otherwise do nothing.
+            if vidi.is_note_on(msg):
+                note = new_note
+            elif vidi.is_note_off(msg) \
+                    and new_note == self.last_note:
+                note = None
+            else:
+                note = self.last_note
+
+            if note != self.last_note:
+                self.last_note = note
+                sample_path = self.get_sample_path(note)
+                if os.path.exists(sample_path):
+                    self.switch(sample_path)
+                else:
+                    print("Warning: videofont is missing sample '%s'" % sample_path)
+
+    def switch(self, sample_path):
+        print("Next: %s" % sample_path)
+        self.uri = Gst.filename_to_uri(sample_path)
+        seek_event = Gst.Event.new_seek(1.0,
+                                        Gst.Format.TIME,
+                                        Gst.SeekFlags.FLUSH,
+                                        Gst.SeekType.END, -1,
+                                        Gst.SeekType.NONE, 0)
+        self.pipeline.send_event(seek_event)
+
+
+class DeviceSampler(Sampler):
+    def __init__(self, videofont_path, midi_source_name):
+        Sampler.__init__(self, videofont_path)
+
+        print(mido)
+        midi_source = mido.open_input(midi_source_name)
+        midi_source.callback = self.midi_message_cb
+
+
+class FileSampler(Sampler):
+    def __init__(self, videofont_path, midi_file_name):
+        Sampler.__init__(self, videofont_path)
+
+        self.midi_file = mido.MidiFile(midi_file_name)
+        overlapping_notes = vidi.check_overlapping_notes(self.midi_file)
+        if overlapping_notes:
+            print("Sorry, supporting only midi file with no overlapping notes on channel 0")
+            return None
+
+    def play(self):
+        def next_midi_msg(midi_file_generator):
+            try:
+                msg = midi_file_generator.__next__()
+                if vidi.is_note(msg) and msg.channel == 0:
+                    self.midi_message_cb(msg)
+                return True
+            except StopIteration:
+                self.stop()
+                return False
+
+
+        GObject.timeout_add(1, next_midi_msg, self.midi_file.play())
+        Sampler.play(self)