annotate auditok/io.py @ 94:19300cbbb84d

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