4 from transitions
.extensions
import HierarchicalMachine
as Machine
7 import sounddevice
as sd
10 from .lock
import Lock
13 file_lock
= Lock("file")
15 pyaudio
= pa
.PyAudio()
17 class MusicFile(Machine
):
18 def __init__(self
, filename
, mapping
, name
= None, gain
= 1):
23 { 'name': 'loaded', 'children': ['stopped', 'playing', 'paused', 'stopping'] }
26 { 'trigger': 'load', 'source': 'initial', 'dest': 'loading'}
,
27 { 'trigger': 'fail', 'source': 'loading', 'dest': 'failed'}
,
28 { 'trigger': 'success', 'source': 'loading', 'dest': 'loaded_stopped'}
,
29 { 'trigger': 'start_playing', 'source': 'loaded_stopped', 'dest': 'loaded_playing'}
,
30 { 'trigger': 'pause', 'source': 'loaded_playing', 'dest': 'loaded_paused'}
,
31 { 'trigger': 'unpause', 'source': 'loaded_paused', 'dest': 'loaded_playing'}
,
32 { 'trigger': 'stop_playing', 'source': ['loaded_playing','loaded_paused'], 'dest': 'loaded_stopping'}
,
33 { 'trigger': 'stopped', 'source': 'loaded_stopping', 'dest': 'loaded_stopped'}
36 Machine
.__init
__(self
, states
=states
, transitions
=transitions
, initial
='initial')
39 self
.mapping
= mapping
40 self
.filename
= filename
42 self
.name
= name
or filename
43 self
.audio_segment
= None
44 self
.audio_segment_frame_width
= 0
45 self
.volume_factor
= gain
46 self
.music_lock
= Lock("music__" + filename
)
47 self
.wait_event
= threading
.Event()
49 threading
.Thread(name
= "MSMusicLoad", target
= self
.load
).start()
51 def on_enter_loading(self
):
54 print("Loading « {} »".format(self
.name
))
55 db_gain
= gain(self
.volume_factor
* 100)
56 self
.audio_segment
= pydub
.AudioSegment
.from_file(self
.filename
).set_frame_rate(44100).apply_gain(db_gain
)
57 self
.audio_segment_frame_width
= self
.audio_segment
.frame_width
58 self
.sound_duration
= self
.audio_segment
.duration_seconds
59 except Exception as e
:
60 print("failed to load « {} »: {}".format(self
.name
, e
))
61 self
.loading_error
= e
65 print("Loaded « {} »".format(self
.name
))
67 def check_is_loaded(self
):
68 return self
.state
.startswith('loaded_')
70 def is_not_stopped(self
):
71 return self
.check_is_loaded() and not self
.is_loaded_stopped()
74 return self
.is_loaded_paused()
77 def sound_position(self
):
78 if self
.is_not_stopped():
79 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
83 def play(self
, fade_in
= 0, volume
= 100, loop
= 0, start_at
= 0):
84 db_gain
= gain(volume
) + self
.mapping
.master_gain
88 ms
= int(start_at
* 1000)
89 ms_fi
= int(fade_in
* 1000)
91 self
.current_audio_segment
= (self
.audio_segment
+ db_gain
)
92 self
.current_frame
= int(start_at
* self
.audio_segment
.frame_rate
)
94 # FIXME: apply it to repeated when looping?
95 self
.a_s_with_effect
= self
.current_audio_segment
[ms
:ms
+ms_fi
].fade_in(ms_fi
)
96 self
.current_frame_with_effect
= 0
98 self
.a_s_with_effect
= None
100 self
.before_loaded_playing()
103 def before_loaded_playing(self
):
104 with self
.music_lock
:
105 segment
= self
.current_audio_segment
107 self
.stream
= sd
.RawOutputStream(samplerate
=segment
.frame_rate
,
108 channels
=segment
.channels
,
109 dtype
='int' + str(8*segment
.sample_width
), # FIXME: ?
111 callback
=self
.play_callback
,
112 finished_callback
=self
.finished_callback
115 def on_enter_loaded_playing(self
):
118 def on_enter_loaded_paused(self
):
121 def finished_callback(self
):
122 if self
.is_loaded_playing():
124 if self
.is_loaded_stopping():
127 def on_enter_loaded_stopped(self
):
128 self
.wait_event
.set()
130 def play_callback(self
, out_data
, frame_count
, time_info
, status_flags
):
131 with self
.music_lock
:
132 [data
, nb_frames
] = self
.get_next_sample(frame_count
)
133 if nb_frames
< frame_count
:
134 if self
.is_loaded_playing() and self
.loop
!= 0:
136 self
.current_frame
= 0
137 [new_data
, new_nb_frames
] = self
.get_next_sample(frame_count
- nb_frames
)
139 nb_frames
+= new_nb_frames
141 raise sd
.CallbackStop
143 out_data
[:] = data
.ljust(len(out_data
), b
'\0')
145 def get_next_sample(self
, frame_count
):
146 fw
= self
.audio_segment_frame_width
150 if self
.a_s_with_effect
is not None:
151 segment
= self
.a_s_with_effect
152 max_val
= int(segment
.frame_count())
154 start_i
= max(self
.current_frame_with_effect
, 0)
155 end_i
= min(self
.current_frame_with_effect
+ frame_count
, max_val
)
157 data
+= segment
._data
[(start_i
* fw
):(end_i
* fw
)]
159 frame_count
= max(0, self
.current_frame_with_effect
+ frame_count
- max_val
)
161 self
.current_frame_with_effect
+= end_i
- start_i
162 self
.current_frame
+= end_i
- start_i
163 nb_frames
+= end_i
- start_i
166 self
.a_s_with_effect
= None
168 segment
= self
.current_audio_segment
169 max_val
= int(segment
.frame_count())
171 start_i
= max(self
.current_frame
, 0)
172 end_i
= min(self
.current_frame
+ frame_count
, max_val
)
173 data
+= segment
._data
[(start_i
* fw
):(end_i
* fw
)]
174 nb_frames
+= end_i
- start_i
175 self
.current_frame
+= end_i
- start_i
177 return [data
, nb_frames
]
179 def seek(self
, value
= 0, delta
= False):
180 # We don't want to do that while stopping
181 if not (self
.is_loaded_playing() or self
.is_loaded_paused()):
183 with self
.music_lock
:
184 self
.a_s_with_effect
= None
185 self
.current_frame
= max(0, int(delta
) * self
.current_frame
+ int(value
* self
.audio_segment
.frame_rate
))
186 # FIXME: si on fait un seek + delta, adapter le "loop"
188 def stop(self
, fade_out
= 0, wait
= False):
189 if self
.is_loaded_playing():
190 ms
= int(self
.sound_position
* 1000)
191 ms_fo
= max(1, int(fade_out
* 1000))
193 # FIXME: stop fade_out puis seek -5 -> on abandonne le fade ? (cf
194 # commentaire dans fonction seek
195 with self
.music_lock
:
196 self
.current_audio_segment
= self
.current_audio_segment
[:ms
+ ms_fo
].fade_out(ms_fo
)
204 def set_gain(self
, db_gain
):
205 if not self
.is_not_stopped():
208 new_audio_segment
= self
.current_audio_segment
+ db_gain
210 new_a_s_with_effect
= None
211 if self
.a_s_with_effect
is not None:
212 new_a_s_with_effect
= self
.a_s_with_effect
+ db_gain
214 with self
.music_lock
:
215 self
.current_audio_segment
= new_audio_segment
216 self
.a_s_with_effect
= new_a_s_with_effect
218 def set_volume(self
, value
, delta
= False):
219 [db_gain
, self
.volume
] = gain(value
+ int(delta
) * self
.volume
, self
.volume
)
221 self
.set_gain(db_gain
)
224 self
.wait_event
.clear()
225 self
.wait_event
.wait()
227 # Add some more functions to AudioSegments
228 def get_sample_slice_data(self
, start_sample
=0, end_sample
=float('inf')):
229 max_val
= int(self
.frame_count())
231 start_i
= max(start_sample
, 0) * self
.frame_width
232 end_i
= min(end_sample
, max_val
) * self
.frame_width
234 return self
._data
[start_i
:end_i
]
236 pydub
.AudioSegment
.get_sample_slice_data
= get_sample_slice_data