Add Sampler classed and the vidi-player.py and vidi-sampler.py programs
[vidi-player.git] / vidi / Sampler.py
1 #!/usr/bin/env python3
2 #
3 # Sampler - play video samples interactively according to midi messages
4 #
5 # Copyright (C) 2016  Antonio Ospite <ao2@ao2.it>
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import os
21 import mido
22
23 import gi
24 gi.require_version('Gst', '1.0')
25 from gi.repository import Gst
26 Gst.init(None)
27
28 from gi.repository import GObject
29 GObject.threads_init()
30
31 import vidi
32
33 class Sampler(vidi.Player):
34     def __init__(self, videofont_path):
35         playbin = Gst.ElementFactory.make("playbin", "player")
36         vidi.Player.__init__(self, playbin)
37
38         self.videofont_path = videofont_path
39         self.last_note = None
40
41         self.uri = Gst.filename_to_uri(self.get_sample_path(self.last_note))
42         self.pipeline.set_property("uri", self.uri)
43         self.pipeline.connect("about-to-finish", self.on_about_to_finish)
44
45     def on_about_to_finish(self, element):
46         element.set_property("uri", self.uri)
47
48     def get_sample_path(self, note):
49         if note is None:
50             sample_name = "rest"
51         else:
52             sample_name = vidi.MidiNote(note).name
53
54         return "%s/sample_%s.webm" % (self.videofont_path, sample_name)
55
56     def midi_message_cb(self, msg):
57         if vidi.is_note(msg):
58             new_note = msg.note
59
60             # The logic is as follows:
61             #  - Always play a new note on;
62             #  - only play silence if the note off is the same one that was
63             #    played last;
64             #  - otherwise do nothing.
65             if vidi.is_note_on(msg):
66                 note = new_note
67             elif vidi.is_note_off(msg) \
68                     and new_note == self.last_note:
69                 note = None
70             else:
71                 note = self.last_note
72
73             if note != self.last_note:
74                 self.last_note = note
75                 sample_path = self.get_sample_path(note)
76                 if os.path.exists(sample_path):
77                     self.switch(sample_path)
78                 else:
79                     print("Warning: videofont is missing sample '%s'" % sample_path)
80
81     def switch(self, sample_path):
82         print("Next: %s" % sample_path)
83         self.uri = Gst.filename_to_uri(sample_path)
84         seek_event = Gst.Event.new_seek(1.0,
85                                         Gst.Format.TIME,
86                                         Gst.SeekFlags.FLUSH,
87                                         Gst.SeekType.END, -1,
88                                         Gst.SeekType.NONE, 0)
89         self.pipeline.send_event(seek_event)
90
91
92 class DeviceSampler(Sampler):
93     def __init__(self, videofont_path, midi_source_name):
94         Sampler.__init__(self, videofont_path)
95
96         print(mido)
97         midi_source = mido.open_input(midi_source_name)
98         midi_source.callback = self.midi_message_cb
99
100
101 class FileSampler(Sampler):
102     def __init__(self, videofont_path, midi_file_name):
103         Sampler.__init__(self, videofont_path)
104
105         self.midi_file = mido.MidiFile(midi_file_name)
106         overlapping_notes = vidi.check_overlapping_notes(self.midi_file)
107         if overlapping_notes:
108             print("Sorry, supporting only midi file with no overlapping notes on channel 0")
109             return None
110
111     def play(self):
112         def next_midi_msg(midi_file_generator):
113             try:
114                 msg = midi_file_generator.__next__()
115                 if vidi.is_note(msg) and msg.channel == 0:
116                     self.midi_message_cb(msg)
117                 return True
118             except StopIteration:
119                 self.stop()
120                 return False
121
122
123         GObject.timeout_add(1, next_midi_msg, self.midi_file.play())
124         Sampler.play(self)