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/music_file.py | |
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/music_file.py')
-rw-r--r-- | helpers/music_file.py | 182 |
1 files changed, 115 insertions, 67 deletions
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 | ||