From 4cd0a64f760b6138a62141c087da38a1b787ac5b Mon Sep 17 00:00:00 2001 From: Antonio Ospite Date: Mon, 5 Dec 2016 18:48:21 +0100 Subject: [PATCH] Add Sampler classed and the vidi-player.py and vidi-sampler.py programs 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 | 21 ++++++++++ vidi-player.py | 51 +++++++++++++++++++++++ vidi-sampler.py | 51 +++++++++++++++++++++++ vidi/Sampler.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ vidi/__init__.py | 1 + 5 files changed, 248 insertions(+) create mode 100755 vidi-player.py create mode 100755 vidi-sampler.py create mode 100755 vidi/Sampler.py diff --git a/README.md b/README.md index 7f8c3db..694a989 100644 --- 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 index 0000000..8fcabc4 --- /dev/null +++ b/vidi-player.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# vidi-player - play video samples interactively from a midi file +# +# Copyright (C) 2016 Antonio Ospite +# +# 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 . + + +import os +import sys +import vidi + + +def usage(): + print("usage: %s " + % 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 index 0000000..4797b71 --- /dev/null +++ b/vidi-sampler.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# vidi-sampler - play video samples interactively from a midi device +# +# Copyright (C) 2016 Antonio Ospite +# +# 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 . + + +import os +import sys +import vidi + + +def usage(): + print("usage: %s " + % 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 index 0000000..645e39e --- /dev/null +++ b/vidi/Sampler.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# +# Sampler - play video samples interactively according to midi messages +# +# Copyright (C) 2016 Antonio Ospite +# +# 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 . + +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) diff --git a/vidi/__init__.py b/vidi/__init__.py index 53d457d..d89030c 100755 --- a/vidi/__init__.py +++ b/vidi/__init__.py @@ -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 -- 2.1.4