4 from transitions
.extensions
import HierarchicalMachine
as Machine
7 import sounddevice
as sd
10 from .lock
import Lock
13 file_lock
= Lock("file")
15 pyaudio
= pa
.PyAudio()
17 class MusicFile(Machine
):
18 def __init__(self
, filename
, mapping
, name
= None, gain
= 1):
23 { 'name': 'loaded', 'children': ['stopped', 'playing', 'paused', 'stopping'] }
26 { 'trigger': 'load', 'source': 'initial', 'dest': 'loading'}
,
27 { 'trigger': 'fail', 'source': 'loading', 'dest': 'failed'}
,
28 { 'trigger': 'success', 'source': 'loading', 'dest': 'loaded_stopped'}
,
29 { 'trigger': 'start_playing', 'source': 'loaded_stopped', 'dest': 'loaded_playing'}
,
30 { 'trigger': 'pause', 'source': 'loaded_playing', 'dest': 'loaded_paused'}
,
31 { 'trigger': 'unpause', 'source': 'loaded_paused', 'dest': 'loaded_playing'}
,
32 { 'trigger': 'stop_playing', 'source': ['loaded_playing','loaded_paused'], 'dest': 'loaded_stopping'}
,
33 { 'trigger': 'stopped', 'source': 'loaded_stopping', 'dest': 'loaded_stopped'}
36 Machine
.__init
__(self
, states
=states
, transitions
=transitions
, initial
='initial')
39 self
.mapping
= mapping
40 self
.filename
= filename
42 self
.name
= name
or filename
43 self
.audio_segment
= None
44 self
.volume_factor
= gain
45 self
.music_lock
= Lock("music__" + filename
)
46 self
.wait_event
= threading
.Event()
48 threading
.Thread(name
= "MSMusicLoad", target
= self
.load
).start()
50 def on_enter_loading(self
):
53 print("Loading « {} »".format(self
.name
))
54 db_gain
= gain(self
.volume_factor
* 100)
55 self
.audio_segment
= pydub
.AudioSegment
.from_file(self
.filename
).set_frame_rate(44100).apply_gain(db_gain
)
56 self
.sound_duration
= self
.audio_segment
.duration_seconds
57 except Exception as e
:
58 print("failed to load « {} »: {}".format(self
.name
, e
))
59 self
.loading_error
= e
63 print("Loaded « {} »".format(self
.name
))
65 def check_is_loaded(self
):
66 return self
.state
.startswith('loaded_')
68 def is_not_stopped(self
):
69 return self
.check_is_loaded() and not self
.is_loaded_stopped()
72 return self
.is_loaded_paused()
75 def sound_position(self
):
76 if self
.is_not_stopped():
77 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
81 def play(self
, fade_in
= 0, volume
= 100, loop
= 0, start_at
= 0):
82 db_gain
= gain(volume
) + self
.mapping
.master_gain
86 ms
= int(start_at
* 1000)
87 ms_fi
= max(1, int(fade_in
* 1000))
89 self
.current_audio_segment
= (self
.audio_segment
+ db_gain
).fade(from_gain
=-120, duration
=ms_fi
, start
=ms
)
90 self
.initial_frame
= int(start_at
* self
.audio_segment
.frame_rate
)
91 self
.before_loaded_playing()
94 def before_loaded_playing(self
):
95 self
.current_frame
= self
.initial_frame
97 segment
= self
.current_audio_segment
99 self
.stream
= sd
.RawOutputStream(samplerate
=segment
.frame_rate
,
100 channels
=segment
.channels
,
101 dtype
='int' + str(8*segment
.sample_width
), # FIXME: ?
103 callback
=self
.play_callback
,
104 finished_callback
=self
.finished_callback
107 def on_enter_loaded_playing(self
):
110 def on_enter_loaded_paused(self
):
113 def finished_callback(self
):
114 if self
.is_loaded_playing():
116 if self
.is_loaded_stopping():
119 def on_enter_loaded_stopped(self
):
120 self
.wait_event
.set()
122 def play_callback(self
, out_data
, frame_count
, time_info
, status_flags
):
123 with self
.music_lock
:
124 audio_segment
= self
.current_audio_segment
.get_sample_slice_data(
125 start_sample
=self
.current_frame
,
126 end_sample
=self
.current_frame
+ frame_count
128 self
.current_frame
+= frame_count
129 if len(audio_segment
) == 0:
130 if self
.is_loaded_playing() and self
.loop
!= 0:
132 self
.current_frame
= self
.initial_frame
134 raise sd
.CallbackStop
136 out_data
[:] = audio_segment
.ljust(len(out_data
), b
'\0')
138 def seek(self
, value
= 0, delta
= False):
139 # We don't want to do that while stopping
140 if not (self
.is_loaded_playing() or self
.is_loaded_paused()):
142 with self
.music_lock
:
143 self
.current_frame
= max(0, int(delta
) * self
.current_frame
+ int(value
* self
.audio_segment
.frame_rate
))
145 def stop(self
, fade_out
= 0, wait
= False):
146 if self
.is_loaded_playing():
147 ms
= int(self
.sound_position
* 1000)
148 ms_fo
= max(1, int(fade_out
* 1000))
150 with self
.music_lock
:
151 self
.current_audio_segment
= self
.current_audio_segment
[:ms
+ ms_fo
].fade_out(ms_fo
)
159 def set_gain(self
, db_gain
):
160 if not self
.is_not_stopped():
163 new_audio_segment
= self
.current_audio_segment
+ db_gain
165 with self
.music_lock
:
166 self
.current_audio_segment
= new_audio_segment
168 def set_volume(self
, value
, delta
= False):
169 [db_gain
, self
.volume
] = gain(value
+ int(delta
) * self
.volume
, self
.volume
)
171 self
.set_gain(db_gain
)
174 self
.wait_event
.clear()
175 self
.wait_event
.wait()
177 # Add some more functions to AudioSegments
178 def get_sample_slice_data(self
, start_sample
=0, end_sample
=float('inf')):
179 max_val
= int(self
.frame_count())
181 start_i
= max(start_sample
, 0) * self
.frame_width
182 end_i
= min(end_sample
, max_val
) * self
.frame_width
184 return self
._data
[start_i
:end_i
]
186 pydub
.AudioSegment
.get_sample_slice_data
= get_sample_slice_data