]>
git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - music_file.py
4ba65e342b921f2cd74f93feaecea7d7b223cf7a
4 from transitions
.extensions
import HierarchicalMachine
as Machine
10 from .lock
import Lock
11 from .helpers
import Config
, gain
, debug_print
, error_print
12 from .mixer
import Mixer
13 from .music_effect
import GainEffect
15 file_lock
= Lock("file")
36 'after': 'poll_loaded'
49 'trigger': 'start_playing',
51 'dest': 'loaded_playing',
52 # if a child has no transitions, then it is bubbled to the parent,
53 # and we don't want that. Not useful in that machine precisely.
54 'conditions': ['is_loaded']
58 'source': 'loaded_playing',
59 'dest': 'loaded_paused'
63 'source': 'loaded_paused',
64 'dest': 'loaded_playing'
67 'trigger': 'stop_playing',
68 'source': ['loaded_playing','loaded_paused'],
69 'dest': 'loaded_stopping'
75 'before': 'trigger_stopped_events',
76 'conditions': ['is_in_use']
80 def __init__(self
, filename
, mapping
, name
=None, gain
=1):
81 Machine(model
=self
, states
=self
.STATES
,
82 transitions
=self
.TRANSITIONS
, initial
='initial',
83 ignore_invalid_triggers
=True)
85 self
.loaded_callbacks
= []
86 self
.mapping
= mapping
87 self
.filename
= filename
88 self
.name
= name
or filename
89 self
.audio_segment
= None
90 self
.initial_volume_factor
= gain
91 self
.music_lock
= Lock("music__" + filename
)
93 threading
.Thread(name
="MSMusicLoad", target
=self
.load
).start()
95 def reload_properties(self
, name
=None, gain
=1):
96 self
.name
= name
or self
.filename
97 if gain
!= self
.initial_volume_factor
:
98 self
.initial_volume_factor
= gain
99 self
.reload_music_file()
101 def reload_music_file(self
):
104 if self
.filename
.startswith("/"):
105 filename
= self
.filename
107 filename
= Config
.music_path
+ self
.filename
109 debug_print("Reloading « {} »".format(self
.name
))
110 initial_db_gain
= gain(self
.initial_volume_factor
* 100)
111 self
.audio_segment
= pydub
.AudioSegment \
112 .from_file(filename
) \
113 .set_frame_rate(Config
.frame_rate
) \
114 .set_channels(Config
.channels
) \
115 .set_sample_width(Config
.sample_width
) \
116 .apply_gain(initial_db_gain
)
117 except Exception as e
:
118 error_print("failed to reload « {} »: {}"\
119 .format(self
.name
, e
))
120 self
.loading_error
= e
123 debug_print("Reloaded « {} »".format(self
.name
))
125 # Machine related events
126 def on_enter_loading(self
):
129 if self
.filename
.startswith("/"):
130 filename
= self
.filename
132 filename
= Config
.music_path
+ self
.filename
134 debug_print("Loading « {} »".format(self
.name
))
135 self
.mixer
= self
.mapping
.mixer
or Mixer()
136 initial_db_gain
= gain(self
.initial_volume_factor
* 100)
137 self
.audio_segment
= pydub
.AudioSegment \
138 .from_file(filename
) \
139 .set_frame_rate(Config
.frame_rate
) \
140 .set_channels(Config
.channels
) \
141 .set_sample_width(Config
.sample_width
) \
142 .apply_gain(initial_db_gain
)
143 self
.sound_duration
= self
.audio_segment
.duration_seconds
144 except Exception as e
:
145 error_print("failed to load « {} »: {}".format(self
.name
, e
))
146 self
.loading_error
= e
150 debug_print("Loaded « {} »".format(self
.name
))
152 def on_enter_loaded(self
):
156 self
.gain_effects
= []
157 self
.set_gain(0, absolute
=True)
158 self
.current_audio_segment
= None
160 self
.wait_event
= threading
.Event()
161 self
.current_loop
= 0
163 def on_enter_loaded_playing(self
):
164 self
.mixer
.add_file(self
)
166 # Machine related states
168 return self
.is_loaded(allow_substates
=True) and not self
.is_loaded()
170 def is_in_use_not_stopping(self
):
171 return self
.is_loaded_playing() or self
.is_loaded_paused()
173 # Machine related triggers
174 def trigger_stopped_events(self
):
175 self
.mixer
.remove_file(self
)
176 self
.wait_event
.set()
179 # Actions and properties called externally
181 def sound_position(self
):
183 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
187 def play(self
, fade_in
=0, volume
=100, loop
=0, start_at
=0):
188 self
.set_gain(gain(volume
) + self
.mapping
.master_gain
, absolute
=True)
191 self
.last_loop
= float('inf')
193 self
.last_loop
= loop
195 with self
.music_lock
:
196 self
.current_audio_segment
= self
.audio_segment
197 self
.current_frame
= int(start_at
* self
.audio_segment
.frame_rate
)
202 db_gain
= gain(self
.volume
, 0)[0]
203 self
.set_gain(-db_gain
)
204 self
.add_fade_effect(db_gain
, fade_in
)
206 def seek(self
, value
=0, delta
=False):
207 if not self
.is_in_use_not_stopping():
210 with self
.music_lock
:
211 self
.abandon_all_effects()
213 frame_count
= int(self
.audio_segment
.frame_count())
214 frame_diff
= int(value
* self
.audio_segment
.frame_rate
)
215 self
.current_frame
+= frame_diff
216 while self
.current_frame
< 0:
217 self
.current_loop
-= 1
218 self
.current_frame
+= frame_count
219 while self
.current_frame
> frame_count
:
220 self
.current_loop
+= 1
221 self
.current_frame
-= frame_count
222 if self
.current_loop
< 0:
223 self
.current_loop
= 0
224 self
.current_frame
= 0
225 if self
.current_loop
> self
.last_loop
:
226 self
.current_loop
= self
.last_loop
227 self
.current_frame
= frame_count
229 self
.current_frame
= max(
231 int(value
* self
.audio_segment
.frame_rate
))
233 def stop(self
, fade_out
=0, wait
=False, set_wait_id
=None):
234 if self
.is_loaded_playing():
235 ms
= int(self
.sound_position
* 1000)
236 ms_fo
= max(1, int(fade_out
* 1000))
238 new_audio_segment
= self
.current_audio_segment
[: ms
+ms_fo
] \
240 with self
.music_lock
:
241 self
.current_audio_segment
= new_audio_segment
244 self
.mapping
.add_wait(self
.wait_event
, wait_id
=set_wait_id
)
249 def abandon_all_effects(self
):
251 for gain_effect
in self
.gain_effects
:
252 db_gain
+= gain_effect
.get_last_gain()
254 self
.gain_effects
= []
255 self
.set_gain(db_gain
)
257 def set_volume(self
, value
, delta
=False, fade
=0):
258 [db_gain
, self
.volume
] = gain(
259 value
+ int(delta
) * self
.volume
,
262 self
.set_gain_with_effect(db_gain
, fade
=fade
)
264 def set_gain_with_effect(self
, db_gain
, fade
=0):
265 if not self
.is_in_use():
269 self
.add_fade_effect(db_gain
, fade
)
271 self
.set_gain(db_gain
)
274 self
.wait_event
.clear()
275 self
.wait_event
.wait()
277 # Let other subscribe for an event when they are ready
278 def subscribe_loaded(self
, callback
):
279 # FIXME: should lock to be sure we have no race, but it makes the
280 # initialization screen not showing until everything is loaded
281 if self
.is_loaded(allow_substates
=True):
283 elif self
.is_failed():
286 self
.loaded_callbacks
.append(callback
)
288 def poll_loaded(self
):
289 for callback
in self
.loaded_callbacks
:
290 callback(self
.is_loaded())
291 self
.loaded_callbacks
= []
294 def finished_callback(self
):
297 def play_callback(self
, out_data_length
, frame_count
):
298 if self
.is_loaded_paused():
299 return b
'\0' * out_data_length
301 with self
.music_lock
:
302 [data
, nb_frames
] = self
.get_next_sample(frame_count
)
303 if nb_frames
< frame_count
:
304 if self
.is_loaded_playing() and\
305 self
.current_loop
< self
.last_loop
:
306 self
.current_loop
+= 1
307 self
.current_frame
= 0
308 [new_data
, new_nb_frames
] = self
.get_next_sample(
309 frame_count
- nb_frames
)
311 nb_frames
+= new_nb_frames
313 # FIXME: too slow when mixing multiple streams
315 name
="MSFinishedCallback",
316 target
=self
.finished_callback
).start()
318 return data
.ljust(out_data_length
, b
'\0')
321 def set_gain(self
, db_gain
, absolute
=False):
323 self
.db_gain
= db_gain
325 self
.db_gain
+= db_gain
327 def get_next_sample(self
, frame_count
):
328 fw
= self
.audio_segment
.frame_width
333 segment
= self
.current_audio_segment
334 max_val
= int(segment
.frame_count())
336 start_i
= max(self
.current_frame
, 0)
337 end_i
= min(self
.current_frame
+ frame_count
, max_val
)
338 data
+= segment
._data
[start_i
*fw
: end_i
*fw
]
339 nb_frames
+= end_i
- start_i
340 self
.current_frame
+= end_i
- start_i
342 volume_factor
= self
.volume_factor(self
.effects_next_gain(nb_frames
))
344 data
= audioop
.mul(data
, Config
.sample_width
, volume_factor
)
346 return [data
, nb_frames
]
348 def add_fade_effect(self
, db_gain
, fade_duration
):
349 if not self
.is_in_use():
352 self
.gain_effects
.append(GainEffect(
354 self
.current_audio_segment
,
357 self
.sound_position
+ fade_duration
,
360 def effects_next_gain(self
, frame_count
):
362 for gain_effect
in self
.gain_effects
:
363 [new_gain
, last_gain
] = gain_effect
.get_next_gain(
368 self
.set_gain(new_gain
)
369 self
.gain_effects
.remove(gain_effect
)
375 def volume_factor(self
, additional_gain
=0):
376 return 10 ** ( (self
.db_gain
+ additional_gain
) / 20)