]>
git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - music_sampler/music_file.py
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")
35 'source': ['initial', 'failed'],
45 'source': ['failed', 'loaded_stopped'],
51 'dest': 'loaded_stopped'
54 'trigger': 'start_playing',
55 'source': 'loaded_stopped',
56 'dest': 'loaded_playing'
60 'source': 'loaded_playing',
61 'dest': 'loaded_paused'
65 'source': 'loaded_paused',
66 'dest': 'loaded_playing'
69 'trigger': 'stop_playing',
70 'source': ['loaded_playing','loaded_paused'],
71 'dest': 'loaded_stopping'
76 'dest': 'loaded_stopped',
77 'before': 'trigger_stopped_events',
78 'unless': 'is_loaded_stopped',
82 def __init__(self
, filename
, mapping
, name
=None, gain
=1):
83 machine
= Machine(model
=self
, states
=self
.STATES
,
84 transitions
=self
.TRANSITIONS
, initial
='initial',
85 auto_transitions
=False,
86 after_state_change
=self
.notify_state_change
)
88 self
.state_change_callbacks
= []
89 self
.mapping
= mapping
90 self
.filename
= filename
91 self
.name
= name
or filename
92 self
.audio_segment
= None
93 self
.initial_volume_factor
= gain
94 self
.music_lock
= Lock("music__" + filename
)
96 if Config
.load_all_musics
:
97 threading
.Thread(name
="MSMusicLoad", target
=self
.load
).start()
99 def reload_properties(self
, name
=None, gain
=1):
100 self
.name
= name
or self
.filename
101 if gain
!= self
.initial_volume_factor
:
102 self
.initial_volume_factor
= gain
105 self
.load(reloading
=True)
107 # Machine related events
108 def on_enter_initial(self
):
109 self
.audio_segment
= None
111 def on_enter_loading(self
, reloading
=False):
120 if self
.mapping
.is_leaving_application
:
125 if self
.filename
.startswith("/"):
126 filename
= self
.filename
128 filename
= Config
.music_path
+ self
.filename
130 debug_print("{}oading « {} »".format(prefix
, self
.name
))
131 self
.mixer
= self
.mapping
.mixer
or Mixer()
132 initial_db_gain
= gain(self
.initial_volume_factor
* 100)
133 self
.audio_segment
= pydub
.AudioSegment \
134 .from_file(filename
) \
135 .set_frame_rate(Config
.frame_rate
) \
136 .set_channels(Config
.channels
) \
137 .set_sample_width(Config
.sample_width
) \
138 .apply_gain(initial_db_gain
)
139 self
.sound_duration
= self
.audio_segment
.duration_seconds
140 except Exception as e
:
141 error_print("failed to {}oad « {} »: {}".format(
142 prefix_s
, self
.name
, e
))
143 self
.loading_error
= e
147 debug_print("{}oaded « {} »".format(prefix
, self
.name
))
149 def on_enter_loaded(self
):
153 self
.gain_effects
= []
154 self
.set_gain(0, absolute
=True)
155 self
.current_audio_segment
= None
157 self
.wait_event
= threading
.Event()
158 self
.current_loop
= 0
160 def on_enter_loaded_playing(self
):
161 self
.mixer
.add_file(self
)
163 # Machine related states
165 return self
.is_loaded(allow_substates
=True) and\
166 not self
.is_loaded_stopped()
168 def is_in_use_not_stopping(self
):
169 return self
.is_loaded_playing() or self
.is_loaded_paused()
171 def is_unloadable(self
):
172 return self
.is_loaded_stopped() or self
.is_failed()
174 # Machine related triggers
175 def trigger_stopped_events(self
):
176 self
.mixer
.remove_file(self
)
177 self
.wait_event
.set()
180 # Actions and properties called externally
182 def sound_position(self
):
184 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
188 def play(self
, fade_in
=0, volume
=100, loop
=0, start_at
=0):
189 self
.set_gain(gain(volume
) + self
.mapping
.master_gain
, absolute
=True)
192 self
.last_loop
= float('inf')
194 self
.last_loop
= loop
196 with self
.music_lock
:
197 self
.current_audio_segment
= self
.audio_segment
198 self
.current_frame
= int(start_at
* self
.audio_segment
.frame_rate
)
203 db_gain
= gain(self
.volume
, 0)[0]
204 self
.set_gain(-db_gain
)
205 self
.add_fade_effect(db_gain
, fade_in
)
207 def seek(self
, value
=0, delta
=False):
208 if not self
.is_in_use_not_stopping():
211 with self
.music_lock
:
212 self
.abandon_all_effects()
214 frame_count
= int(self
.audio_segment
.frame_count())
215 frame_diff
= int(value
* self
.audio_segment
.frame_rate
)
216 self
.current_frame
+= frame_diff
217 while self
.current_frame
< 0:
218 self
.current_loop
-= 1
219 self
.current_frame
+= frame_count
220 while self
.current_frame
> frame_count
:
221 self
.current_loop
+= 1
222 self
.current_frame
-= frame_count
223 if self
.current_loop
< 0:
224 self
.current_loop
= 0
225 self
.current_frame
= 0
226 if self
.current_loop
> self
.last_loop
:
227 self
.current_loop
= self
.last_loop
228 self
.current_frame
= frame_count
230 self
.current_frame
= max(
232 int(value
* self
.audio_segment
.frame_rate
))
234 def stop(self
, fade_out
=0, wait
=False, set_wait_id
=None):
235 if self
.is_loaded_playing():
236 ms
= int(self
.sound_position
* 1000)
237 ms_fo
= max(1, int(fade_out
* 1000))
239 new_audio_segment
= self
.current_audio_segment
[: ms
+ms_fo
] \
241 with self
.music_lock
:
242 self
.current_audio_segment
= new_audio_segment
245 self
.mapping
.add_wait(self
.wait_event
, wait_id
=set_wait_id
)
247 elif self
.is_loaded(allow_substates
=True):
250 def abandon_all_effects(self
):
252 for gain_effect
in self
.gain_effects
:
253 db_gain
+= gain_effect
.get_last_gain()
255 self
.gain_effects
= []
256 self
.set_gain(db_gain
)
258 def set_volume(self
, value
, delta
=False, fade
=0):
259 [db_gain
, self
.volume
] = gain(
260 value
+ int(delta
) * self
.volume
,
263 self
.set_gain_with_effect(db_gain
, fade
=fade
)
265 def set_gain_with_effect(self
, db_gain
, fade
=0):
266 if not self
.is_in_use():
270 self
.add_fade_effect(db_gain
, fade
)
272 self
.set_gain(db_gain
)
275 self
.wait_event
.clear()
276 self
.wait_event
.wait()
278 # Let other subscribe for state change
279 def notify_state_change(self
, **kwargs
):
280 for callback
in self
.state_change_callbacks
:
283 def subscribe_state_change(self
, callback
):
284 if callback
not in self
.state_change_callbacks
:
285 self
.state_change_callbacks
.append(callback
)
288 def unsubscribe_state_change(self
, callback
):
289 if callback
in self
.state_change_callbacks
:
290 self
.state_change_callbacks
.remove(callback
)
293 def finished_callback(self
):
296 def play_callback(self
, out_data_length
, frame_count
):
297 if self
.is_loaded_paused():
298 return b
'\0' * out_data_length
300 with self
.music_lock
:
301 [data
, nb_frames
] = self
.get_next_sample(frame_count
)
302 if nb_frames
< frame_count
:
303 if self
.is_loaded_playing() and\
304 self
.current_loop
< self
.last_loop
:
305 self
.current_loop
+= 1
306 self
.current_frame
= 0
307 [new_data
, new_nb_frames
] = self
.get_next_sample(
308 frame_count
- nb_frames
)
310 nb_frames
+= new_nb_frames
312 # FIXME: too slow when mixing multiple streams
314 name
="MSFinishedCallback",
315 target
=self
.finished_callback
).start()
317 return data
.ljust(out_data_length
, b
'\0')
320 def set_gain(self
, db_gain
, absolute
=False):
322 self
.db_gain
= db_gain
324 self
.db_gain
+= db_gain
326 def get_next_sample(self
, frame_count
):
327 fw
= self
.audio_segment
.frame_width
332 segment
= self
.current_audio_segment
333 max_val
= int(segment
.frame_count())
335 start_i
= max(self
.current_frame
, 0)
336 end_i
= min(self
.current_frame
+ frame_count
, max_val
)
337 data
+= segment
._data
[start_i
*fw
: end_i
*fw
]
338 nb_frames
+= end_i
- start_i
339 self
.current_frame
+= end_i
- start_i
341 volume_factor
= self
.volume_factor(self
.effects_next_gain(nb_frames
))
343 data
= audioop
.mul(data
, Config
.sample_width
, volume_factor
)
345 return [data
, nb_frames
]
347 def add_fade_effect(self
, db_gain
, fade_duration
):
348 if not self
.is_in_use():
351 self
.gain_effects
.append(GainEffect(
353 self
.current_audio_segment
,
356 self
.sound_position
+ fade_duration
,
359 def effects_next_gain(self
, frame_count
):
361 for gain_effect
in self
.gain_effects
:
362 [new_gain
, last_gain
] = gain_effect
.get_next_gain(
367 self
.set_gain(new_gain
)
368 self
.gain_effects
.remove(gain_effect
)
374 def volume_factor(self
, additional_gain
=0):
375 return 10 ** ( (self
.db_gain
+ additional_gain
) / 20)