Add Sampler classed and the vidi-player.py and vidi-sampler.py programs
authorAntonio Ospite <ao2@ao2.it>
Mon, 5 Dec 2016 17:48:21 +0000 (18:48 +0100)
committerAntonio Ospite <ao2@ao2.it>
Mon, 5 Dec 2016 22:56:54 +0000 (23:56 +0100)
vidi-player.py plays midi files in real-time, instead of waiting for the
timeline creation like vidi-timeline.py does.

vidi-sampler.py plays samples according to the notes produced by a midi
controller.

README.md
vidi-player.py [new file with mode: 0755]
vidi-sampler.py [new file with mode: 0755]
vidi/Sampler.py [new file with mode: 0755]
vidi/__init__.py

index 7f8c3db..694a989 100644 (file)
--- a/README.md
+++ b/README.md
@@ -13,6 +13,9 @@ vidi-timeline allows to create more easily videos like these:
 Examples of use
 ===============
 
+vidi-timeline.py
+----------------
+
 Create a synthetinc VideoFont:
 
     $ ./create_test_videofont.py videofont
@@ -33,6 +36,24 @@ Render the timeline to a video file:
     $ ges-launch-1.0 --load Beyer_008.xges --outputuri Beyer_008.webm --format="video/webm:video/x-vp8:audio/x-vorbis"
 
 
+vidi-player.py
+--------------
+
+Play a midi file in real time (if the CPU and the disk can keep up):
+
+    $ ./vidi-player.py examples/Beyer\ Op.\ 101\ -\ Exercise\ 008.midi videofont/
+
+
+vidi-sampler.py
+---------------
+
+Play samples from the note hit on a midi controller:
+
+    $ xset -r && vkeybd && xset r on &
+    $ ./vidi-sampler.py 'Virtual Keyboard' videofont/
+    $ fg
+
+
 Similar projects
 ================
 
diff --git a/vidi-player.py b/vidi-player.py
new file mode 100755 (executable)
index 0000000..8fcabc4
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+#
+# vidi-player - play video samples interactively from a midi file
+#
+# 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 sys
+import vidi
+
+
+def usage():
+    print("usage: %s <midi_file> <videofont_directory>"
+          % os.path.basename(sys.argv[0]))
+
+
+def main():
+    if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"]:
+        usage()
+        return 0
+
+    if len(sys.argv) < 3:
+        usage()
+        return 1
+
+    if not os.path.isdir(sys.argv[2]):
+        sys.stderr.write("The second argument must be the path of the videofont directory\n")
+        usage()
+        return 1
+
+    video_font_path = os.path.realpath(sys.argv[2])
+    sampler = vidi.FileSampler(video_font_path, sys.argv[1])
+    sampler.play()
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/vidi-sampler.py b/vidi-sampler.py
new file mode 100755 (executable)
index 0000000..4797b71
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+#
+# vidi-sampler - play video samples interactively from a midi device
+#
+# 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 sys
+import vidi
+
+
+def usage():
+    print("usage: %s <midi_input_device> <videofont_directory>"
+          % os.path.basename(sys.argv[0]))
+
+
+def main():
+    if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"]:
+        usage()
+        return 0
+
+    if len(sys.argv) < 3:
+        usage()
+        return 1
+
+    if not os.path.isdir(sys.argv[2]):
+        sys.stderr.write("The second argument must be the path of the videofont directory\n")
+        usage()
+        return 1
+
+    video_font_path = os.path.realpath(sys.argv[2])
+    sampler = vidi.DeviceSampler(video_font_path, sys.argv[1])
+    sampler.play()
+
+
+if __name__ == '__main__':
+    sys.exit(main())
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)
index 53d457d..d89030c 100755 (executable)
@@ -3,4 +3,5 @@
 from .MidiUtils import is_note, is_note_on, is_note_off, check_overlapping_notes
 from .Note import SpnNote, MidiNote, PIANO_88_KEYS_RANGE
 from .Player import Player
+from .Sampler import DeviceSampler, FileSampler
 from .Timeline import Timeline