]>
git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - helpers/music_file.py
ccf60ce5eb8874f5b68dc81d48a89b9e86fd8626
4 from transitions
.extensions
import HierarchicalMachine
as Machine
10 from .lock
import Lock
11 from . import Config
, gain
, debug_print
, error_print
12 from .mixer
import Mixer
13 from .music_effect
import GainEffect
15 file_lock
= Lock("file")
48 'trigger': 'start_playing',
50 'dest': 'loaded_playing'
54 'source': 'loaded_playing',
55 'dest': 'loaded_paused'
59 'source': 'loaded_paused',
60 'dest': 'loaded_playing'
63 'trigger': 'stop_playing',
64 'source': ['loaded_playing','loaded_paused'],
65 'dest': 'loaded_stopping'
71 'before': 'trigger_stopped_events'
75 def __init__(self
, filename
, mapping
, name
=None, gain
=1):
76 Machine(model
=self
, states
=self
.STATES
,
77 transitions
=self
.TRANSITIONS
, initial
='initial',
78 ignore_invalid_triggers
=True)
80 self
.mapping
= mapping
81 self
.filename
= filename
82 self
.name
= name
or filename
83 self
.audio_segment
= None
84 self
.initial_volume_factor
= gain
85 self
.music_lock
= Lock("music__" + filename
)
87 threading
.Thread(name
="MSMusicLoad", target
=self
.load
).start()
89 # Machine related events
90 def on_enter_loading(self
):
93 debug_print("Loading « {} »".format(self
.name
))
94 self
.mixer
= self
.mapping
.mixer
or Mixer()
95 initial_db_gain
= gain(self
.initial_volume_factor
* 100)
96 self
.audio_segment
= pydub
.AudioSegment \
97 .from_file(self
.filename
) \
98 .set_frame_rate(Config
.frame_rate
) \
99 .set_channels(Config
.channels
) \
100 .set_sample_width(Config
.sample_width
) \
101 .apply_gain(initial_db_gain
)
102 self
.sound_duration
= self
.audio_segment
.duration_seconds
103 except Exception as e
:
104 error_print("failed to load « {} »: {}".format(self
.name
, e
))
105 self
.loading_error
= e
109 debug_print("Loaded « {} »".format(self
.name
))
111 def on_enter_loaded(self
):
112 self
.gain_effects
= []
113 self
.set_gain(0, absolute
=True)
114 self
.current_audio_segment
= None
116 self
.wait_event
= threading
.Event()
117 self
.current_loop
= 0
119 def on_enter_loaded_playing(self
):
120 self
.mixer
.add_file(self
)
122 # Machine related states
124 return self
.is_loaded(allow_substates
=True) and not self
.is_loaded()
126 def is_in_use_not_stopping(self
):
127 return self
.is_loaded_playing() or self
.is_loaded_paused()
129 # Machine related triggers
130 def trigger_stopped_events(self
):
131 self
.mixer
.remove_file(self
)
132 self
.wait_event
.set()
134 # Actions and properties called externally
136 def sound_position(self
):
138 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
142 def play(self
, fade_in
=0, volume
=100, loop
=0, start_at
=0):
143 self
.set_gain(gain(volume
) + self
.mapping
.master_gain
, absolute
=True)
146 self
.last_loop
= float('inf')
148 self
.last_loop
= loop
150 with self
.music_lock
:
151 self
.current_audio_segment
= self
.audio_segment
152 self
.current_frame
= int(start_at
* self
.audio_segment
.frame_rate
)
157 db_gain
= gain(self
.volume
, 0)[0]
158 self
.set_gain(-db_gain
)
159 self
.add_fade_effect(db_gain
, fade_in
)
161 def seek(self
, value
=0, delta
=False):
162 if not self
.is_in_use_not_stopping():
165 with self
.music_lock
:
166 self
.abandon_all_effects()
168 frame_count
= int(self
.audio_segment
.frame_count())
169 frame_diff
= int(value
* self
.audio_segment
.frame_rate
)
170 self
.current_frame
+= frame_diff
171 while self
.current_frame
< 0:
172 self
.current_loop
-= 1
173 self
.current_frame
+= frame_count
174 while self
.current_frame
> frame_count
:
175 self
.current_loop
+= 1
176 self
.current_frame
-= frame_count
177 if self
.current_loop
< 0:
178 self
.current_loop
= 0
179 self
.current_frame
= 0
180 if self
.current_loop
> self
.last_loop
:
181 self
.current_loop
= self
.last_loop
182 self
.current_frame
= frame_count
184 self
.current_frame
= max(
186 int(value
* self
.audio_segment
.frame_rate
))
188 def stop(self
, fade_out
=0, wait
=False, set_wait_id
=None):
189 if self
.is_loaded_playing():
190 ms
= int(self
.sound_position
* 1000)
191 ms_fo
= max(1, int(fade_out
* 1000))
193 new_audio_segment
= self
.current_audio_segment
[: ms
+ms_fo
] \
195 with self
.music_lock
:
196 self
.current_audio_segment
= new_audio_segment
199 if set_wait_id
is not None:
200 self
.mapping
.add_wait_id(set_wait_id
, self
.wait_event
)
205 def abandon_all_effects(self
):
207 for gain_effect
in self
.gain_effects
:
208 db_gain
+= gain_effect
.get_last_gain()
210 self
.gain_effects
= []
211 self
.set_gain(db_gain
)
213 def set_volume(self
, value
, delta
=False, fade
=0):
214 [db_gain
, self
.volume
] = gain(
215 value
+ int(delta
) * self
.volume
,
218 self
.set_gain_with_effect(db_gain
, fade
=fade
)
220 def set_gain_with_effect(self
, db_gain
, fade
=0):
221 if not self
.is_in_use():
225 self
.add_fade_effect(db_gain
, fade
)
227 self
.set_gain(db_gain
)
230 self
.wait_event
.clear()
231 self
.wait_event
.wait()
234 def finished_callback(self
):
237 def play_callback(self
, out_data_length
, frame_count
):
238 if self
.is_loaded_paused():
239 return b
'\0' * out_data_length
241 with self
.music_lock
:
242 [data
, nb_frames
] = self
.get_next_sample(frame_count
)
243 if nb_frames
< frame_count
:
244 if self
.is_loaded_playing() and\
245 self
.current_loop
< self
.last_loop
:
246 self
.current_loop
+= 1
247 self
.current_frame
= 0
248 [new_data
, new_nb_frames
] = self
.get_next_sample(
249 frame_count
- nb_frames
)
251 nb_frames
+= new_nb_frames
253 # FIXME: too slow when mixing multiple streams
255 name
="MSFinishedCallback",
256 target
=self
.finished_callback
).start()
258 return data
.ljust(out_data_length
, b
'\0')
261 def set_gain(self
, db_gain
, absolute
=False):
263 self
.db_gain
= db_gain
265 self
.db_gain
+= db_gain
267 def get_next_sample(self
, frame_count
):
268 fw
= self
.audio_segment
.frame_width
273 segment
= self
.current_audio_segment
274 max_val
= int(segment
.frame_count())
276 start_i
= max(self
.current_frame
, 0)
277 end_i
= min(self
.current_frame
+ frame_count
, max_val
)
278 data
+= segment
._data
[start_i
*fw
: end_i
*fw
]
279 nb_frames
+= end_i
- start_i
280 self
.current_frame
+= end_i
- start_i
282 volume_factor
= self
.volume_factor(self
.effects_next_gain(nb_frames
))
284 data
= audioop
.mul(data
, Config
.sample_width
, volume_factor
)
286 return [data
, nb_frames
]
288 def add_fade_effect(self
, db_gain
, fade_duration
):
289 if not self
.is_in_use():
292 self
.gain_effects
.append(GainEffect(
294 self
.current_audio_segment
,
297 self
.sound_position
+ fade_duration
,
300 def effects_next_gain(self
, frame_count
):
302 for gain_effect
in self
.gain_effects
:
303 [new_gain
, last_gain
] = gain_effect
.get_next_gain(
308 self
.set_gain(new_gain
)
309 self
.gain_effects
.remove(gain_effect
)
315 def volume_factor(self
, additional_gain
=0):
316 return 10 ** ( (self
.db_gain
+ additional_gain
) / 20)