aboutsummaryrefslogtreecommitdiff
path: root/helpers
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2016-07-14 13:26:39 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2016-07-14 13:26:39 +0200
commit29597680758e4924aa71fc021465189e153f2016 (patch)
treef86b35983958ed5d82d8d4c3dd47dd3cd6ca1815 /helpers
parent9b9dd12a0253f2e65c5934068d91b544f7679f12 (diff)
downloadMusicSampler-29597680758e4924aa71fc021465189e153f2016.tar.gz
MusicSampler-29597680758e4924aa71fc021465189e153f2016.tar.zst
MusicSampler-29597680758e4924aa71fc021465189e153f2016.zip
Move from pygame to sounddevice for sound handling
Move lock files to files Add with statement to lock
Diffstat (limited to 'helpers')
-rw-r--r--helpers/action.py39
-rw-r--r--helpers/lock.py6
-rw-r--r--helpers/mapping.py21
-rw-r--r--helpers/music_file.py182
4 files changed, 141 insertions, 107 deletions
diff --git a/helpers/action.py b/helpers/action.py
index b921fbf..69ae96f 100644
--- a/helpers/action.py
+++ b/helpers/action.py
@@ -38,43 +38,37 @@ class Action:
38 def command(self, command = "", **kwargs): 38 def command(self, command = "", **kwargs):
39 pass 39 pass
40 40
41 def pause(self, music = None, **kwargs): 41 def music_list(self, music):
42 if music is not None: 42 if music is not None:
43 music.pause() 43 return [music]
44 else: 44 else:
45 for music in self.key.parent.open_files.values(): 45 return self.key.parent.open_files.values()
46 if music.is_playing() and not music.is_paused(): 46
47 music.pause() 47 def pause(self, music = None, **kwargs):
48 for music in self.music_list(music):
49 if music.is_loaded_playing():
50 music.pause()
48 51
49 def unpause(self, music = None, **kwargs): 52 def unpause(self, music = None, **kwargs):
50 if music is not None: 53 for music in self.music_list(music):
51 music.unpause() 54 if music.is_loaded_paused():
52 else: 55 music.unpause()
53 for music in self.key.parent.open_files.values():
54 if music.is_playing() and music.is_paused():
55 music.unpause()
56 56
57 def play(self, music = None, fade_in = 0, start_at = 0, 57 def play(self, music = None, fade_in = 0, start_at = 0,
58 restart_if_running = False, volume = 100, **kwargs): 58 restart_if_running = False, volume = 100, **kwargs):
59 if music is not None: 59 if music is not None:
60 if restart_if_running: 60 if restart_if_running:
61 if music.is_playing(): 61 if music.is_not_stopped():
62 music.stop() 62 music.stop()
63 music.play(volume = volume, fade_in = fade_in, start_at = start_at) 63 music.play(volume = volume, fade_in = fade_in, start_at = start_at)
64 else: 64 else:
65 if not music.is_playing(): 65 if not music.is_not_stopped():
66 music.play(volume = volume, fade_in = fade_in, start_at = start_at) 66 music.play(volume = volume, fade_in = fade_in, start_at = start_at)
67 else:
68 pygame.mixer.unpause()
69 67
70 def stop(self, music = None, fade_out = 0, **kwargs): 68 def stop(self, music = None, fade_out = 0, **kwargs):
71 if music is not None: 69 for music in self.music_list(music):
72 music.stop(fade_out = fade_out) 70 if music.is_loaded_paused() or music.is_loaded_playing():
73 else: 71 music.stop(fade_out = fade_out)
74 if fade_out > 0:
75 pygame.mixer.fadeout(int(fade_out * 1000))
76 else:
77 pygame.mixer.stop()
78 72
79 def stop_all_actions(self, **kwargs): 73 def stop_all_actions(self, **kwargs):
80 self.key.parent.stop_all_running() 74 self.key.parent.stop_all_running()
@@ -83,6 +77,7 @@ class Action:
83 if music is not None: 77 if music is not None:
84 music.set_volume(value) 78 music.set_volume(value)
85 else: 79 else:
80 # FIXME: todo
86 pass 81 pass
87 82
88 def wait(self, duration = 0, music = None, **kwargs): 83 def wait(self, duration = 0, music = None, **kwargs):
diff --git a/helpers/lock.py b/helpers/lock.py
index dff8b1f..85d281a 100644
--- a/helpers/lock.py
+++ b/helpers/lock.py
@@ -5,6 +5,12 @@ class Lock:
5 self.type = lock_type 5 self.type = lock_type
6 self.lock = threading.RLock() 6 self.lock = threading.RLock()
7 7
8 def __enter__(self, *args, **kwargs):
9 self.acquire(*args, **kwargs)
10
11 def __exit__(self, type, value, traceback, *args, **kwargs):
12 self.release(*args, **kwargs)
13
8 def acquire(self, *args, **kwargs): 14 def acquire(self, *args, **kwargs):
9 #print("acquiring lock for {}".format(self.type)) 15 #print("acquiring lock for {}".format(self.type))
10 self.lock.acquire(*args, **kwargs) 16 self.lock.acquire(*args, **kwargs)
diff --git a/helpers/mapping.py b/helpers/mapping.py
index dd51246..95c9d67 100644
--- a/helpers/mapping.py
+++ b/helpers/mapping.py
@@ -4,11 +4,9 @@ from kivy.core.window import Window
4from kivy.clock import Clock 4from kivy.clock import Clock
5 5
6import threading 6import threading
7import pygame
8import yaml 7import yaml
9import sys 8import sys
10 9
11from .lock import *
12from .music_file import * 10from .music_file import *
13from . import yml_file 11from . import yml_file
14 12
@@ -17,7 +15,7 @@ class Mapping(RelativeLayout):
17 ready_color = ListProperty([1, 165/255, 0, 1]) 15 ready_color = ListProperty([1, 165/255, 0, 1])
18 16
19 def __init__(self, **kwargs): 17 def __init__(self, **kwargs):
20 self.key_config, self.channel_number, self.open_files = self.parse_config() 18 self.key_config, self.open_files = self.parse_config()
21 super(Mapping, self).__init__(**kwargs) 19 super(Mapping, self).__init__(**kwargs)
22 self._keyboard = Window.request_keyboard(self._keyboard_closed, self) 20 self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
23 self._keyboard.bind(on_key_down=self._on_keyboard_down) 21 self._keyboard.bind(on_key_down=self._on_keyboard_down)
@@ -25,9 +23,6 @@ class Mapping(RelativeLayout):
25 Clock.schedule_interval(self.not_all_keys_ready, 1) 23 Clock.schedule_interval(self.not_all_keys_ready, 1)
26 24
27 25
28 pygame.mixer.init(frequency = 44100)
29 pygame.mixer.set_num_channels(self.channel_number)
30
31 def _keyboard_closed(self): 26 def _keyboard_closed(self):
32 self._keyboard.unbind(on_key_down=self._on_keyboard_down) 27 self._keyboard.unbind(on_key_down=self._on_keyboard_down)
33 self._keyboard = None 28 self._keyboard = None
@@ -42,7 +37,6 @@ class Mapping(RelativeLayout):
42 continue 37 continue
43 thread.join() 38 thread.join()
44 39
45 pygame.quit()
46 sys.exit() 40 sys.exit()
47 return True 41 return True
48 42
@@ -91,10 +85,6 @@ class Mapping(RelativeLayout):
91 aliases = config['aliases'] 85 aliases = config['aliases']
92 seen_files = {} 86 seen_files = {}
93 87
94 file_lock = Lock("file")
95
96 channel_id = 0
97
98 key_properties = {} 88 key_properties = {}
99 89
100 for key in config['key_properties']: 90 for key in config['key_properties']:
@@ -146,15 +136,10 @@ class Mapping(RelativeLayout):
146 if filename in config['music_properties']: 136 if filename in config['music_properties']:
147 seen_files[filename] = MusicFile( 137 seen_files[filename] = MusicFile(
148 filename, 138 filename,
149 file_lock,
150 channel_id,
151 **config['music_properties'][filename]) 139 **config['music_properties'][filename])
152 else: 140 else:
153 seen_files[filename] = MusicFile( 141 seen_files[filename] = MusicFile(
154 filename, 142 filename)
155 file_lock,
156 channel_id)
157 channel_id = channel_id + 1
158 143
159 if filename not in key_properties[mapped_key]['files']: 144 if filename not in key_properties[mapped_key]['files']:
160 key_properties[mapped_key]['files'].append(seen_files[filename]) 145 key_properties[mapped_key]['files'].append(seen_files[filename])
@@ -166,6 +151,6 @@ class Mapping(RelativeLayout):
166 151
167 key_properties[mapped_key]['actions'].append([action_name, action_args]) 152 key_properties[mapped_key]['actions'].append([action_name, action_args])
168 153
169 return (key_properties, channel_id + 1, seen_files) 154 return (key_properties, seen_files)
170 155
171 156
diff --git a/helpers/music_file.py b/helpers/music_file.py
index f9c5816..b40de1a 100644
--- a/helpers/music_file.py
+++ b/helpers/music_file.py
@@ -1,114 +1,162 @@
1import threading 1import threading
2import pydub 2import pydub
3import pygame
4import math 3import math
5import time 4import time
6from transitions.extensions import HierarchicalMachine as Machine 5from transitions.extensions import HierarchicalMachine as Machine
7 6
7import pyaudio as pa
8import sounddevice as sd
9import os.path
10
11from .lock import Lock
12file_lock = Lock("file")
13
14pyaudio = pa.PyAudio()
15
8class MusicFile(Machine): 16class MusicFile(Machine):
9 def __init__(self, filename, lock, channel_id, name = None, gain = 1): 17 def __init__(self, filename, name = None, gain = 1):
10 states = [ 18 states = [
11 'initial', 19 'initial',
12 'loading', 20 'loading',
13 'failed', 21 'failed',
14 { 'name': 'loaded', 'children': ['stopped', 'playing', 'paused'] } 22 { 'name': 'loaded', 'children': ['stopped', 'playing', 'paused', 'stopping'] }
15 ] 23 ]
16 transitions = [ 24 transitions = [
17 { 'trigger': 'load', 'source': 'initial', 'dest': 'loading'}, 25 { 'trigger': 'load', 'source': 'initial', 'dest': 'loading'},
18 { 'trigger': 'fail', 'source': 'loading', 'dest': 'failed'}, 26 { 'trigger': 'fail', 'source': 'loading', 'dest': 'failed'},
19 { 'trigger': 'success', 'source': 'loading', 'dest': 'loaded_stopped'}, 27 { 'trigger': 'success', 'source': 'loading', 'dest': 'loaded_stopped'},
20 #{ 'trigger': 'play', 'source': 'loaded_stopped', 'dest': 'loaded_playing'}, 28 { 'trigger': 'start_playing', 'source': 'loaded_stopped', 'dest': 'loaded_playing'},
21 #{ 'trigger': 'pause', 'source': 'loaded_playing', 'dest': 'loaded_paused'}, 29 { 'trigger': 'pause', 'source': 'loaded_playing', 'dest': 'loaded_paused'},
22 #{ 'trigger': 'stop', 'source': ['loaded_playing','loaded_paused'], 'dest': 'loaded_stopped'} 30 { 'trigger': 'unpause', 'source': 'loaded_paused', 'dest': 'loaded_playing'},
31 { 'trigger': 'stop_playing', 'source': ['loaded_playing','loaded_paused'], 'dest': 'loaded_stopping'},
32 { 'trigger': 'stopped', 'source': 'loaded_stopping', 'dest': 'loaded_stopped'}
23 ] 33 ]
24 34
25 Machine.__init__(self, states=states, transitions=transitions, initial='initial') 35 Machine.__init__(self, states=states, transitions=transitions, initial='initial')
26 36
27 self.filename = filename 37 self.filename = filename
28 self.channel_id = channel_id 38 self.stream = None
29 self.name = name or filename 39 self.name = name or filename
30 self.raw_data = None 40 self.audio_segment = None
31 self.gain = gain 41 self.gain = gain
42 self.music_lock = Lock("music__" + filename)
32 43
33 self.flag_paused = False 44 self.flag_paused = False
34 threading.Thread(name = "MSMusicLoad", target = self.load, kwargs = {'lock': lock}).start() 45 threading.Thread(name = "MSMusicLoad", target = self.load).start()
35 46
36 def on_enter_loading(self, lock=None): 47 def on_enter_loading(self):
37 lock.acquire() 48 with file_lock:
38 try: 49 try:
39 print("Loading « {} »".format(self.name)) 50 print("Loading « {} »".format(self.name))
40 volume_factor = 20 * math.log10(self.gain) 51 volume_factor = 20 * math.log10(self.gain)
41 audio_segment = pydub.AudioSegment.from_file(self.filename).set_frame_rate(44100).apply_gain(volume_factor) 52 self.audio_segment = pydub.AudioSegment.from_file(self.filename).set_frame_rate(44100).apply_gain(volume_factor)
42 self.sound_duration = audio_segment.duration_seconds 53 self.sound_duration = self.audio_segment.duration_seconds
43 self.raw_data = audio_segment.raw_data 54 except Exception as e:
44 except Exception as e: 55 print("failed to load « {} »: {}".format(self.name, e))
45 print("failed to load « {} »: {}".format(self.name, e)) 56 self.loading_error = e
46 self.loading_error = e 57 self.fail()
47 self.fail() 58 else:
48 else: 59 self.success()
49 self.success() 60 print("Loaded « {} »".format(self.name))
50 print("Loaded « {} »".format(self.name))
51 finally:
52 lock.release()
53 61
54 def check_is_loaded(self): 62 def check_is_loaded(self):
55 return self.state.startswith('loaded_') 63 return self.state.startswith('loaded_')
56 64
57 def is_playing(self): 65 def is_not_stopped(self):
58 return self.channel().get_busy() 66 return self.check_is_loaded() and not self.is_loaded_stopped()
59 67
60 def is_paused(self): 68 def is_paused(self):
61 return self.flag_paused 69 return self.is_loaded_paused()
62 70
63 @property 71 @property
64 def sound_position(self): 72 def sound_position(self):
65 if self.is_playing() and not self.is_paused(): 73 if self.is_not_stopped():
66 return min(time.time() - self.started_at, self.sound_duration) 74 return self.current_frame / self.current_audio_segment.frame_rate
67 elif self.is_playing() and self.is_paused():
68 return min(self.paused_at - self.started_at, self.sound_duration)
69 else: 75 else:
70 return 0 76 return 0
71 77
72 def play(self, fade_in = 0, volume = 100, start_at = 0): 78 def play(self, fade_in = 0, volume = 100, start_at = 0):
73 self.set_volume(volume) 79 self.db_gain = self.volume_to_gain(volume)
74 80 ms = int(start_at * 1000)
75 if start_at > 0: 81 ms_fi = max(1, int(fade_in * 1000))
76 raw_data_length = len(self.raw_data) 82 with self.music_lock:
77 start_offset = int((raw_data_length / self.sound_duration) * start_at) 83 self.current_audio_segment = (self.audio_segment + self.db_gain).fade(from_gain=-120, duration=ms_fi, start=ms)
78 start_offset = start_offset - (start_offset % 2) 84 self.before_loaded_playing(initial_frame = int(start_at * self.audio_segment.frame_rate))
79 sound = pygame.mixer.Sound(self.raw_data[start_offset:]) 85 self.start_playing()
80 else: 86
81 sound = pygame.mixer.Sound(self.raw_data) 87 def before_loaded_playing(self, initial_frame = 0):
82 88 self.current_frame = initial_frame
83 self.started_at = time.time() 89 with self.music_lock:
84 self.channel().play(sound, fade_ms = int(fade_in * 1000)) 90 segment = self.current_audio_segment
85 self.flag_paused = False 91
86 92 self.stream = sd.RawOutputStream(samplerate=segment.frame_rate,
87 def pause(self): 93 channels=segment.channels,
88 self.paused_at = time.time() 94 dtype='int' + str(8*segment.sample_width), # FIXME: ?
89 self.channel().pause() 95 latency=1.,
90 self.flag_paused = True 96 callback=self.play_callback,
91 97 finished_callback=self.finished_callback
92 def unpause(self): 98 )
93 self.started_at += (time.time() - self.paused_at) 99
94 self.channel().unpause() 100 def on_enter_loaded_playing(self):
95 self.flag_paused = False 101 self.stream.start()
102
103 def on_enter_loaded_paused(self):
104 self.stream.stop()
105
106 def finished_callback(self):
107 if self.is_loaded_playing():
108 self.stop_playing()
109 if self.is_loaded_stopping():
110 self.stopped()
111
112 def play_callback(self, out_data, frame_count, time_info, status_flags):
113 with self.music_lock:
114 audio_segment = self.current_audio_segment.get_sample_slice_data(
115 start_sample=self.current_frame,
116 end_sample=self.current_frame + frame_count
117 )
118 self.current_frame += frame_count
119 if len(audio_segment) == 0:
120 raise sd.CallbackStop
121
122 out_data[:] = audio_segment.ljust(len(out_data), b'\0')
96 123
97 def stop(self, fade_out = 0): 124 def stop(self, fade_out = 0):
98 if fade_out > 0: 125 if self.is_loaded_playing():
99 self.channel().fadeout(int(fade_out * 1000)) 126 ms = int(self.sound_position * 1000)
127 ms_fo = max(1, int(fade_out * 1000))
128
129 with self.music_lock:
130 self.current_audio_segment = self.current_audio_segment[:ms + ms_fo].fade_out(ms_fo)
131 self.stop_playing()
100 else: 132 else:
101 self.channel().stop() 133 self.stop_playing()
134 self.stopped()
102 135
103 def set_volume(self, value): 136 def set_volume(self, value):
104 if value < 0: 137 if self.is_loaded_stopped():
105 value = 0 138 return
106 if value > 100: 139
107 value = 100 140 db_gain = self.volume_to_gain(value)
108 self.channel().set_volume(value / 100) 141 new_audio_segment = self.current_audio_segment + (db_gain - self.db_gain)
142 self.db_gain = db_gain
143 with self.music_lock:
144 self.current_audio_segment = new_audio_segment
145
146 def volume_to_gain(self, volume):
147 return 20 * math.log10(max(volume, 0.0001) / 100)
109 148
110 def wait_end(self): 149 def wait_end(self):
150 # FIXME: todo
111 pass 151 pass
112 152
113 def channel(self): 153# Add some more functions to AudioSegments
114 return pygame.mixer.Channel(self.channel_id) 154def get_sample_slice_data(self, start_sample=0, end_sample=float('inf')):
155 max_val = int(self.frame_count())
156
157 start_i = max(start_sample, 0) * self.frame_width
158 end_i = min(end_sample, max_val) * self.frame_width
159
160 return self._data[start_i:end_i]
161
162pydub.AudioSegment.get_sample_slice_data = get_sample_slice_data