diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-17 00:31:00 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-17 01:15:59 +0200 |
commit | 22514f3ae6d4e19537ae5ab6bdb5bc9f99a64f47 (patch) | |
tree | 7e32523681e0e3f8232a620a96cc3f047fd7ecc0 /helpers | |
parent | 1bf9e4940feed9d47a4bc36acdd71a47704d82cf (diff) | |
download | MusicSampler-22514f3ae6d4e19537ae5ab6bdb5bc9f99a64f47.tar.gz MusicSampler-22514f3ae6d4e19537ae5ab6bdb5bc9f99a64f47.tar.zst MusicSampler-22514f3ae6d4e19537ae5ab6bdb5bc9f99a64f47.zip |
Add a common mixer
Diffstat (limited to 'helpers')
-rw-r--r-- | helpers/mapping.py | 2 | ||||
-rw-r--r-- | helpers/mixer.py | 50 | ||||
-rw-r--r-- | helpers/music_file.py | 41 |
3 files changed, 66 insertions, 27 deletions
diff --git a/helpers/mapping.py b/helpers/mapping.py index 84845fc..60c7691 100644 --- a/helpers/mapping.py +++ b/helpers/mapping.py | |||
@@ -8,6 +8,7 @@ import yaml | |||
8 | import sys | 8 | import sys |
9 | 9 | ||
10 | from .music_file import * | 10 | from .music_file import * |
11 | from .mixer import Mixer | ||
11 | from . import yml_file,gain | 12 | from . import yml_file,gain |
12 | 13 | ||
13 | class Mapping(RelativeLayout): | 14 | class Mapping(RelativeLayout): |
@@ -23,6 +24,7 @@ class Mapping(RelativeLayout): | |||
23 | self.running = [] | 24 | self.running = [] |
24 | Clock.schedule_interval(self.not_all_keys_ready, 1) | 25 | Clock.schedule_interval(self.not_all_keys_ready, 1) |
25 | 26 | ||
27 | self.mixer = Mixer() | ||
26 | 28 | ||
27 | @property | 29 | @property |
28 | def master_gain(self): | 30 | def master_gain(self): |
diff --git a/helpers/mixer.py b/helpers/mixer.py new file mode 100644 index 0000000..9e8179a --- /dev/null +++ b/helpers/mixer.py | |||
@@ -0,0 +1,50 @@ | |||
1 | import sounddevice as sd | ||
2 | import audioop | ||
3 | import time | ||
4 | |||
5 | frame_rate = 44100 | ||
6 | channels = 2 | ||
7 | sample_width = 2 | ||
8 | |||
9 | class Mixer: | ||
10 | def __init__(self): | ||
11 | self.stream = sd.RawOutputStream(samplerate=frame_rate, | ||
12 | channels=channels, | ||
13 | dtype='int' + str(8*sample_width), # FIXME: ? | ||
14 | latency="high", | ||
15 | blocksize=5000, | ||
16 | callback=self.play_callback, | ||
17 | ) | ||
18 | self.open_files = [] | ||
19 | |||
20 | def add_file(self, music_file): | ||
21 | if music_file not in self.open_files: | ||
22 | self.open_files.append(music_file) | ||
23 | self.start() | ||
24 | |||
25 | def remove_file(self, music_file): | ||
26 | self.open_files.remove(music_file) | ||
27 | if len(self.open_files) == 0: | ||
28 | self.stop() | ||
29 | |||
30 | def stop(self): | ||
31 | self.stream.stop() | ||
32 | |||
33 | def start(self): | ||
34 | self.stream.start() | ||
35 | |||
36 | def play_callback(self, out_data, frame_count, time_info, status_flags): | ||
37 | out_data_length = len(out_data) | ||
38 | empty_data = b"\0" * out_data_length | ||
39 | data = b"\0" * out_data_length | ||
40 | |||
41 | for open_file in self.open_files: | ||
42 | file_data = open_file.play_callback(out_data_length, frame_count) | ||
43 | |||
44 | if data == empty_data: | ||
45 | data = file_data | ||
46 | elif file_data != empty_data: | ||
47 | data = audioop.add(data, file_data, sample_width) | ||
48 | |||
49 | out_data[:] = data | ||
50 | |||
diff --git a/helpers/music_file.py b/helpers/music_file.py index ebe458b..6da547b 100644 --- a/helpers/music_file.py +++ b/helpers/music_file.py | |||
@@ -3,7 +3,6 @@ import pydub | |||
3 | import time | 3 | import time |
4 | from transitions.extensions import HierarchicalMachine as Machine | 4 | from transitions.extensions import HierarchicalMachine as Machine |
5 | 5 | ||
6 | import sounddevice as sd | ||
7 | import os.path | 6 | import os.path |
8 | 7 | ||
9 | from .lock import Lock | 8 | from .lock import Lock |
@@ -27,7 +26,7 @@ class MusicFile(Machine): | |||
27 | { 'trigger': 'pause', 'source': 'loaded_playing', 'dest': 'loaded_paused'}, | 26 | { 'trigger': 'pause', 'source': 'loaded_playing', 'dest': 'loaded_paused'}, |
28 | { 'trigger': 'unpause', 'source': 'loaded_paused', 'dest': 'loaded_playing'}, | 27 | { 'trigger': 'unpause', 'source': 'loaded_paused', 'dest': 'loaded_playing'}, |
29 | { 'trigger': 'stop_playing', 'source': ['loaded_playing','loaded_paused'], 'dest': 'loaded_stopping'}, | 28 | { 'trigger': 'stop_playing', 'source': ['loaded_playing','loaded_paused'], 'dest': 'loaded_stopping'}, |
30 | { 'trigger': 'stopped', 'source': 'loaded_stopping', 'dest': 'loaded_stopped'} | 29 | { 'trigger': 'stopped', 'source': 'loaded_stopping', 'dest': 'loaded_stopped', 'after': 'trigger_stopped_events'} |
31 | ] | 30 | ] |
32 | 31 | ||
33 | Machine.__init__(self, states=states, transitions=transitions, initial='initial') | 32 | Machine.__init__(self, states=states, transitions=transitions, initial='initial') |
@@ -35,7 +34,6 @@ class MusicFile(Machine): | |||
35 | self.volume = 100 | 34 | self.volume = 100 |
36 | self.mapping = mapping | 35 | self.mapping = mapping |
37 | self.filename = filename | 36 | self.filename = filename |
38 | self.stream = None | ||
39 | self.name = name or filename | 37 | self.name = name or filename |
40 | self.audio_segment = None | 38 | self.audio_segment = None |
41 | self.audio_segment_frame_width = 0 | 39 | self.audio_segment_frame_width = 0 |
@@ -50,7 +48,7 @@ class MusicFile(Machine): | |||
50 | try: | 48 | try: |
51 | print("Loading « {} »".format(self.name)) | 49 | print("Loading « {} »".format(self.name)) |
52 | db_gain = gain(self.volume_factor * 100) | 50 | db_gain = gain(self.volume_factor * 100) |
53 | self.audio_segment = pydub.AudioSegment.from_file(self.filename).set_frame_rate(44100).apply_gain(db_gain) | 51 | self.audio_segment = pydub.AudioSegment.from_file(self.filename).set_frame_rate(44100).set_channels(2).set_sample_width(2).apply_gain(db_gain) |
54 | self.audio_segment_frame_width = self.audio_segment.frame_width | 52 | self.audio_segment_frame_width = self.audio_segment.frame_width |
55 | self.sound_duration = self.audio_segment.duration_seconds | 53 | self.sound_duration = self.audio_segment.duration_seconds |
56 | except Exception as e: | 54 | except Exception as e: |
@@ -94,26 +92,10 @@ class MusicFile(Machine): | |||
94 | else: | 92 | else: |
95 | self.a_s_with_effect = None | 93 | self.a_s_with_effect = None |
96 | 94 | ||
97 | self.before_loaded_playing() | ||
98 | self.start_playing() | 95 | self.start_playing() |
99 | 96 | ||
100 | def before_loaded_playing(self): | ||
101 | with self.music_lock: | ||
102 | segment = self.current_audio_segment | ||
103 | |||
104 | self.stream = sd.RawOutputStream(samplerate=segment.frame_rate, | ||
105 | channels=segment.channels, | ||
106 | dtype='int' + str(8*segment.sample_width), # FIXME: ? | ||
107 | latency=1., | ||
108 | callback=self.play_callback, | ||
109 | finished_callback=self.finished_callback | ||
110 | ) | ||
111 | |||
112 | def on_enter_loaded_playing(self): | 97 | def on_enter_loaded_playing(self): |
113 | self.stream.start() | 98 | self.mapping.mixer.add_file(self) |
114 | |||
115 | def on_enter_loaded_paused(self): | ||
116 | self.stream.stop() | ||
117 | 99 | ||
118 | def finished_callback(self): | 100 | def finished_callback(self): |
119 | if self.is_loaded_playing(): | 101 | if self.is_loaded_playing(): |
@@ -121,10 +103,14 @@ class MusicFile(Machine): | |||
121 | if self.is_loaded_stopping(): | 103 | if self.is_loaded_stopping(): |
122 | self.stopped() | 104 | self.stopped() |
123 | 105 | ||
124 | def on_enter_loaded_stopped(self): | 106 | def trigger_stopped_events(self): |
107 | self.mapping.mixer.remove_file(self) | ||
125 | self.wait_event.set() | 108 | self.wait_event.set() |
126 | 109 | ||
127 | def play_callback(self, out_data, frame_count, time_info, status_flags): | 110 | def play_callback(self, out_data_length, frame_count): |
111 | if self.is_loaded_paused(): | ||
112 | return b'\0' * out_data_length | ||
113 | |||
128 | with self.music_lock: | 114 | with self.music_lock: |
129 | [data, nb_frames] = self.get_next_sample(frame_count) | 115 | [data, nb_frames] = self.get_next_sample(frame_count) |
130 | if nb_frames < frame_count: | 116 | if nb_frames < frame_count: |
@@ -135,9 +121,9 @@ class MusicFile(Machine): | |||
135 | data += new_data | 121 | data += new_data |
136 | nb_frames += new_nb_frames | 122 | nb_frames += new_nb_frames |
137 | elif nb_frames == 0: | 123 | elif nb_frames == 0: |
138 | raise sd.CallbackStop | 124 | threading.Thread(name = "MSFinishedCallback", target=self.finished_callback).start() |
139 | 125 | ||
140 | out_data[:] = data.ljust(len(out_data), b'\0') | 126 | return data.ljust(out_data_length, b'\0') |
141 | 127 | ||
142 | def get_next_sample(self, frame_count): | 128 | def get_next_sample(self, frame_count): |
143 | fw = self.audio_segment_frame_width | 129 | fw = self.audio_segment_frame_width |
@@ -189,9 +175,10 @@ class MusicFile(Machine): | |||
189 | 175 | ||
190 | # FIXME: stop fade_out puis seek -5 -> on abandonne le fade ? (cf | 176 | # FIXME: stop fade_out puis seek -5 -> on abandonne le fade ? (cf |
191 | # commentaire dans fonction seek | 177 | # commentaire dans fonction seek |
178 | new_audio_segment = self.current_audio_segment[:ms + ms_fo].fade_out(ms_fo) | ||
192 | with self.music_lock: | 179 | with self.music_lock: |
193 | self.current_audio_segment = self.current_audio_segment[:ms + ms_fo].fade_out(ms_fo) | 180 | self.current_audio_segment = new_audio_segment |
194 | self.stop_playing() | 181 | self.stop_playing() |
195 | if wait: | 182 | if wait: |
196 | self.wait_end() | 183 | self.wait_end() |
197 | else: | 184 | else: |