amine@2
|
1 """
|
amine@2
|
2 Module for low-level audio input-output operations.
|
amine@2
|
3
|
amine@2
|
4 September 2015
|
amine@2
|
5 @author: Amine SEHILI <amine.sehili@gmail.com>
|
amine@2
|
6 """
|
amine@2
|
7
|
amine@2
|
8 from abc import ABCMeta, abstractmethod
|
amine@2
|
9 import wave
|
amine@2
|
10
|
amine@2
|
11 __all__ = ["AudioSource", "Rewindable", "BufferAudioSource", "WaveAudioSource",
|
amine@2
|
12 "PyAudioSource", "PyAudioPlayer", "from_file", "player_for"]
|
amine@2
|
13
|
amine@2
|
14 DEFAULT_SAMPLE_RATE = 16000
|
amine@2
|
15 DEFAULT_SAMPLE_WIDTH = 2
|
amine@2
|
16 DEFAULT_NB_CHANNELS = 1
|
amine@2
|
17
|
amine@2
|
18
|
amine@2
|
19 class AudioSource():
|
amine@2
|
20 __metaclass__ = ABCMeta
|
amine@2
|
21
|
amine@2
|
22 """
|
amine@2
|
23 Base class for audio source.
|
amine@2
|
24
|
amine@2
|
25 Subclasses should implement methods to open/close and audio stream
|
amine@2
|
26 and read the desired amount of audio samples.
|
amine@2
|
27
|
amine@2
|
28 """
|
amine@2
|
29
|
amine@2
|
30 def __init__(self, sampling_rate = DEFAULT_SAMPLE_RATE,
|
amine@2
|
31 sample_width = DEFAULT_SAMPLE_WIDTH,
|
amine@2
|
32 channels = DEFAULT_NB_CHANNELS):
|
amine@2
|
33
|
amine@2
|
34 """
|
amine@2
|
35
|
amine@2
|
36 Parameters
|
amine@2
|
37 ----------
|
amine@2
|
38
|
amine@2
|
39 `sampling_rate` *(int)* :
|
amine@2
|
40 Number of samples per second of audio stream. Default = 16000.
|
amine@2
|
41
|
amine@2
|
42 `sample_width` *(int)* :
|
amine@2
|
43 Size in bytes of one audio sample. Possible values : 1, 2, 4.
|
amine@2
|
44 Default = 2.
|
amine@2
|
45
|
amine@2
|
46 `channels` *(int)* :
|
amine@2
|
47 Number of channels of audio stream. The current version supports
|
amine@2
|
48 only mono audio streams (i.e. one channel).
|
amine@2
|
49
|
amine@2
|
50 """
|
amine@2
|
51
|
amine@2
|
52 if not sample_width in (1, 2, 4):
|
amine@2
|
53 raise ValueError("Sample width must be one of: 1, 2 or 4 (bytes)")
|
amine@2
|
54
|
amine@2
|
55 if channels != 1:
|
amine@2
|
56 raise ValueError("Only mono audio is currently handled")
|
amine@2
|
57
|
amine@2
|
58 self.sampling_rate = sampling_rate
|
amine@2
|
59 self.sample_width = sample_width
|
amine@2
|
60 self.channels = channels
|
amine@2
|
61
|
amine@2
|
62 @abstractmethod
|
amine@2
|
63 def is_open(self):
|
amine@2
|
64 """ Return True if audio source is open, False otherwise """
|
amine@2
|
65
|
amine@2
|
66 @abstractmethod
|
amine@2
|
67 def open(self):
|
amine@2
|
68 """ Open audio source """
|
amine@2
|
69
|
amine@2
|
70 @abstractmethod
|
amine@2
|
71 def close(self):
|
amine@2
|
72 """ Close audio source """
|
amine@2
|
73
|
amine@2
|
74 @abstractmethod
|
amine@2
|
75 def read(self, size):
|
amine@2
|
76 """
|
amine@2
|
77 Read and return `size` audio samples at most.
|
amine@2
|
78
|
amine@2
|
79 Parameters
|
amine@2
|
80 ----------
|
amine@2
|
81 `size` : *(int)* :
|
amine@2
|
82 the number of samples to read.
|
amine@2
|
83
|
amine@2
|
84 Returns
|
amine@2
|
85 --------
|
amine@2
|
86 Audio data as a string of length 'N' * 'smaple_width' * 'channels', where 'N' is:
|
amine@2
|
87
|
amine@2
|
88 `size` if `size` < 'left_samples'
|
amine@2
|
89
|
amine@2
|
90 'left_samples' if `size` > 'left_samples'
|
amine@2
|
91
|
amine@2
|
92 """
|
amine@2
|
93
|
amine@2
|
94 def get_sampling_rate(self):
|
amine@2
|
95 """ Return the number of samples per second of audio stream """
|
amine@2
|
96 return self.sampling_rate
|
amine@2
|
97
|
amine@2
|
98 def get_sample_width(self):
|
amine@2
|
99 """ Return the number of bytes used to represent one audio sample """
|
amine@2
|
100 return self.sample_width
|
amine@2
|
101
|
amine@2
|
102 def get_channels(self):
|
amine@2
|
103 """ Return the number of channels of this audio source """
|
amine@2
|
104 return self.channels
|
amine@2
|
105
|
amine@2
|
106
|
amine@2
|
107
|
amine@2
|
108 class Rewindable():
|
amine@2
|
109 __metaclass__ = ABCMeta
|
amine@2
|
110
|
amine@2
|
111 """
|
amine@2
|
112 Base class for rewindable audio streams.
|
amine@2
|
113 Subclasses should implement methods to return to the beginning of an
|
amine@2
|
114 audio stream as well as method to move to an absolute audio position
|
amine@2
|
115 expressed in time or in number of samples.
|
amine@2
|
116
|
amine@2
|
117 """
|
amine@2
|
118
|
amine@2
|
119 @abstractmethod
|
amine@2
|
120 def rewind(self):
|
amine@2
|
121 """ Go back to the beginning of audio stream """
|
amine@2
|
122 pass
|
amine@2
|
123
|
amine@2
|
124 @abstractmethod
|
amine@2
|
125 def get_position(self):
|
amine@2
|
126 """ Return the total number of already read samples """
|
amine@2
|
127
|
amine@2
|
128 @abstractmethod
|
amine@2
|
129 def get_time_position(self):
|
amine@2
|
130 """ Return the total duration in seconds of already read data """
|
amine@2
|
131
|
amine@2
|
132 @abstractmethod
|
amine@2
|
133 def set_position(self, position):
|
amine@2
|
134 """ Move to an absolute position
|
amine@2
|
135
|
amine@2
|
136 Parameters
|
amine@2
|
137 ----------
|
amine@2
|
138 `position` : *(int)*
|
amine@2
|
139 number of samples to skip from the start of the stream
|
amine@2
|
140 """
|
amine@2
|
141
|
amine@2
|
142 @abstractmethod
|
amine@2
|
143 def set_time_position(self, time_position):
|
amine@2
|
144 """ Move to an absolute position expressed in seconds
|
amine@2
|
145
|
amine@2
|
146 Parameters
|
amine@2
|
147 ----------
|
amine@2
|
148 `time_position` : *(float)*
|
amine@2
|
149 seconds to skip from the start of the stream
|
amine@2
|
150 """
|
amine@2
|
151 pass
|
amine@2
|
152
|
amine@2
|
153
|
amine@2
|
154
|
amine@2
|
155 class BufferAudioSource(AudioSource, Rewindable):
|
amine@2
|
156
|
amine@2
|
157 """
|
amine@2
|
158 A class that represent audio data as a memory buffer. It implements
|
amine@2
|
159 methods from `io.Rewindable` and is therefore a navigable `io.AudioSource`.
|
amine@2
|
160 """
|
amine@2
|
161
|
amine@2
|
162 def __init__(self, data_buffer,
|
amine@2
|
163 sampling_rate = DEFAULT_SAMPLE_RATE,
|
amine@2
|
164 sample_width = DEFAULT_SAMPLE_WIDTH,
|
amine@2
|
165 channels = DEFAULT_NB_CHANNELS):
|
amine@2
|
166
|
amine@2
|
167 if len(data_buffer) % (sample_width * channels) !=0:
|
amine@2
|
168 raise ValueError("length of data_buffer must be a multiple of (sample_width * channels)")
|
amine@2
|
169
|
amine@2
|
170 AudioSource.__init__(self, sampling_rate, sample_width, channels)
|
amine@2
|
171 self._buffer = data_buffer
|
amine@2
|
172 self._index = 0
|
amine@2
|
173 self._left = 0 if self._buffer is None else len(self._buffer)
|
amine@2
|
174 self._is_open = False
|
amine@2
|
175
|
amine@2
|
176 def is_open(self):
|
amine@2
|
177 return self._is_open
|
amine@2
|
178
|
amine@2
|
179 def open(self):
|
amine@2
|
180 self._is_open = True
|
amine@2
|
181
|
amine@2
|
182 def close(self):
|
amine@2
|
183 self._is_open = False
|
amine@2
|
184 self.rewind()
|
amine@2
|
185
|
amine@2
|
186 def read(self, size=None):
|
amine@2
|
187
|
amine@2
|
188 if not self._is_open:
|
amine@2
|
189 raise IOError("Stream is not open")
|
amine@2
|
190
|
amine@2
|
191 if self._left > 0:
|
amine@2
|
192
|
amine@2
|
193 to_read = size * self.sample_width * self.channels
|
amine@2
|
194 if to_read > self._left:
|
amine@2
|
195 to_read = self._left
|
amine@2
|
196
|
amine@2
|
197 data = self._buffer[self._index: self._index + to_read]
|
amine@2
|
198 self._index += to_read
|
amine@2
|
199 self._left -= to_read
|
amine@2
|
200
|
amine@2
|
201 return data
|
amine@2
|
202
|
amine@2
|
203 return None
|
amine@2
|
204
|
amine@2
|
205 def get_data_buffer(self):
|
amine@2
|
206 """ Return all audio data as one string buffer. """
|
amine@2
|
207 return self._buffer
|
amine@2
|
208
|
amine@2
|
209 def set_data(self, data_buffer):
|
amine@2
|
210 """ Set new data for this audio stream.
|
amine@2
|
211
|
amine@2
|
212 Parameters
|
amine@2
|
213 ----------
|
amine@2
|
214 `data_buffer` :
|
amine@2
|
215 a string buffer with a length multiple of (sample_width * channels)
|
amine@2
|
216 """
|
amine@2
|
217 if len(data_buffer) % (self.sample_width * self.channels) !=0:
|
amine@2
|
218 raise ValueError("length of data_buffer must be a multiple of (sample_width * channels)")
|
amine@2
|
219 self._buffer = data_buffer
|
amine@2
|
220 self._index = 0
|
amine@2
|
221 self._left = 0 if self._buffer is None else len(self._buffer)
|
amine@2
|
222
|
amine@2
|
223 def append_data(self, data_buffer):
|
amine@2
|
224 """ Append data to this audio stream
|
amine@2
|
225
|
amine@2
|
226 Parameters
|
amine@2
|
227 ----------
|
amine@2
|
228 `data_buffer` :
|
amine@2
|
229 a string buffer with a length multiple of (sample_width * channels)
|
amine@2
|
230
|
amine@2
|
231 """
|
amine@2
|
232
|
amine@2
|
233 if len(data_buffer) % (self.sample_width * self.channels) !=0:
|
amine@2
|
234 raise ValueError("length of data_buffer must be a multiple of (sample_width * channels)")
|
amine@2
|
235
|
amine@2
|
236 self._buffer += data_buffer
|
amine@2
|
237 self._left += len(data_buffer)
|
amine@2
|
238
|
amine@2
|
239
|
amine@2
|
240 def rewind(self):
|
amine@2
|
241 self.set_position(0)
|
amine@2
|
242
|
amine@2
|
243 def get_position(self):
|
amine@2
|
244 return self._index / self.sample_width
|
amine@2
|
245
|
amine@2
|
246 def get_time_position(self):
|
amine@2
|
247 return float(self._index) / (self.sample_width * self.sampling_rate)
|
amine@2
|
248
|
amine@2
|
249 def set_position(self, position):
|
amine@2
|
250 if position < 0:
|
amine@2
|
251 raise ValueError("position must be >= 0")
|
amine@2
|
252
|
amine@2
|
253 if self._buffer is None:
|
amine@2
|
254 self._index = 0
|
amine@2
|
255 self._left = 0
|
amine@2
|
256 return
|
amine@2
|
257
|
amine@2
|
258 position *= self.sample_width
|
amine@2
|
259 self._index = position if position < len(self._buffer) else len(self._buffer)
|
amine@2
|
260 self._left = len(self._buffer) - self._index
|
amine@2
|
261
|
amine@2
|
262
|
amine@2
|
263 def set_time_position(self, time_position): # time in seconds
|
amine@2
|
264
|
amine@2
|
265 position = int(self.sampling_rate * time_position)
|
amine@2
|
266 self.set_position(position)
|
amine@2
|
267
|
amine@2
|
268
|
amine@2
|
269
|
amine@2
|
270
|
amine@2
|
271 class WaveAudioSource(AudioSource):
|
amine@2
|
272
|
amine@2
|
273 """ A class for an `AudioSource` that reads data from a wave file. """
|
amine@2
|
274
|
amine@2
|
275 def __init__(self, filename):
|
amine@2
|
276
|
amine@2
|
277 """
|
amine@2
|
278 Parameters
|
amine@2
|
279 ----------
|
amine@2
|
280 `filename` :
|
amine@2
|
281 path to a valid wave file
|
amine@2
|
282
|
amine@2
|
283 """
|
amine@2
|
284
|
amine@2
|
285 self._filename = filename
|
amine@2
|
286 self._audio_stream = None
|
amine@2
|
287
|
amine@2
|
288 stream = wave.open(self._filename)
|
amine@2
|
289 AudioSource.__init__(self, stream.getframerate(),
|
amine@2
|
290 stream.getsampwidth(),
|
amine@2
|
291 stream.getnchannels())
|
amine@2
|
292 stream.close()
|
amine@2
|
293
|
amine@2
|
294
|
amine@2
|
295 def is_open(self):
|
amine@2
|
296 return self._audio_stream is not None
|
amine@2
|
297
|
amine@2
|
298 def open(self):
|
amine@2
|
299 if(self._audio_stream is None):
|
amine@2
|
300 self._audio_stream = wave.open(self._filename)
|
amine@2
|
301
|
amine@2
|
302
|
amine@2
|
303 def close(self):
|
amine@2
|
304 if self._audio_stream is not None:
|
amine@2
|
305 self._audio_stream.close()
|
amine@2
|
306 self._audio_stream = None
|
amine@2
|
307
|
amine@2
|
308
|
amine@2
|
309 def read(self, size):
|
amine@2
|
310
|
amine@2
|
311 if self._audio_stream is None:
|
amine@2
|
312 raise IOError("Stream is not open")
|
amine@2
|
313 else:
|
amine@2
|
314 data = self._audio_stream.readframes(size)
|
amine@2
|
315 if data is None or len(data) < 1:
|
amine@2
|
316 return None
|
amine@2
|
317 return data
|
amine@2
|
318
|
amine@2
|
319
|
amine@2
|
320 class PyAudioSource(AudioSource):
|
amine@2
|
321
|
amine@2
|
322 """ A class for an `AudioSource` that reads data the built-in microphone. """
|
amine@2
|
323
|
amine@2
|
324 def __init__(self, sampling_rate = DEFAULT_SAMPLE_RATE,
|
amine@2
|
325 sample_width = DEFAULT_SAMPLE_WIDTH,
|
amine@2
|
326 channels = DEFAULT_NB_CHANNELS,
|
amine@2
|
327 frames_per_buffer = 1024):
|
amine@2
|
328
|
amine@2
|
329
|
amine@2
|
330 AudioSource.__init__(self, sampling_rate, sample_width, channels)
|
amine@2
|
331 self._chunk_size = frames_per_buffer
|
amine@2
|
332
|
amine@2
|
333 import pyaudio
|
amine@2
|
334 self._pyaudio_object = pyaudio.PyAudio()
|
amine@2
|
335 self._pyaudio_format = self._pyaudio_object.get_format_from_width(self.sample_width)
|
amine@2
|
336 self._audio_stream = None
|
amine@2
|
337
|
amine@2
|
338
|
amine@2
|
339 def is_open(self):
|
amine@2
|
340 return self._audio_stream is not None
|
amine@2
|
341
|
amine@2
|
342 def open(self):
|
amine@2
|
343 self._audio_stream = self._pyaudio_object.open(format = self._pyaudio_format,
|
amine@2
|
344 channels = self.channels,
|
amine@2
|
345 rate = self.sampling_rate,
|
amine@2
|
346 input = True,
|
amine@2
|
347 output = False,
|
amine@2
|
348 frames_per_buffer = self._chunk_size)
|
amine@2
|
349
|
amine@2
|
350
|
amine@2
|
351 def close(self):
|
amine@2
|
352 if self._audio_stream is not None:
|
amine@2
|
353 self._audio_stream.stop_stream()
|
amine@2
|
354 self._audio_stream.close()
|
amine@2
|
355 self._audio_stream = None
|
amine@2
|
356
|
amine@2
|
357
|
amine@2
|
358 def read(self, size):
|
amine@2
|
359
|
amine@2
|
360 if self._audio_stream is None:
|
amine@2
|
361 raise IOError("Stream is not open")
|
amine@2
|
362
|
amine@2
|
363 if self._audio_stream.is_active():
|
amine@2
|
364 data = self._audio_stream.read(size)
|
amine@2
|
365 if data is None or len(data) < 1:
|
amine@2
|
366 return None
|
amine@2
|
367 return data
|
amine@2
|
368
|
amine@2
|
369 return None
|
amine@2
|
370
|
amine@2
|
371
|
amine@2
|
372
|
amine@2
|
373 class PyAudioPlayer():
|
amine@2
|
374 """ A class for audio playback """
|
amine@2
|
375
|
amine@2
|
376 def __init__(self, sampling_rate = DEFAULT_SAMPLE_RATE,
|
amine@2
|
377 sample_width = DEFAULT_SAMPLE_WIDTH,
|
amine@2
|
378 channels = DEFAULT_NB_CHANNELS):
|
amine@2
|
379
|
amine@2
|
380
|
amine@2
|
381 if not sample_width in (1, 2, 4):
|
amine@2
|
382 raise ValueError("Sample width must be one of: 1, 2 or 4 (bytes)")
|
amine@2
|
383
|
amine@2
|
384 self.sampling_rate = sampling_rate
|
amine@2
|
385 self.sample_width = sample_width
|
amine@2
|
386 self.channels = channels
|
amine@2
|
387
|
amine@2
|
388 import pyaudio
|
amine@2
|
389 self._p = pyaudio.PyAudio()
|
amine@2
|
390 self.stream = self._p.open(format = self._p.get_format_from_width(self.sample_width),
|
amine@2
|
391 channels = self.channels, rate = self.sampling_rate,
|
amine@2
|
392 input = False, output = True)
|
amine@2
|
393
|
amine@2
|
394 def play(self, data):
|
amine@2
|
395 if self.stream.is_stopped():
|
amine@2
|
396 self.stream.start_stream()
|
amine@2
|
397 self.stream.write(data)
|
amine@2
|
398 self.stream.stop_stream()
|
amine@2
|
399
|
amine@2
|
400
|
amine@2
|
401 def stop(self):
|
amine@2
|
402 if not self.stream.is_stopped():
|
amine@2
|
403 self.stream.stop_stream()
|
amine@2
|
404 self.stream.close()
|
amine@2
|
405 self._p.terminate()
|
amine@2
|
406
|
amine@2
|
407
|
amine@2
|
408
|
amine@2
|
409
|
amine@2
|
410 def from_file(filename):
|
amine@2
|
411
|
amine@2
|
412 """
|
amine@2
|
413 Create an `AudioSource` object using the audio file specified by `filename`.
|
amine@2
|
414 The appropriate `AudioSource` class is guessed from file's extension.
|
amine@2
|
415
|
amine@2
|
416 Parameters
|
amine@2
|
417 ----------
|
amine@2
|
418 `filename` :
|
amine@2
|
419 path to an audio file
|
amine@2
|
420
|
amine@2
|
421 Returns
|
amine@2
|
422 -------
|
amine@2
|
423 an `AudioSource` object that reads data from the given file.
|
amine@2
|
424
|
amine@2
|
425 """
|
amine@2
|
426
|
amine@2
|
427 if filename.lower().endswith(".wav"):
|
amine@2
|
428 return WaveAudioSource(filename)
|
amine@2
|
429
|
amine@2
|
430 raise Exception("Can not create an AudioSource object from '%s'" %(filename))
|
amine@2
|
431
|
amine@2
|
432
|
amine@2
|
433 def player_for(audio_source):
|
amine@2
|
434 """
|
amine@2
|
435 Return a `PyAudioPlayer` that can play data from `audio_source`.
|
amine@2
|
436
|
amine@2
|
437 Parameters
|
amine@2
|
438 ----------
|
amine@2
|
439 `audio_source` :
|
amine@2
|
440 an `AudioSource` object.
|
amine@2
|
441
|
amine@2
|
442 Returns
|
amine@2
|
443 -------
|
amine@2
|
444 `PyAudioPlayer` that has the same sampling rate, sample width and number of channels
|
amine@2
|
445 as `audio_source`.
|
amine@2
|
446 """
|
amine@2
|
447
|
amine@2
|
448 return PyAudioPlayer(audio_source.get_sampling_rate(),
|
amine@2
|
449 audio_source.get_sample_width(),
|
amine@2
|
450 audio_source.get_channels())
|
amine@2
|
451
|
amine@2
|
452
|
amine@2
|
453
|