diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-14 22:18:51 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-14 22:18:51 +0200 |
commit | 1b4b78f5b6df7182ac066fcc26a7b4f0e8586a47 (patch) | |
tree | ef2bddec7b9f09c614012ac6ee2588cd732242ee | |
parent | 71715c049145a074b0f2b8d90c8c8c47830323c3 (diff) | |
download | MusicSampler-1b4b78f5b6df7182ac066fcc26a7b4f0e8586a47.tar.gz MusicSampler-1b4b78f5b6df7182ac066fcc26a7b4f0e8586a47.tar.zst MusicSampler-1b4b78f5b6df7182ac066fcc26a7b4f0e8586a47.zip |
Some new features:
- gain function moved to helpers/__init__
- cleanup some unused functions
- stop can now wait for fade_out to finish before returning
- volume can be incremented
- master volume
-rw-r--r-- | helpers/__init__.py | 8 | ||||
-rw-r--r-- | helpers/action.py | 56 | ||||
-rw-r--r-- | helpers/mapping.py | 24 | ||||
-rw-r--r-- | helpers/music_file.py | 38 | ||||
-rw-r--r-- | music_sampler.kv | 10 |
5 files changed, 89 insertions, 47 deletions
diff --git a/helpers/__init__.py b/helpers/__init__.py index 3b97f2f..2339b9b 100644 --- a/helpers/__init__.py +++ b/helpers/__init__.py | |||
@@ -2,6 +2,7 @@ | |||
2 | import argparse | 2 | import argparse |
3 | import sys | 3 | import sys |
4 | import os | 4 | import os |
5 | import math | ||
5 | 6 | ||
6 | class Config: | 7 | class Config: |
7 | def __init__(self, **kwargs): | 8 | def __init__(self, **kwargs): |
@@ -60,3 +61,10 @@ def duration_to_min_sec(duration): | |||
60 | return "{:2}:{:0>2}".format(minutes, seconds) | 61 | return "{:2}:{:0>2}".format(minutes, seconds) |
61 | else: | 62 | else: |
62 | return "{}:{:0>2}".format(minutes, seconds) | 63 | return "{}:{:0>2}".format(minutes, seconds) |
64 | |||
65 | def gain(volume, old_volume = None): | ||
66 | if old_volume is None: | ||
67 | return 20 * math.log10(volume / 100) | ||
68 | else: | ||
69 | return [20 * math.log10(max(volume, 0.1) / max(old_volume, 0.1)), max(volume, 0)] | ||
70 | |||
diff --git a/helpers/action.py b/helpers/action.py index 9145629..28afcee 100644 --- a/helpers/action.py +++ b/helpers/action.py | |||
@@ -20,6 +20,7 @@ class Action: | |||
20 | raise Exception("Unknown action {}".format(action)) | 20 | raise Exception("Unknown action {}".format(action)) |
21 | 21 | ||
22 | self.key = key | 22 | self.key = key |
23 | self.mapping = key.parent | ||
23 | self.arguments = kwargs | 24 | self.arguments = kwargs |
24 | self.sleep_event = None | 25 | self.sleep_event = None |
25 | 26 | ||
@@ -49,7 +50,7 @@ class Action: | |||
49 | if music is not None: | 50 | if music is not None: |
50 | return [music] | 51 | return [music] |
51 | else: | 52 | else: |
52 | return self.key.parent.open_files.values() | 53 | return self.mapping.open_files.values() |
53 | 54 | ||
54 | def pause(self, music = None, **kwargs): | 55 | def pause(self, music = None, **kwargs): |
55 | for music in self.music_list(music): | 56 | for music in self.music_list(music): |
@@ -72,20 +73,25 @@ class Action: | |||
72 | if not music.is_not_stopped(): | 73 | if not music.is_not_stopped(): |
73 | music.play(volume = volume, fade_in = fade_in, start_at = start_at) | 74 | music.play(volume = volume, fade_in = fade_in, start_at = start_at) |
74 | 75 | ||
75 | def stop(self, music = None, fade_out = 0, **kwargs): | 76 | def stop(self, music = None, fade_out = 0, wait = False, **kwargs): |
77 | previous = None | ||
76 | for music in self.music_list(music): | 78 | for music in self.music_list(music): |
77 | if music.is_loaded_paused() or music.is_loaded_playing(): | 79 | if music.is_loaded_paused() or music.is_loaded_playing(): |
78 | music.stop(fade_out = fade_out) | 80 | if previous is not None: |
81 | previous.stop(fade_out = fade_out) | ||
82 | previous = music | ||
83 | |||
84 | if previous is not None: | ||
85 | previous.stop(fade_out = fade_out, wait = wait) | ||
79 | 86 | ||
80 | def stop_all_actions(self, **kwargs): | 87 | def stop_all_actions(self, **kwargs): |
81 | self.key.parent.stop_all_running() | 88 | self.mapping.stop_all_running() |
82 | 89 | ||
83 | def volume(self, music = None, value = 100, **kwargs): | 90 | def volume(self, music = None, value = 100, add = False, **kwargs): |
84 | if music is not None: | 91 | if music is not None: |
85 | music.set_volume(value) | 92 | music.set_volume(value, add = add) |
86 | else: | 93 | else: |
87 | # FIXME: todo | 94 | self.mapping.set_master_volume(value, add = add) |
88 | pass | ||
89 | 95 | ||
90 | def wait(self, duration = 0, music = None, **kwargs): | 96 | def wait(self, duration = 0, music = None, **kwargs): |
91 | self.sleep_event = threading.Event() | 97 | self.sleep_event = threading.Event() |
@@ -133,26 +139,34 @@ class Action: | |||
133 | 139 | ||
134 | return message | 140 | return message |
135 | 141 | ||
136 | def stop_print(self, music = None, fade_out = 0, **kwargs): | 142 | def stop_print(self, music = None, fade_out = 0, wait = False, **kwargs): |
143 | message = "stopping " | ||
137 | if music is not None: | 144 | if music is not None: |
138 | if fade_out == 0: | 145 | message += "music « {} »".format(music.name) |
139 | return "stopping music « {} »".format(music.name) | ||
140 | else: | ||
141 | return "stopping music « {} » with {}s fadeout".format(music.name, fade_out) | ||
142 | else: | 146 | else: |
143 | if fade_out == 0: | 147 | message += "all musics" |
144 | return "stopping all musics" | 148 | |
145 | else: | 149 | if fade_out > 0: |
146 | return "stopping all musics with {}s fadeout".format(fade_out) | 150 | message += " with {}s fadeout".format(fade_out) |
151 | if wait: | ||
152 | message += " (waiting the end of fadeout)" | ||
153 | |||
154 | return message | ||
147 | 155 | ||
148 | def stop_all_actions_print(self, **kwargs): | 156 | def stop_all_actions_print(self, **kwargs): |
149 | return "stopping all actions" | 157 | return "stopping all actions" |
150 | 158 | ||
151 | def volume_print(self, music = None, value = 100, **kwargs): | 159 | def volume_print(self, music = None, value = 100, add = False, **kwargs): |
152 | if music is not None: | 160 | if add: |
153 | return "setting volume of « {} » to {}%".format(music.name, value) | 161 | if music is not None: |
162 | return "{:+d}% to volume of « {} »".format(value, music.name) | ||
163 | else: | ||
164 | return "{:+d}% to volume".format(value) | ||
154 | else: | 165 | else: |
155 | return "setting volume to {}%".format(value) | 166 | if music is not None: |
167 | return "setting volume of « {} » to {}%".format(music.name, value) | ||
168 | else: | ||
169 | return "setting volume to {}%".format(value) | ||
156 | 170 | ||
157 | def wait_print(self, duration = 0, music = None, **kwargs): | 171 | def wait_print(self, duration = 0, music = None, **kwargs): |
158 | if music is None: | 172 | if music is None: |
diff --git a/helpers/mapping.py b/helpers/mapping.py index ea9d075..d9b7ba0 100644 --- a/helpers/mapping.py +++ b/helpers/mapping.py | |||
@@ -8,10 +8,11 @@ import yaml | |||
8 | import sys | 8 | import sys |
9 | 9 | ||
10 | from .music_file import * | 10 | from .music_file import * |
11 | from . import yml_file | 11 | from . import yml_file,gain |
12 | 12 | ||
13 | class Mapping(RelativeLayout): | 13 | class Mapping(RelativeLayout): |
14 | expected_keys = NumericProperty(0) | 14 | expected_keys = NumericProperty(0) |
15 | master_volume = NumericProperty(100) | ||
15 | ready_color = ListProperty([1, 165/255, 0, 1]) | 16 | ready_color = ListProperty([1, 165/255, 0, 1]) |
16 | 17 | ||
17 | def __init__(self, **kwargs): | 18 | def __init__(self, **kwargs): |
@@ -23,6 +24,15 @@ class Mapping(RelativeLayout): | |||
23 | Clock.schedule_interval(self.not_all_keys_ready, 1) | 24 | Clock.schedule_interval(self.not_all_keys_ready, 1) |
24 | 25 | ||
25 | 26 | ||
27 | @property | ||
28 | def master_gain(self): | ||
29 | return gain(self.master_volume) | ||
30 | |||
31 | def set_master_volume(self, value, add = False): | ||
32 | [db_gain, self.master_volume] = gain(value + int(add) * self.master_volume, self.master_volume) | ||
33 | for music in self.open_files.values(): | ||
34 | music.set_gain(db_gain) | ||
35 | |||
26 | def _keyboard_closed(self): | 36 | def _keyboard_closed(self): |
27 | self._keyboard.unbind(on_key_down=self._on_keyboard_down) | 37 | self._keyboard.unbind(on_key_down=self._on_keyboard_down) |
28 | self._keyboard = None | 38 | self._keyboard = None |
@@ -45,16 +55,6 @@ class Mapping(RelativeLayout): | |||
45 | return self.ids["Key_" + str(key_code[0])] | 55 | return self.ids["Key_" + str(key_code[0])] |
46 | return None | 56 | return None |
47 | 57 | ||
48 | def find_by_unicode(self, key_sym): | ||
49 | for key in self.children: | ||
50 | if not type(key).__name__ == "Key": | ||
51 | continue | ||
52 | print(key.key_sym, key_sym) | ||
53 | if key.key_sym == key_sym: | ||
54 | print("found") | ||
55 | return key | ||
56 | return None | ||
57 | |||
58 | def not_all_keys_ready(self, dt): | 58 | def not_all_keys_ready(self, dt): |
59 | for key in self.children: | 59 | for key in self.children: |
60 | if not type(key).__name__ == "Key": | 60 | if not type(key).__name__ == "Key": |
@@ -139,9 +139,11 @@ class Mapping(RelativeLayout): | |||
139 | if filename in config['music_properties']: | 139 | if filename in config['music_properties']: |
140 | seen_files[filename] = MusicFile( | 140 | seen_files[filename] = MusicFile( |
141 | filename, | 141 | filename, |
142 | self, | ||
142 | **config['music_properties'][filename]) | 143 | **config['music_properties'][filename]) |
143 | else: | 144 | else: |
144 | seen_files[filename] = MusicFile( | 145 | seen_files[filename] = MusicFile( |
146 | self, | ||
145 | filename) | 147 | filename) |
146 | 148 | ||
147 | if filename not in key_properties[mapped_key]['files']: | 149 | if filename not in key_properties[mapped_key]['files']: |
diff --git a/helpers/music_file.py b/helpers/music_file.py index e6a340d..6a28d62 100644 --- a/helpers/music_file.py +++ b/helpers/music_file.py | |||
@@ -1,6 +1,5 @@ | |||
1 | import threading | 1 | import threading |
2 | import pydub | 2 | import pydub |
3 | import math | ||
4 | import time | 3 | import time |
5 | from transitions.extensions import HierarchicalMachine as Machine | 4 | from transitions.extensions import HierarchicalMachine as Machine |
6 | 5 | ||
@@ -9,12 +8,14 @@ import sounddevice as sd | |||
9 | import os.path | 8 | import os.path |
10 | 9 | ||
11 | from .lock import Lock | 10 | from .lock import Lock |
11 | from . import gain | ||
12 | |||
12 | file_lock = Lock("file") | 13 | file_lock = Lock("file") |
13 | 14 | ||
14 | pyaudio = pa.PyAudio() | 15 | pyaudio = pa.PyAudio() |
15 | 16 | ||
16 | class MusicFile(Machine): | 17 | class MusicFile(Machine): |
17 | def __init__(self, filename, name = None, gain = 1): | 18 | def __init__(self, filename, mapping, name = None, gain = 1): |
18 | states = [ | 19 | states = [ |
19 | 'initial', | 20 | 'initial', |
20 | 'loading', | 21 | 'loading', |
@@ -34,11 +35,13 @@ class MusicFile(Machine): | |||
34 | 35 | ||
35 | Machine.__init__(self, states=states, transitions=transitions, initial='initial') | 36 | Machine.__init__(self, states=states, transitions=transitions, initial='initial') |
36 | 37 | ||
38 | self.volume = 100 | ||
39 | self.mapping = mapping | ||
37 | self.filename = filename | 40 | self.filename = filename |
38 | self.stream = None | 41 | self.stream = None |
39 | self.name = name or filename | 42 | self.name = name or filename |
40 | self.audio_segment = None | 43 | self.audio_segment = None |
41 | self.gain = gain | 44 | self.volume_factor = gain |
42 | self.music_lock = Lock("music__" + filename) | 45 | self.music_lock = Lock("music__" + filename) |
43 | self.wait_event = threading.Event() | 46 | self.wait_event = threading.Event() |
44 | 47 | ||
@@ -48,8 +51,8 @@ class MusicFile(Machine): | |||
48 | with file_lock: | 51 | with file_lock: |
49 | try: | 52 | try: |
50 | print("Loading « {} »".format(self.name)) | 53 | print("Loading « {} »".format(self.name)) |
51 | volume_factor = 20 * math.log10(self.gain) | 54 | db_gain = gain(self.volume_factor * 100) |
52 | self.audio_segment = pydub.AudioSegment.from_file(self.filename).set_frame_rate(44100).apply_gain(volume_factor) | 55 | self.audio_segment = pydub.AudioSegment.from_file(self.filename).set_frame_rate(44100).apply_gain(db_gain) |
53 | self.sound_duration = self.audio_segment.duration_seconds | 56 | self.sound_duration = self.audio_segment.duration_seconds |
54 | except Exception as e: | 57 | except Exception as e: |
55 | print("failed to load « {} »: {}".format(self.name, e)) | 58 | print("failed to load « {} »: {}".format(self.name, e)) |
@@ -76,11 +79,13 @@ class MusicFile(Machine): | |||
76 | return 0 | 79 | return 0 |
77 | 80 | ||
78 | def play(self, fade_in = 0, volume = 100, start_at = 0): | 81 | def play(self, fade_in = 0, volume = 100, start_at = 0): |
79 | self.db_gain = self.volume_to_gain(volume) | 82 | db_gain = gain(volume) + self.mapping.master_gain |
83 | self.volume = volume | ||
84 | |||
80 | ms = int(start_at * 1000) | 85 | ms = int(start_at * 1000) |
81 | ms_fi = max(1, int(fade_in * 1000)) | 86 | ms_fi = max(1, int(fade_in * 1000)) |
82 | with self.music_lock: | 87 | with self.music_lock: |
83 | self.current_audio_segment = (self.audio_segment + self.db_gain).fade(from_gain=-120, duration=ms_fi, start=ms) | 88 | self.current_audio_segment = (self.audio_segment + db_gain).fade(from_gain=-120, duration=ms_fi, start=ms) |
84 | self.before_loaded_playing(initial_frame = int(start_at * self.audio_segment.frame_rate)) | 89 | self.before_loaded_playing(initial_frame = int(start_at * self.audio_segment.frame_rate)) |
85 | self.start_playing() | 90 | self.start_playing() |
86 | 91 | ||
@@ -124,7 +129,7 @@ class MusicFile(Machine): | |||
124 | 129 | ||
125 | out_data[:] = audio_segment.ljust(len(out_data), b'\0') | 130 | out_data[:] = audio_segment.ljust(len(out_data), b'\0') |
126 | 131 | ||
127 | def stop(self, fade_out = 0): | 132 | def stop(self, fade_out = 0, wait = False): |
128 | if self.is_loaded_playing(): | 133 | if self.is_loaded_playing(): |
129 | ms = int(self.sound_position * 1000) | 134 | ms = int(self.sound_position * 1000) |
130 | ms_fo = max(1, int(fade_out * 1000)) | 135 | ms_fo = max(1, int(fade_out * 1000)) |
@@ -132,22 +137,25 @@ class MusicFile(Machine): | |||
132 | with self.music_lock: | 137 | with self.music_lock: |
133 | self.current_audio_segment = self.current_audio_segment[:ms + ms_fo].fade_out(ms_fo) | 138 | self.current_audio_segment = self.current_audio_segment[:ms + ms_fo].fade_out(ms_fo) |
134 | self.stop_playing() | 139 | self.stop_playing() |
140 | if wait: | ||
141 | self.wait_end() | ||
135 | else: | 142 | else: |
136 | self.stop_playing() | 143 | self.stop_playing() |
137 | self.stopped() | 144 | self.stopped() |
138 | 145 | ||
139 | def set_volume(self, value): | 146 | def set_gain(self, db_gain): |
140 | if self.is_loaded_stopped(): | 147 | if not self.is_not_stopped(): |
141 | return | 148 | return |
142 | 149 | ||
143 | db_gain = self.volume_to_gain(value) | 150 | new_audio_segment = self.current_audio_segment + db_gain |
144 | new_audio_segment = self.current_audio_segment + (db_gain - self.db_gain) | 151 | |
145 | self.db_gain = db_gain | ||
146 | with self.music_lock: | 152 | with self.music_lock: |
147 | self.current_audio_segment = new_audio_segment | 153 | self.current_audio_segment = new_audio_segment |
148 | 154 | ||
149 | def volume_to_gain(self, volume): | 155 | def set_volume(self, value, add = False): |
150 | return 20 * math.log10(max(volume, 0.0001) / 100) | 156 | [db_gain, self.volume] = gain(value + int(add) * self.volume, self.volume) |
157 | |||
158 | self.set_gain(db_gain) | ||
151 | 159 | ||
152 | def wait_end(self): | 160 | def wait_end(self): |
153 | self.wait_event.clear() | 161 | self.wait_event.clear() |
diff --git a/music_sampler.kv b/music_sampler.kv index 84c40b5..3232956 100644 --- a/music_sampler.kv +++ b/music_sampler.kv | |||
@@ -329,6 +329,16 @@ | |||
329 | Ellipse: | 329 | Ellipse: |
330 | pos: self.width - self.key_size / 2, self.height - self.key_size /2 | 330 | pos: self.width - self.key_size / 2, self.height - self.key_size /2 |
331 | size: self.key_size / 3, self.key_size / 3 | 331 | size: self.key_size / 3, self.key_size / 3 |
332 | Label: | ||
333 | font_name: h.path() + "fonts/Ubuntu-Regular.ttf" | ||
334 | font_size: math.ceil(2 * math.sqrt(self.parent.key_size or 10)) | ||
335 | color: 0, 0, 0, 1 | ||
336 | text: "volume: {}%".format(self.parent.master_volume) | ||
337 | valign: "top" | ||
338 | size_hint: None, None | ||
339 | size: self.texture_size[0], self.texture_size[1] | ||
340 | x: self.parent.width - self.width - 2 * self.parent.key_size / 3 | ||
341 | center_y: self.parent.height - self.height | ||
332 | Key: | 342 | Key: |
333 | id: Key_27 | 343 | id: Key_27 |
334 | key_code: 27 | 344 | key_code: 27 |