aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2016-07-14 22:18:51 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2016-07-14 22:18:51 +0200
commit1b4b78f5b6df7182ac066fcc26a7b4f0e8586a47 (patch)
treeef2bddec7b9f09c614012ac6ee2588cd732242ee
parent71715c049145a074b0f2b8d90c8c8c47830323c3 (diff)
downloadMusicSampler-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__.py8
-rw-r--r--helpers/action.py56
-rw-r--r--helpers/mapping.py24
-rw-r--r--helpers/music_file.py38
-rw-r--r--music_sampler.kv10
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 @@
2import argparse 2import argparse
3import sys 3import sys
4import os 4import os
5import math
5 6
6class Config: 7class 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
65def 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
8import sys 8import sys
9 9
10from .music_file import * 10from .music_file import *
11from . import yml_file 11from . import yml_file,gain
12 12
13class Mapping(RelativeLayout): 13class 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 @@
1import threading 1import threading
2import pydub 2import pydub
3import math
4import time 3import time
5from transitions.extensions import HierarchicalMachine as Machine 4from transitions.extensions import HierarchicalMachine as Machine
6 5
@@ -9,12 +8,14 @@ import sounddevice as sd
9import os.path 8import os.path
10 9
11from .lock import Lock 10from .lock import Lock
11from . import gain
12
12file_lock = Lock("file") 13file_lock = Lock("file")
13 14
14pyaudio = pa.PyAudio() 15pyaudio = pa.PyAudio()
15 16
16class MusicFile(Machine): 17class 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