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")
17 class MusicFile(Machine
):
18 def __init__(self
, filename
, mapping
, name
=None, gain
=1):
25 'children': ['stopped', 'playing', 'paused', 'stopping']
42 'dest': 'loaded_stopped'
45 'trigger': 'start_playing',
46 'source': 'loaded_stopped',
47 'dest': 'loaded_playing'
51 'source': 'loaded_playing',
52 'dest': 'loaded_paused'
56 'source': 'loaded_paused',
57 'dest': 'loaded_playing'
60 'trigger': 'stop_playing',
61 'source': ['loaded_playing','loaded_paused'],
62 'dest': 'loaded_stopping'
66 'source': 'loaded_stopping',
67 'dest': 'loaded_stopped',
68 'after': 'trigger_stopped_events'
72 Machine
.__init
__(self
, states
=states
,
73 transitions
=transitions
, initial
='initial')
76 self
.mapping
= mapping
77 self
.filename
= filename
78 self
.name
= name
or filename
79 self
.audio_segment
= None
80 self
.audio_segment_frame_width
= 0
81 self
.initial_volume_factor
= gain
82 self
.music_lock
= Lock("music__" + filename
)
83 self
.wait_event
= threading
.Event()
85 self
.gain_effects
= []
87 threading
.Thread(name
="MSMusicLoad", target
=self
.load
).start()
89 def on_enter_loading(self
):
92 debug_print("Loading « {} »".format(self
.name
))
93 self
.mixer
= self
.mapping
.mixer
or Mixer()
94 initial_db_gain
= gain(self
.initial_volume_factor
* 100)
95 self
.audio_segment
= pydub
.AudioSegment \
96 .from_file(self
.filename
) \
97 .set_frame_rate(Config
.frame_rate
) \
98 .set_channels(Config
.channels
) \
99 .set_sample_width(Config
.sample_width
) \
100 .apply_gain(initial_db_gain
)
101 self
.audio_segment_frame_width
= self
.audio_segment
.frame_width
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 check_is_loaded(self
):
112 return self
.state
.startswith('loaded_')
114 def is_not_stopped(self
):
115 return self
.check_is_loaded() and not self
.is_loaded_stopped()
118 return self
.is_loaded_paused()
121 def sound_position(self
):
122 if self
.is_not_stopped():
123 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
127 def play(self
, fade_in
=0, volume
=100, loop
=0, start_at
=0):
128 self
.set_gain(gain(volume
) + self
.mapping
.master_gain
, absolute
=True)
130 self
.current_loop
= 0
132 self
.last_loop
= float('inf')
134 self
.last_loop
= loop
136 with self
.music_lock
:
137 self
.current_audio_segment
= self
.audio_segment
138 self
.current_frame
= int(start_at
* self
.audio_segment
.frame_rate
)
140 db_gain
= gain(self
.volume
, 0)[0]
141 self
.set_gain(-db_gain
)
142 self
.gain_effects
.append(GainEffect(
144 self
.current_audio_segment
,
147 self
.sound_position
+ fade_in
,
152 def on_enter_loaded_playing(self
):
153 self
.mixer
.add_file(self
)
155 def finished_callback(self
):
156 if self
.is_loaded_playing():
158 if self
.is_loaded_stopping():
161 def trigger_stopped_events(self
):
162 self
.mixer
.remove_file(self
)
163 self
.wait_event
.set()
165 def play_callback(self
, out_data_length
, frame_count
):
166 if self
.is_loaded_paused():
167 return b
'\0' * out_data_length
169 with self
.music_lock
:
170 [data
, nb_frames
] = self
.get_next_sample(frame_count
)
171 if nb_frames
< frame_count
:
172 if self
.is_loaded_playing() and\
173 self
.current_loop
< self
.last_loop
:
174 self
.current_loop
+= 1
175 self
.current_frame
= 0
176 [new_data
, new_nb_frames
] = self
.get_next_sample(
177 frame_count
- nb_frames
)
179 nb_frames
+= new_nb_frames
183 name
="MSFinishedCallback",
184 target
=self
.finished_callback
).start()
186 return data
.ljust(out_data_length
, b
'\0')
188 def get_next_sample(self
, frame_count
):
189 fw
= self
.audio_segment_frame_width
194 segment
= self
.current_audio_segment
195 max_val
= int(segment
.frame_count())
197 start_i
= max(self
.current_frame
, 0)
198 end_i
= min(self
.current_frame
+ frame_count
, max_val
)
199 data
+= segment
._data
[start_i
*fw
: end_i
*fw
]
200 nb_frames
+= end_i
- start_i
201 self
.current_frame
+= end_i
- start_i
203 volume_factor
= self
.volume_factor(self
.effects_next_gain(nb_frames
))
205 data
= audioop
.mul(data
, Config
.sample_width
, volume_factor
)
207 return [data
, nb_frames
]
209 def seek(self
, value
=0, delta
=False):
210 # We don't want to do that while stopping
211 if not (self
.is_loaded_playing() or self
.is_loaded_paused()):
213 with self
.music_lock
:
214 self
.abandon_all_effects()
215 self
.current_frame
= max(
217 int(delta
) * self
.current_frame
218 + int(value
* self
.audio_segment
.frame_rate
))
219 # FIXME: si on fait un seek + delta, adapter le "loop"
221 def effects_next_gain(self
, frame_count
):
223 for gain_effect
in self
.gain_effects
:
224 [new_gain
, last_gain
] = gain_effect
.get_next_gain(
229 self
.set_gain(new_gain
)
230 self
.gain_effects
.remove(gain_effect
)
236 def abandon_all_effects(self
):
238 for gain_effect
in self
.gain_effects
:
239 db_gain
+= gain_effect
.get_last_gain()
241 self
.gain_effects
= []
242 self
.set_gain(db_gain
)
244 def stop(self
, fade_out
=0, wait
=False):
245 if self
.is_loaded_playing():
246 ms
= int(self
.sound_position
* 1000)
247 ms_fo
= max(1, int(fade_out
* 1000))
249 new_audio_segment
= self
.current_audio_segment
[: ms
+ms_fo
] \
251 with self
.music_lock
:
252 self
.current_audio_segment
= new_audio_segment
260 def volume_factor(self
, additional_gain
):
261 return 10 ** ( (self
.db_gain
+ additional_gain
) / 20)
263 def set_gain(self
, db_gain
, absolute
=False):
265 self
.db_gain
= db_gain
267 self
.db_gain
+= db_gain
269 def set_volume(self
, value
, delta
=False, fade
=0):
270 [db_gain
, self
.volume
] = gain(
271 value
+ int(delta
) * self
.volume
,
275 self
.gain_effects
.append(GainEffect(
277 self
.current_audio_segment
,
280 self
.sound_position
+ fade
,
283 self
.set_gain(db_gain
)
286 self
.wait_event
.clear()
287 self
.wait_event
.wait()