5 from transitions
.extensions
import HierarchicalMachine
as Machine
8 import sounddevice
as sd
11 from .lock
import Lock
12 file_lock
= Lock("file")
14 pyaudio
= pa
.PyAudio()
16 class MusicFile(Machine
):
17 def __init__(self
, filename
, name
= None, gain
= 1):
22 { 'name': 'loaded', 'children': ['stopped', 'playing', 'paused', 'stopping'] }
25 { 'trigger': 'load', 'source': 'initial', 'dest': 'loading'}
,
26 { 'trigger': 'fail', 'source': 'loading', 'dest': 'failed'}
,
27 { 'trigger': 'success', 'source': 'loading', 'dest': 'loaded_stopped'}
,
28 { 'trigger': 'start_playing', 'source': 'loaded_stopped', 'dest': 'loaded_playing'}
,
29 { 'trigger': 'pause', 'source': 'loaded_playing', 'dest': 'loaded_paused'}
,
30 { 'trigger': 'unpause', 'source': 'loaded_paused', 'dest': 'loaded_playing'}
,
31 { 'trigger': 'stop_playing', 'source': ['loaded_playing','loaded_paused'], 'dest': 'loaded_stopping'}
,
32 { 'trigger': 'stopped', 'source': 'loaded_stopping', 'dest': 'loaded_stopped'}
35 Machine
.__init
__(self
, states
=states
, transitions
=transitions
, initial
='initial')
37 self
.filename
= filename
39 self
.name
= name
or filename
40 self
.audio_segment
= None
42 self
.music_lock
= Lock("music__" + filename
)
43 self
.wait_event
= threading
.Event()
45 threading
.Thread(name
= "MSMusicLoad", target
= self
.load
).start()
47 def on_enter_loading(self
):
50 print("Loading « {} »".format(self
.name
))
51 volume_factor
= 20 * math
.log10(self
.gain
)
52 self
.audio_segment
= pydub
.AudioSegment
.from_file(self
.filename
).set_frame_rate(44100).apply_gain(volume_factor
)
53 self
.sound_duration
= self
.audio_segment
.duration_seconds
54 except Exception as e
:
55 print("failed to load « {} »: {}".format(self
.name
, e
))
56 self
.loading_error
= e
60 print("Loaded « {} »".format(self
.name
))
62 def check_is_loaded(self
):
63 return self
.state
.startswith('loaded_')
65 def is_not_stopped(self
):
66 return self
.check_is_loaded() and not self
.is_loaded_stopped()
69 return self
.is_loaded_paused()
72 def sound_position(self
):
73 if self
.is_not_stopped():
74 return self
.current_frame
/ self
.current_audio_segment
.frame_rate
78 def play(self
, fade_in
= 0, volume
= 100, start_at
= 0):
79 self
.db_gain
= self
.volume_to_gain(volume
)
80 ms
= int(start_at
* 1000)
81 ms_fi
= max(1, int(fade_in
* 1000))
83 self
.current_audio_segment
= (self
.audio_segment
+ self
.db_gain
).fade(from_gain
=-120, duration
=ms_fi
, start
=ms
)
84 self
.before_loaded_playing(initial_frame
= int(start_at
* self
.audio_segment
.frame_rate
))
87 def before_loaded_playing(self
, initial_frame
= 0):
88 self
.current_frame
= initial_frame
90 segment
= self
.current_audio_segment
92 self
.stream
= sd
.RawOutputStream(samplerate
=segment
.frame_rate
,
93 channels
=segment
.channels
,
94 dtype
='int' + str(8*segment
.sample_width
), # FIXME: ?
96 callback
=self
.play_callback
,
97 finished_callback
=self
.finished_callback
100 def on_enter_loaded_playing(self
):
103 def on_enter_loaded_paused(self
):
106 def finished_callback(self
):
107 if self
.is_loaded_playing():
109 if self
.is_loaded_stopping():
112 def on_enter_loaded_stopped(self
):
113 self
.wait_event
.set()
115 def play_callback(self
, out_data
, frame_count
, time_info
, status_flags
):
116 with self
.music_lock
:
117 audio_segment
= self
.current_audio_segment
.get_sample_slice_data(
118 start_sample
=self
.current_frame
,
119 end_sample
=self
.current_frame
+ frame_count
121 self
.current_frame
+= frame_count
122 if len(audio_segment
) == 0:
123 raise sd
.CallbackStop
125 out_data
[:] = audio_segment
.ljust(len(out_data
), b
'\0')
127 def stop(self
, fade_out
= 0):
128 if self
.is_loaded_playing():
129 ms
= int(self
.sound_position
* 1000)
130 ms_fo
= max(1, int(fade_out
* 1000))
132 with self
.music_lock
:
133 self
.current_audio_segment
= self
.current_audio_segment
[:ms
+ ms_fo
].fade_out(ms_fo
)
139 def set_volume(self
, value
):
140 if self
.is_loaded_stopped():
143 db_gain
= self
.volume_to_gain(value
)
144 new_audio_segment
= self
.current_audio_segment
+ (db_gain
- self
.db_gain
)
145 self
.db_gain
= db_gain
146 with self
.music_lock
:
147 self
.current_audio_segment
= new_audio_segment
149 def volume_to_gain(self
, volume
):
150 return 20 * math
.log10(max(volume
, 0.0001) / 100)
153 self
.wait_event
.clear()
154 self
.wait_event
.wait()
156 # Add some more functions to AudioSegments
157 def get_sample_slice_data(self
, start_sample
=0, end_sample
=float('inf')):
158 max_val
= int(self
.frame_count())
160 start_i
= max(start_sample
, 0) * self
.frame_width
161 end_i
= min(end_sample
, max_val
) * self
.frame_width
163 return self
._data
[start_i
:end_i
]
165 pydub
.AudioSegment
.get_sample_slice_data
= get_sample_slice_data