diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-14 13:26:39 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-14 13:26:39 +0200 |
commit | 29597680758e4924aa71fc021465189e153f2016 (patch) | |
tree | f86b35983958ed5d82d8d4c3dd47dd3cd6ca1815 /helpers | |
parent | 9b9dd12a0253f2e65c5934068d91b544f7679f12 (diff) | |
download | MusicSampler-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.py | 39 | ||||
-rw-r--r-- | helpers/lock.py | 6 | ||||
-rw-r--r-- | helpers/mapping.py | 21 | ||||
-rw-r--r-- | helpers/music_file.py | 182 |
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 | |||
4 | from kivy.clock import Clock | 4 | from kivy.clock import Clock |
5 | 5 | ||
6 | import threading | 6 | import threading |
7 | import pygame | ||
8 | import yaml | 7 | import yaml |
9 | import sys | 8 | import sys |
10 | 9 | ||
11 | from .lock import * | ||
12 | from .music_file import * | 10 | from .music_file import * |
13 | from . import yml_file | 11 | from . 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 @@ | |||
1 | import threading | 1 | import threading |
2 | import pydub | 2 | import pydub |
3 | import pygame | ||
4 | import math | 3 | import math |
5 | import time | 4 | import time |
6 | from transitions.extensions import HierarchicalMachine as Machine | 5 | from transitions.extensions import HierarchicalMachine as Machine |
7 | 6 | ||
7 | import pyaudio as pa | ||
8 | import sounddevice as sd | ||
9 | import os.path | ||
10 | |||
11 | from .lock import Lock | ||
12 | file_lock = Lock("file") | ||
13 | |||
14 | pyaudio = pa.PyAudio() | ||
15 | |||
8 | class MusicFile(Machine): | 16 | class 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) | 154 | def 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 | |||
162 | pydub.AudioSegment.get_sample_slice_data = get_sample_slice_data | ||