annotate auditok/io.py @ 2:edee860b9f61

First release on Github
author Amine Sehili <amine.sehili@gmail.com>
date Thu, 17 Sep 2015 22:01:30 +0200
parents
children 252d698ae642
rev   line source
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