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
14 file_lock
= Lock("file")
16 class MusicFile(Machine
):
17 def __init__(self
, filename
, mapping
, name
=None, gain
=1):
24 'children': ['stopped', 'playing', 'paused', 'stopping']
41 'dest': 'loaded_stopped'
44 'trigger': 'start_playing',
45 'source': 'loaded_stopped',
46 'dest': 'loaded_playing'
50 'source': 'loaded_playing',
51 'dest': 'loaded_paused'
55 'source': 'loaded_paused',
56 'dest': 'loaded_playing'
59 'trigger': 'stop_playing',
60 'source': ['loaded_playing','loaded_paused'],
61 'dest': 'loaded_stopping'
65 'source': 'loaded_stopping',
66 'dest': 'loaded_stopped',
67 'after': 'trigger_stopped_events'
71 Machine
.__init
__(self
, states
=states
,
72 transitions
=transitions
, initial
='initial')
75 self
.mapping
= mapping
76 self
.filename
= filename
77 self
.name
= name
or filename
78 self
.audio_segment
= None
79 self
.audio_segment_frame_width
= 0
80 self
.initial_volume_factor
= gain
81 self
.music_lock
= Lock("music__" + filename
)
82 self
.wait_event
= threading
.Event()
85 threading
.Thread(name
="MSMusicLoad", target
=self
.load
).start()
87 def on_enter_loading(self
):
90 debug_print("Loading « {} »".format(self
.name
))
91 self
.mixer
= self
.mapping
.mixer
or Mixer()
92 initial_db_gain
= gain(self
.initial_volume_factor
* 100)
93 self
.audio_segment
= pydub
.AudioSegment \
94 .from_file(self
.filename
) \
95 .set_frame_rate(Config
.frame_rate
) \
96 .set_channels(Config
.channels
) \
97 .set_sample_width(Config
.sample_width
) \
98 .apply_gain(initial_db_gain
)
99 self
.audio_segment_frame_width
= self
.audio_segment
.frame_width
100 self
.sound_duration
= self
.audio_segment
.duration_seconds
101 except Exception as e
:
102 error_print("failed to load « {} »: {}".format(self
.name
, e
))
103 self
.loading_error
= e
107 debug_print("Loaded « {} »".format(self
.name
))
109 def check_is_loaded(self
):
110 return self
.state
.startswith('loaded_')
112 def is_not_stopped(self
):
113 return self
.check_is_loaded() and not self
.is_loaded_stopped()
116 return self
.is_loaded_paused()
119 def sound_position(self
):
120 if self
.is_not_stopped():
121 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
125 def play(self
, fade_in
=0, volume
=100, loop
=0, start_at
=0):
126 self
.db_gain
= gain(volume
) + self
.mapping
.master_gain
130 ms
= int(start_at
* 1000)
131 ms_fi
= int(fade_in
* 1000)
132 with self
.music_lock
:
133 self
.current_audio_segment
= self
.audio_segment
134 self
.current_frame
= int(start_at
* self
.audio_segment
.frame_rate
)
136 # FIXME: apply it to repeated when looping?
137 self
.a_s_with_effect
= self \
138 .current_audio_segment
[ms
: ms
+ms_fi
] \
140 self
.current_frame_with_effect
= 0
142 self
.a_s_with_effect
= None
146 def on_enter_loaded_playing(self
):
147 self
.mixer
.add_file(self
)
149 def finished_callback(self
):
150 if self
.is_loaded_playing():
152 if self
.is_loaded_stopping():
155 def trigger_stopped_events(self
):
156 self
.mixer
.remove_file(self
)
157 self
.wait_event
.set()
159 def play_callback(self
, out_data_length
, frame_count
):
160 if self
.is_loaded_paused():
161 return b
'\0' * out_data_length
163 with self
.music_lock
:
164 [data
, nb_frames
] = self
.get_next_sample(frame_count
)
165 if nb_frames
< frame_count
:
166 if self
.is_loaded_playing() and self
.loop
!= 0:
168 self
.current_frame
= 0
169 [new_data
, new_nb_frames
] = self
.get_next_sample(
170 frame_count
- nb_frames
)
172 nb_frames
+= new_nb_frames
176 name
="MSFinishedCallback",
177 target
=self
.finished_callback
).start()
179 return data
.ljust(out_data_length
, b
'\0')
181 def get_next_sample(self
, frame_count
):
182 fw
= self
.audio_segment_frame_width
186 if self
.a_s_with_effect
is not None:
187 segment
= self
.a_s_with_effect
188 max_val
= int(segment
.frame_count())
190 start_i
= max(self
.current_frame_with_effect
, 0)
191 end_i
= min(self
.current_frame_with_effect
+ frame_count
, max_val
)
193 data
+= segment
._data
[start_i
*fw
: end_i
*fw
]
197 self
.current_frame_with_effect
+ frame_count
- max_val
)
199 self
.current_frame_with_effect
+= end_i
- start_i
200 self
.current_frame
+= end_i
- start_i
201 nb_frames
+= end_i
- start_i
204 self
.a_s_with_effect
= None
206 segment
= self
.current_audio_segment
207 max_val
= int(segment
.frame_count())
209 start_i
= max(self
.current_frame
, 0)
210 end_i
= min(self
.current_frame
+ frame_count
, max_val
)
211 data
+= segment
._data
[start_i
*fw
: end_i
*fw
]
212 nb_frames
+= end_i
- start_i
213 self
.current_frame
+= end_i
- start_i
215 data
= audioop
.mul(data
, Config
.sample_width
, self
.volume_factor
)
217 return [data
, nb_frames
]
219 def seek(self
, value
=0, delta
=False):
220 # We don't want to do that while stopping
221 if not (self
.is_loaded_playing() or self
.is_loaded_paused()):
223 with self
.music_lock
:
224 self
.a_s_with_effect
= None
225 self
.current_frame
= max(
227 int(delta
) * self
.current_frame
228 + int(value
* self
.audio_segment
.frame_rate
))
229 # FIXME: si on fait un seek + delta, adapter le "loop"
231 def stop(self
, fade_out
=0, wait
=False):
232 if self
.is_loaded_playing():
233 ms
= int(self
.sound_position
* 1000)
234 ms_fo
= max(1, int(fade_out
* 1000))
236 new_audio_segment
= self
.current_audio_segment
[: ms
+ms_fo
] \
238 with self
.music_lock
:
239 self
.current_audio_segment
= new_audio_segment
247 def set_gain(self
, db_gain
):
248 self
.db_gain
+= db_gain
249 self
.volume_factor
= 10 ** (self
.db_gain
/ 20)
251 def set_volume(self
, value
, delta
=False):
252 [db_gain
, self
.volume
] = gain(
253 value
+ int(delta
) * self
.volume
,
256 self
.set_gain(db_gain
)
259 self
.wait_event
.clear()
260 self
.wait_event
.wait()