Coverage for src / audioio / playaudio.py: 40%

811 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-17 21:34 +0000

1"""Play numpy arrays as audio. 

2 

3Accepted data for playback are 1-D or 2-D (frames, channels) numpy 

4arrays with values ranging from -1 to 1. 

5If necessary data are downsampled automatically to match supported 

6sampling rates. 

7 

8## Class 

9 

10Use the `PlayAudio` class for audio output to a speaker: 

11 

12``` 

13with PlayAudio() as audio: 

14 audio.beep() 

15``` 

16 

17or without context management: 

18 

19``` 

20audio = PlayAudio() 

21audio.beep(1.0, 'a4') 

22audio.close() 

23``` 

24 

25## Functions 

26 

27Alternatively, the globally defined functions `play()` and `beep()` 

28use the global instance `handle` of the `PlayAudio` class to play a 

29sound on the default audio output device. 

30 

31- `play()`: playback audio data. 

32- `beep()`: playback a tone. 

33- `close()`: close the global PlayAudio instance. 

34 

35 

36## Helper functions 

37 

38- `speaker_devices()`: query available output devices. 

39- `print_speaker_devices()`: print available output devices. 

40- `fade_in()`: fade in a signal in place. 

41- `fade_out()`: fade out a signal in place. 

42- `fade()`: fade in and out a signal in place. 

43- `note2freq()`: convert textual note to corresponding frequency. 

44 

45 

46## Installation 

47 

48You might need to install additional packages for better audio output. 

49See [installation](https://bendalab.github.io/audioio/installation) 

50for further instructions. 

51 

52 

53## Demo 

54 

55For a demo, run the script as: 

56``` 

57python -m src.audioio.playaudio 

58``` 

59 

60""" 

61 

62import os 

63import warnings 

64import numpy as np 

65 

66from sys import platform 

67from scipy.signal import decimate 

68from time import sleep 

69from io import BytesIO 

70from multiprocessing import Process 

71 

72from .audiomodules import * 

73 

74 

75handle = None 

76"""Default audio device handler. 

77 

78Defaults to `None`. Will get a PlayAudio instance assigned via 

79`play()` or `beep()`. 

80""" 

81 

82 

83def note2freq(note, a4freq=440.0): 

84 """Convert textual note to corresponding frequency. 

85 

86 Parameters 

87 ---------- 

88 note: string 

89 A musical note like 'a4', 'f#3', 'eb5'. 

90 The first character is the note, it can be 

91 'a', 'b', 'c', 'd', 'e', 'f', or 'g'. 

92 The optional second character is either a 'b' 

93 or a '#' to decrease or increase by half a note. 

94 The last character specifies the octave. 

95 'a4' is defined by `a4freq`. 

96 a4freq: float 

97 The frequency of a4 in Hertz. 

98 

99 Returns 

100 ------- 

101 freq: float 

102 The frequency of the note in Hertz. 

103 

104 Raises 

105 ------ 

106 ValueError: 

107 No or an invalid note was specified. 

108 """ 

109 freq = a4freq 

110 tone = 0 

111 octave = 4 

112 if not isinstance(note, str) or len(note) == 0: 

113 raise ValueError('no note specified') 

114 # note: 

115 if note[0] < 'a' or note[0] > 'g': 

116 raise ValueError('invalid note', note[0]) 

117 index = 0 

118 tonemap = [0, 2, 3, 5, 7, 8, 10] 

119 tone = tonemap[ord(note[index]) - ord('a')] 

120 index += 1 

121 # flat or sharp: 

122 flat = False 

123 sharp = False 

124 if index < len(note): 

125 if note[index] == 'b': 

126 flat = True 

127 tone -= 1 

128 index += 1 

129 elif note[index] == '#': 

130 sharp = True 

131 tone += 1 

132 index += 1 

133 # octave: 

134 if index < len(note) and note[index] >= '0' and note[index] <= '9': 

135 octave = 0 

136 while index < len(note) and note[index] >= '0' and note[index] <= '9': 

137 octave *= 10 

138 octave += ord(note[index]) - ord('0') 

139 index += 1 

140 # remaining characters: 

141 if index < len(note): 

142 raise ValueError('invalid characters in note', note) 

143 # compute frequency: 

144 if (tone >= 3 and not sharp) or (tone == 2 and flat): 

145 octave -= 1 

146 tone += 12*(octave-4) 

147 # frequency: 

148 freq = a4freq * 2.0**(tone/12.0) 

149 return freq 

150 

151 

152def fade_in(data, rate, fadetime): 

153 """Fade in a signal in place. 

154 

155 The first `fadetime` seconds of the data are multiplied with a 

156 squared sine in place. If `fadetime` is larger than half the 

157 duration of the data, then `fadetime` is reduced to half of the 

158 duration. 

159  

160 Parameters 

161 ---------- 

162 data: array 

163 The data to be faded in, either 1-D array for single channel output, 

164 or 2-D array with first axis time and second axis channel. 

165 rate: float 

166 The sampling rate in Hertz. 

167 fadetime: float 

168 Time for fading in in seconds. 

169 """ 

170 if len(data) < 4: 

171 return 

172 nr = min(int(np.round(fadetime*rate)), len(data)//2) 

173 x = np.arange(float(nr))/float(nr) # 0 to pi/2 

174 y = np.sin(0.5*np.pi*x)**2.0 

175 if data.ndim > 1: 

176 data[:nr, :] *= y[:, None] 

177 else: 

178 data[:nr] *= y 

179 

180 

181def fade_out(data, rate, fadetime): 

182 """Fade out a signal in place. 

183 

184 The last `fadetime` seconds of the data are multiplied with a 

185 squared sine in place. If `fadetime` is larger than half the 

186 duration of the data, then `fadetime` is reduced to half of the 

187 duration. 

188  

189 Parameters 

190 ---------- 

191 data: array 

192 The data to be faded out, either 1-D array for single channel output, 

193 or 2-D array with first axis time and second axis channel. 

194 rate: float 

195 The sampling rate in Hertz. 

196 fadetime: float 

197 Time for fading out in seconds. 

198 """ 

199 if len(data) < 4: 

200 return 

201 nr = min(int(np.round(fadetime*rate)), len(data)//2) 

202 x = np.arange(float(nr))/float(nr) + 1.0 # pi/2 to pi 

203 y = np.sin(0.5*np.pi*x)**2.0 

204 if data.ndim > 1: 

205 data[-nr:, :] *= y[:, None] 

206 else: 

207 data[-nr:] *= y 

208 

209 

210def fade(data, rate, fadetime): 

211 """Fade in and out a signal in place. 

212 

213 The first and last `fadetime` seconds of the data are multiplied 

214 with a squared sine in place. If `fadetime` is larger than half the 

215 duration of the data, then `fadetime` is reduced to half of the 

216 duration. 

217  

218 Parameters 

219 ---------- 

220 data: array 

221 The data to be faded, either 1-D array for single channel output, 

222 or 2-D array with first axis time and second axis channel. 

223 rate: float 

224 The sampling rate in Hertz. 

225 fadetime: float 

226 Time for fading in and out in seconds. 

227 """ 

228 fade_in(data, rate, fadetime) 

229 fade_out(data, rate, fadetime) 

230 

231 

232class PlayAudio(object): 

233 """ Audio playback. 

234 

235 Parameters 

236 ---------- 

237 device_index: int or None 

238 Index of the playback device to be used. 

239 If None take the default device. 

240 Use the speaker_devices() function to query available devices. 

241 verbose: int 

242 Verbosity level. 

243 library: str or None 

244 If specified, open a specific sound library. 

245 

246 

247 Attributes 

248 ---------- 

249 lib: string 

250 The library used for playback. 

251 verbose: int 

252 Verbosity level. 

253 

254 Methods 

255 ------- 

256 - `play(data, rate, scale=None, blocking=True)`: Playback audio data. 

257 - `beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, fadetime=0.05, blocking=True)`: Playback a pure tone. 

258 - `open()`: Initialize the PlayAudio class with the best module available. 

259 - `close()`: Terminate module for playing audio. 

260 - `stop()`: Stop any playback in progress. 

261 

262 Examples 

263 -------- 

264 ``` 

265 from audioio import PlayAudio 

266  

267 with PlayAudio() as audio: 

268 audio.beep() 

269 ``` 

270 or without context management: 

271 ``` 

272 audio = PlayAudio() 

273 audio.beep(1.0, 'a4') 

274 audio.close() 

275 ``` 

276 """ 

277 

278 def __init__(self, device_index=None, verbose=0, library=None): 

279 self.verbose = verbose 

280 self.handle = None 

281 self._do_play = self._play 

282 self.close = self._close 

283 self.stop = self._stop 

284 self.lib = None 

285 self.open(device_index, library) 

286 

287 def _close(self): 

288 """Terminate PlayAudio class for playing audio.""" 

289 self.handle = None 

290 self._do_play = self._play 

291 self.close = self._close 

292 self.stop = self._stop 

293 self.lib = None 

294 

295 def _stop(self): 

296 """Stop any playback in progress.""" 

297 pass 

298 

299 def _play(self, blocking=True): 

300 """Default implementation of playing a sound: does nothing.""" 

301 pass 

302 

303 def play(self, data, rate, scale=None, blocking=True, device_index=None): 

304 """Playback audio data. 

305 

306 Parameters 

307 ---------- 

308 data: array 

309 The data to be played, either 1-D array for single channel output, 

310 or 2-D array with first axis time and second axis channel. 

311 Data values range between -1 and 1. 

312 rate: float 

313 The sampling rate in Hertz. 

314 scale: float 

315 Multiply data with scale before playing. 

316 If `None` scale it to the maximum value, if 1.0 do not scale. 

317 blocking: boolean 

318 If False do not block.  

319 device_index: int or None 

320 Index of the playback device to be used, 

321 if not already openend via the constructor. 

322 If None take the default device. 

323 

324 Raises 

325 ------ 

326 ValueError 

327 Invalid sampling rate (after some attemps of resampling). 

328 FileNotFoundError 

329 No audio device for playback. 

330 """ 

331 if self.handle is None: 

332 self.open(device_index) 

333 else: 

334 self.stop() 

335 self.rate = rate 

336 self.channels = 1 

337 if data.ndim > 1: 

338 self.channels = data.shape[1] 

339 # convert data: 

340 rawdata = data - np.mean(data, axis=0) 

341 if scale is None: 

342 scale = 1.0/np.max(np.abs(rawdata)) 

343 rawdata *= scale 

344 self.data = np.floor(rawdata*(2**15-1)).astype(np.int16, order='C') 

345 self.index = 0 

346 self._do_play(blocking) 

347 

348 def beep(self, duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, 

349 fadetime=0.05, blocking=True, device_index=None): 

350 """Playback a pure tone. 

351 

352 Parameters 

353 ---------- 

354 duration: float 

355 The duration of the tone in seconds. 

356 frequency: float or string 

357 If float, the frequency of the tone in Hertz. 

358 If string, a musical note like 'f#5'. 

359 See `note2freq()` for details. 

360 amplitude: float 

361 The ampliude (volume) of the tone in the range from 0.0 to 1.0. 

362 rate: float 

363 The sampling rate in Hertz. 

364 fadetime: float 

365 Time for fading in and out in seconds. 

366 blocking: boolean 

367 If False do not block. 

368 device_index: int or None 

369 Index of the playback device to be used, 

370 if not already openend via the constructor. 

371 If None take the default device. 

372 

373 Raises 

374 ------ 

375 ValueError 

376 Invalid sampling rate (after some attemps of resampling). 

377 FileNotFoundError 

378 No audio device for playback. 

379  

380 See also 

381 -------- 

382 https://mail.python.org/pipermail/tutor/2012-September/091529.html 

383 for fourier series based construction of waveforms.  

384 """ 

385 # frequency 

386 if isinstance(frequency, str): 

387 frequency = note2freq(frequency) 

388 # sine wave: 

389 time = np.arange(0.0, duration, 1.0/rate) 

390 data = amplitude*np.sin(2.0*np.pi*frequency*time) 

391 # fade in and out: 

392 fade(data, rate, fadetime) 

393 # # final click for testing (mono only): 

394 # data = np.hstack((data, np.sin(2.0*np.pi*1000.0*time[0:int(np.ceil(4.0*rate/1000.0))]))) 

395 # play: 

396 self.play(data, rate, scale=1.0, blocking=blocking, 

397 device_index=device_index) 

398 

399 def _down_sample(self, channels, scale=1): 

400 """Sample the data down and adapt maximum channel number.""" 

401 iscale = 1 

402 rscale = scale 

403 if isinstance(scale, int): 

404 iscale = scale 

405 rscale = 1.0 

406 elif scale > 2: 

407 iscale = int(np.floor(scale)) 

408 rscale = scale/iscale 

409 

410 if iscale > 1: 

411 data = decimate(self.data, iscale, axis=0) 

412 if self.data.ndim > 1: 

413 self.data = np.asarray(data[:,:channels], 

414 dtype=np.int16, order='C') 

415 else: 

416 self.data = np.asarray(data, dtype=np.int16, order='C') 

417 if self.verbose > 0: 

418 print(f'decimated sampling rate from {self.rate:.1f}Hz down to {self.rate/iscale:.1f}Hz') 

419 self.rate /= iscale 

420 

421 if rscale != 1.0: 

422 dt0 = 1.0/self.rate 

423 dt1 = rscale/self.rate 

424 old_time = np.arange(len(self.data))*dt0 

425 new_time = np.arange(0.0, old_time[-1]+0.5*dt0, dt1) 

426 if self.data.ndim > 1: 

427 data = np.zeros((len(new_time), channels), order='C') 

428 for c in range(channels): 

429 data[:,c] = np.interp(new_time, old_time, self.data[:,c]) 

430 else: 

431 data = np.interp(new_time, old_time, self.data) 

432 self.data = np.asarray(data, dtype=self.data.dtype, order='C') 

433 if self.verbose > 0: 

434 print(f'adapted sampling rate from {self.rate:.1f}Hz to {self.rate/rscale:.1f}Hz') 

435 self.rate /= rscale 

436 self.channels = channels 

437 

438 def __del__(self): 

439 """Terminate the audio module.""" 

440 self.close() 

441 

442 def __enter__(self): 

443 return self 

444 

445 def __exit__(self, type, value, tb): 

446 self.__del__() 

447 return value 

448 

449 

450 def open_pyaudio(self, device_index=None): 

451 """Initialize audio output via PyAudio module. 

452 

453 Parameters 

454 ---------- 

455 device_index: int or None 

456 Index of the playback device to be used. 

457 If None take the default device. 

458 

459 Raises 

460 ------ 

461 ImportError 

462 PyAudio module is not available. 

463 FileNotFoundError 

464 Failed to open audio device. 

465 

466 Documentation 

467 ------------- 

468 https://people.csail.mit.edu/hubert/pyaudio/ 

469 http://www.portaudio.com/ 

470 

471 Installation 

472 ------------ 

473 ``` 

474 sudo apt install -y libportaudio2 portaudio19-dev python-pyaudio python3-pyaudio 

475 ``` 

476  

477 On Windows, download an appropriate (latest version, 32 or 64 bit) wheel from 

478 <https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio>. Install this file with pip, 

479 that is go to the folder where the wheel file is downloaded and run 

480 ``` 

481 pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl 

482 ``` 

483 replace the wheel file name by the one you downloaded. 

484 """ 

485 if not audio_modules['pyaudio']: 

486 raise ImportError 

487 oldstderr = os.dup(2) 

488 os.close(2) 

489 tmpfile = 'tmpfile.tmp' 

490 os.open(tmpfile, os.O_WRONLY | os.O_CREAT) 

491 self.handle = pyaudio.PyAudio() 

492 self.stream = None 

493 os.close(2) 

494 os.dup(oldstderr) 

495 os.close(oldstderr) 

496 os.remove(tmpfile) 

497 try: 

498 if device_index is None: 

499 info = self.handle.get_default_output_device_info() 

500 else: 

501 info = self.handle.get_device_info_by_index(device_index) 

502 self.max_channels = info['maxOutputChannels'] 

503 self.default_rate = info['defaultSampleRate'] 

504 self.device_index = info['index'] 

505 self.handle.is_format_supported(self.default_rate, 

506 output_device=self.device_index, 

507 output_channels=1, 

508 output_format=pyaudio.paInt16) 

509 except Exception as e: 

510 if self.verbose > 0: 

511 print(str(e)) 

512 self.handle.terminate() 

513 self._close() 

514 raise FileNotFoundError('failed to initialize audio device') 

515 self.index = 0 

516 self.data = None 

517 self.close = self._close_pyaudio 

518 self.stop = self._stop_pyaudio 

519 self._do_play = self._play_pyaudio 

520 self.lib = 'pyaudio' 

521 return self 

522 

523 def _callback_pyaudio(self, in_data, frames, time_info, status): 

524 """Callback for pyaudio for supplying output with data.""" 

525 flag = pyaudio.paContinue 

526 if not self.run: 

527 flag = pyaudio.paComplete 

528 if self.index < len(self.data): 

529 out_data = self.data[self.index:self.index+frames] 

530 self.index += len(out_data) 

531 # zero padding: 

532 if len(out_data) < frames: 

533 if self.data.ndim > 1: 

534 out_data = np.vstack((out_data, 

535 np.zeros((frames-len(out_data), self.channels), dtype=np.int16))) 

536 else: 

537 out_data = np.hstack((out_data, np.zeros(frames-len(out_data), dtype=np.int16))) 

538 return (out_data, flag) 

539 else: 

540 # we need to play more to make sure everything is played! 

541 # This is because of an ALSA bug and might be fixed in newer versions, 

542 # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html 

543 out_data = np.zeros(frames*self.channels, dtype=np.int16) 

544 self.index += frames 

545 if self.index >= len(self.data) + 2*self.latency: 

546 flag = pyaudio.paComplete 

547 return (out_data, flag) 

548 

549 def _stop_pyaudio(self): 

550 """Stop any ongoing activity of the pyaudio module.""" 

551 if self.stream is not None: 

552 if self.stream.is_active(): 

553 # fade out: 

554 fadetime = 0.1 

555 nr = int(np.round(fadetime*self.rate)) 

556 index = self.index+nr 

557 if nr > len(self.data) - index: 

558 nr = len(self.data) - index 

559 else: 

560 self.data[index+nr:] = 0 

561 if nr > 0: 

562 for k in range(nr) : 

563 self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] * 

564 np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C') 

565 try: 

566 sleep(2*fadetime) 

567 except SystemError: 

568 # pyaudio interferes with sleep in python 3.10 

569 pass 

570 if self.stream.is_active(): 

571 self.run = False 

572 while self.stream.is_active(): 

573 try: 

574 sleep(0.01) 

575 except SystemError: 

576 # pyaudio interferes with sleep in python 3.10 

577 pass 

578 self.stream.stop_stream() 

579 self.stream.close() 

580 self.stream = None 

581 

582 def _play_pyaudio(self, blocking=True): 

583 """Play audio data using the pyaudio module. 

584 

585 Parameters 

586 ---------- 

587 blocking: boolean 

588 If False do not block. 

589 

590 Raises 

591 ------ 

592 ValueError 

593 Invalid sampling rate (after some attemps of resampling). 

594 """ 

595 # check channel count: 

596 channels = self.channels 

597 if self.channels > self.max_channels: 

598 channels = self.max_channels 

599 # check sampling rate: 

600 scale_fac = 1 

601 scaled_rate = self.rate 

602 max_rate = 48000.0 

603 if self.rate > max_rate: 

604 scale_fac = int(np.ceil(self.rate/max_rate)) 

605 scaled_rate = int(self.rate//scale_fac) 

606 rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate] 

607 scales = [1, scale_fac, None, None, None, None] 

608 success = False 

609 for rate, scale in zip(rates, scales): 

610 try: 

611 if self.handle.is_format_supported(int(rate), 

612 output_device=self.device_index, 

613 output_channels=channels, 

614 output_format=pyaudio.paInt16): 

615 if scale is None: 

616 scale = self.rate/float(rate) 

617 success = True 

618 break 

619 except Exception as e: 

620 if self.verbose > 0: 

621 print(f'invalid sampling rate of {rate}Hz') 

622 if e.args[1] != pyaudio.paInvalidSampleRate: 

623 raise 

624 if not success: 

625 raise ValueError('No valid sampling rate found') 

626 if channels != self.channels or scale != 1: 

627 self._down_sample(channels, scale) 

628 

629 # play: 

630 self.run = True 

631 self.stream = self.handle.open(format=pyaudio.paInt16, channels=self.channels, 

632 rate=int(self.rate), output=True, 

633 stream_callback=self._callback_pyaudio) 

634 self.latency = int(self.stream.get_output_latency()*self.rate) 

635 self.stream.start_stream() 

636 if blocking: 

637 while self.stream.is_active(): 

638 try: 

639 sleep(0.01) 

640 except (ValueError, SystemError): 

641 # pyaudio interferes with sleep in python 3.10 

642 pass 

643 self.run = False 

644 self.stream.stop_stream() 

645 self.stream.close() 

646 self.stream = None 

647 

648 def _close_pyaudio(self): 

649 """Terminate pyaudio module.""" 

650 self._stop_pyaudio() 

651 if self.handle is not None: 

652 self.handle.terminate() 

653 self._close() 

654 

655 

656 def open_sounddevice(self, device_index=None): 

657 """Initialize audio output via sounddevice module. 

658 

659 Parameters 

660 ---------- 

661 device_index: int or None 

662 Index of the playback device to be used. 

663 If None take the default device. 

664 

665 Raises 

666 ------ 

667 ImportError 

668 sounddevice module is not available.  

669 FileNotFoundError 

670 Failed to open audio device. 

671 

672 Documentation 

673 ------------- 

674 https://python-sounddevice.readthedocs.io 

675 

676 Installation 

677 ------------ 

678 ``` 

679 sudo apt install -y libportaudio2 portaudio19-dev 

680 sudo pip install sounddevice 

681 ``` 

682 """ 

683 if not audio_modules['sounddevice']: 

684 raise ImportError 

685 self.handle = True 

686 self.index = 0 

687 self.data = None 

688 self.stream = None 

689 try: 

690 if device_index is None: 

691 info_in = sounddevice.query_devices(kind='input') 

692 info_out = sounddevice.query_devices(kind='output') 

693 if info_in['index'] == info_out['index']: 

694 info = info_out 

695 else: 

696 info = info_out 

697 if info_in['max_output_channels'] > info_out['max_output_channels']: 

698 info = info_in 

699 else: 

700 info = sounddevice.query_devices(device_index) 

701 self.device_index = info['index'] 

702 self.max_channels = info['max_output_channels'] 

703 self.default_rate = info['default_samplerate'] 

704 sounddevice.check_output_settings(device=self.device_index, 

705 channels=1, dtype=np.int16, 

706 samplerate=48000) 

707 except Exception as e: 

708 if self.verbose > 0: 

709 print(str(e)) 

710 self._close() 

711 raise FileNotFoundError('failed to initialize audio device') 

712 self.close = self._close_sounddevice 

713 self.stop = self._stop_sounddevice 

714 self._do_play = self._play_sounddevice 

715 self.lib = 'sounddevice' 

716 return self 

717 

718 def _callback_sounddevice(self, out_data, frames, time_info, status): 

719 """Callback for sounddevice for supplying output with data.""" 

720 if status: 

721 print(status) 

722 if self.index < len(self.data): 

723 ndata = len(self.data) - self.index 

724 if ndata >= frames : 

725 if self.data.ndim <= 1: 

726 out_data[:,0] = self.data[self.index:self.index+frames] 

727 else: 

728 out_data[:, :] = self.data[self.index:self.index+frames, :] 

729 self.index += frames 

730 else: 

731 if self.data.ndim <= 1: 

732 out_data[:ndata, 0] = self.data[self.index:] 

733 out_data[ndata:, 0] = np.zeros(frames-ndata, dtype=np.int16) 

734 else: 

735 out_data[:ndata, :] = self.data[self.index:, :] 

736 out_data[ndata:, :] = np.zeros((frames-ndata, self.channels), 

737 dtype=np.int16) 

738 self.index += frames 

739 else: 

740 # we need to play more to make sure everything is played! 

741 # This is because of an ALSA bug and might be fixed in newer versions, 

742 # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html 

743 if self.data.ndim <= 1: 

744 out_data[:, 0] = np.zeros(frames, dtype=np.int16) 

745 else: 

746 out_data[:, :] = np.zeros((frames, self.channels), dtype=np.int16) 

747 self.index += frames 

748 if self.index >= len(self.data) + 2*self.latency: 

749 raise sounddevice.CallbackStop 

750 if not self.run: 

751 raise sounddevice.CallbackStop 

752 

753 def _stop_sounddevice(self): 

754 """Stop any ongoing activity of the sounddevice module.""" 

755 if self.stream is not None: 

756 if self.stream.active: 

757 # fade out: 

758 fadetime = 0.1 

759 nr = int(np.round(fadetime*self.rate)) 

760 index = self.index+nr 

761 if nr > len(self.data) - index: 

762 nr = len(self.data) - index 

763 else: 

764 self.data[index+nr:] = 0 

765 if nr > 0: 

766 for k in range(nr) : 

767 self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] * 

768 np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C') 

769 sounddevice.sleep(int(2000*fadetime)) 

770 if self.stream.active: 

771 self.run = False 

772 while self.stream.active: 

773 sounddevice.sleep(10) 

774 self.stream.stop() 

775 self.stream.close() 

776 self.stream = None 

777 

778 def _play_sounddevice(self, blocking=True): 

779 """Play audio data using the sounddevice module. 

780 

781 Parameters 

782 ---------- 

783 blocking: boolean 

784 If False do not block. 

785 

786 Raises 

787 ------ 

788 ValueError 

789 Invalid sampling rate (after some attemps of resampling). 

790 """ 

791 # check channel count: 

792 channels = self.channels 

793 if self.channels > self.max_channels: 

794 channels = self.max_channels 

795 # check sampling rate: 

796 scale_fac = 1 

797 scaled_rate = self.rate 

798 max_rate = 48000.0 

799 if self.rate > max_rate: 

800 scale_fac = int(np.ceil(self.rate/max_rate)) 

801 scaled_rate = int(self.rate//scale_fac) 

802 rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate] 

803 scales = [1, scale_fac, None, None, None, None] 

804 success = False 

805 for rate, scale in zip(rates, scales): 

806 try: 

807 sounddevice.check_output_settings(device=self.device_index, 

808 channels=channels, 

809 dtype=np.int16, 

810 samplerate=rate) 

811 if scale is None: 

812 scale = self.rate/float(rate) 

813 success = True 

814 break 

815 except sounddevice.PortAudioError as pae: 

816 if pae.args[1] != -9997: 

817 raise 

818 elif self.verbose > 0: 

819 print(f'invalid sampling rate of {rate}Hz') 

820 if not success: 

821 raise ValueError('No valid sampling rate found') 

822 if channels != self.channels or scale != 1: 

823 self._down_sample(channels, scale) 

824 

825 # play: 

826 self.stream = sounddevice.OutputStream(samplerate=self.rate, 

827 device=self.device_index, 

828 channels=self.channels, 

829 dtype=np.int16, 

830 callback=self._callback_sounddevice) 

831 self.latency = self.stream.latency*self.rate 

832 self.run = True 

833 self.stream.start() 

834 if blocking: 

835 while self.stream.active: 

836 sounddevice.sleep(10) 

837 self.run = False 

838 self.stream.stop() 

839 self.stream.close() 

840 self.stream = None 

841 

842 def _close_sounddevice(self): 

843 """Terminate sounddevice module.""" 

844 self._stop_sounddevice() 

845 self._close() 

846 

847 

848 def open_simpleaudio(self, device_index=None): 

849 """Initialize audio output via simpleaudio package. 

850 

851 Parameters 

852 ---------- 

853 device_index: int or None 

854 Index of the playback device to be used. 

855 If None take the default device. 

856 Not supported by simpleaudio. 

857 

858 Raises 

859 ------ 

860 ImportError 

861 simpleaudio module is not available. 

862 

863 Documentation 

864 ------------- 

865 https://simpleaudio.readthedocs.io 

866 """ 

867 if not audio_modules['simpleaudio']: 

868 raise ImportError 

869 self.handle = True 

870 self._do_play = self._play_simpleaudio 

871 self.close = self._close_simpleaudio 

872 self.stop = self._stop_simpleaudio 

873 self.lib = 'simpleaudio' 

874 return self 

875 

876 def _stop_simpleaudio(self): 

877 """Stop any ongoing activity of the simpleaudio package.""" 

878 if self.handle is not None and self.handle is not True: 

879 self.handle.stop() 

880 

881 def _play_simpleaudio(self, blocking=True): 

882 """Play audio data using the simpleaudio package. 

883 

884 Parameters 

885 ---------- 

886 blocking: boolean 

887 If False do not block.  

888 

889 Raises 

890 ------ 

891 ValueError 

892 Invalid sampling rate (after some attemps of resampling). 

893 FileNotFoundError 

894 No audio device for playback. 

895 """ 

896 rates = [self.rate, 44100, 48000, 22050] 

897 scales = [1, None, None, None] 

898 success = False 

899 for rate, scale in zip(rates, scales): 

900 if scale is None: 

901 scale = self.rate/float(rate) 

902 if scale != 1: 

903 self._down_sample(self.channels, scale) 

904 try: 

905 self.handle = simpleaudio.play_buffer(self.data, self.channels, 

906 2, int(self.rate)) 

907 success = True 

908 break 

909 except ValueError as e: 

910 if self.verbose > 0: 

911 print(f'invalid sampling rate of {rate}Hz') 

912 except simpleaudio._simpleaudio.SimpleaudioError as e: 

913 if self.verbose > 0: 

914 print('simpleaudio SimpleaudioError:', str(e)) 

915 if 'Error opening' in str(e): 

916 raise FileNotFoundError('No audio device found') 

917 except Exception as e: 

918 if self.verbose > 0: 

919 print('simpleaudio Exception:', str(e)) 

920 if not success: 

921 raise ValueError('No valid sampling rate found') 

922 elif blocking: 

923 self.handle.wait_done() 

924 

925 def _close_simpleaudio(self): 

926 """Close audio output using simpleaudio package.""" 

927 self._stop_simpleaudio() 

928 simpleaudio.stop_all() 

929 self._close() 

930 

931 

932 def open_soundcard(self, device_index=None): 

933 """Initialize audio output via soundcard package. 

934 

935 Parameters 

936 ---------- 

937 device_index: int or None 

938 Index of the playback device to be used. 

939 If None take the default device. 

940 

941 Raises 

942 ------ 

943 ImportError 

944 soundcard module is not available. 

945 FileNotFoundError 

946 Failed to open audio device. 

947 

948 Documentation 

949 ------------- 

950 https://github.com/bastibe/SoundCard 

951 """ 

952 if not audio_modules['soundcard']: 

953 raise ImportError 

954 try: 

955 if device_index is None: 

956 self.handle = soundcard.default_speaker() 

957 else: 

958 self.handle = soundcard.all_speakers()[device_index] 

959 except IndexError: 

960 raise FileNotFoundError('No audio device found') 

961 except Exception as e: 

962 print('soundcard Exception:', type(e).__name__, str(e)) 

963 if self.handle is None: 

964 raise FileNotFoundError('No audio device found') 

965 self._do_play = self._play_soundcard 

966 self.close = self._close_soundcard 

967 self.stop = self._stop_soundcard 

968 self.lib = 'soundcard' 

969 return self 

970 

971 def _stop_soundcard(self): 

972 """Stop any ongoing activity of the soundcard package.""" 

973 pass 

974 

975 def _play_soundcard(self, blocking=True): 

976 """Play audio data using the soundcard package. 

977 

978 Parameters 

979 ---------- 

980 blocking: boolean 

981 If False do not block. 

982 Non-blocking playback not supported by soundcard. 

983 Return immediately without playing sound. 

984 

985 Raises 

986 ------ 

987 ValueError 

988 Invalid sampling rate (after some attemps of resampling). 

989 """ 

990 if not blocking: 

991 warnings.warn('soundcard module does not support non-blocking playback') 

992 return 

993 rates = [self.rate, 44100, 48000, 22050] 

994 scales = [1, None, None, None] 

995 success = False 

996 for rate, scale in zip(rates, scales): 

997 if scale is None: 

998 scale = self.rate/float(rate) 

999 if scale != 1: 

1000 self._down_sample(self.channels, scale) 

1001 try: 

1002 self.handle.play(self.data, samplerate=int(self.rate)) 

1003 success = True 

1004 break 

1005 except RuntimeError as e: 

1006 if 'invalid sample spec' in str(e): 

1007 if self.verbose > 0: 

1008 print(f'invalid sampling rate of {rate}Hz') 

1009 else: 

1010 if self.verbose > 0: 

1011 print('soundcard error:', type(e).__name__, str(e)) 

1012 except Exception as e: 

1013 if self.verbose > 0: 

1014 print('soundcard error:', type(e).__name__, str(e)) 

1015 if not success: 

1016 raise ValueError('No valid sampling rate found') 

1017 

1018 def _close_soundcard(self): 

1019 """Close audio output using soundcard package.""" 

1020 self._stop_soundcard() 

1021 self._close() 

1022 

1023 

1024 def open_ossaudiodev(self, device_index=None): 

1025 """Initialize audio output via ossaudiodev module. 

1026 

1027 The OSS audio module is part of the python standard library. 

1028 

1029 Parameters 

1030 ---------- 

1031 device_index: int or None 

1032 Index of the playback device to be used. 

1033 If None take the default device. 

1034 There is only a single OSS audio device. 

1035 

1036 Raises 

1037 ------ 

1038 ImportError 

1039 ossaudiodev module is not available. 

1040 FileNotFoundError 

1041 Failed to open audio device. 

1042 

1043 Documentation 

1044 ------------- 

1045 https://docs.python.org/2/library/ossaudiodev.html 

1046 

1047 Installation 

1048 ------------ 

1049 The ossaudiodev module needs an oss `/dev/dsp` device file. 

1050 Enable an oss emulation via alsa by installing 

1051 ``` 

1052 sudo apt install -y osspd 

1053 ``` 

1054 """ 

1055 if not audio_modules['ossaudiodev']: 

1056 raise ImportError 

1057 self.handle = True 

1058 self.osshandle = None 

1059 self.run = False 

1060 self.play_thread = None 

1061 try: 

1062 handle = ossaudiodev.open('w') 

1063 handle.close() 

1064 except Exception as e: 

1065 if self.verbose > 0: 

1066 print(str(e)) 

1067 self._close() 

1068 raise FileNotFoundError('failed to initialize audio device') 

1069 self.close = self._close_ossaudiodev 

1070 self.stop = self._stop_ossaudiodev 

1071 self._do_play = self._play_ossaudiodev 

1072 self.lib = 'ossaudiodev' 

1073 return self 

1074 

1075 def _stop_ossaudiodev(self): 

1076 """Stop any ongoing activity of the ossaudiodev module.""" 

1077 if self.osshandle is not None: 

1078 self.run = False 

1079 self.osshandle.reset() 

1080 if self.play_thread is not None: 

1081 if self.play_thread.is_alive(): 

1082 self.play_thread.join() 

1083 self.play_thread = None 

1084 self.osshandle.close() 

1085 self.osshandle = None 

1086 

1087 def _run_play_ossaudiodev(self): 

1088 """Play the data using the ossaudiodev module.""" 

1089 self.osshandle.writeall(self.data) 

1090 if self.run: 

1091 sleep(0.5) 

1092 self.osshandle.close() 

1093 self.osshandle = None 

1094 self.run = False 

1095 

1096 def _play_ossaudiodev(self, blocking=True): 

1097 """Play audio data using the ossaudiodev module. 

1098 

1099 Raises 

1100 ------ 

1101 ValueError 

1102 Invalid sampling rate (after some attemps of resampling). 

1103 

1104 Parameters 

1105 ---------- 

1106 blocking: boolean 

1107 If False do not block.  

1108 """ 

1109 self.osshandle = ossaudiodev.open('w') 

1110 self.osshandle.setfmt(ossaudiodev.AFMT_S16_LE) 

1111 # set and check channel count: 

1112 channels = self.osshandle.channels(self.channels) 

1113 # check sampling rate: 

1114 scale_fac = 1 

1115 scaled_rate = self.rate 

1116 max_rate = 48000.0 

1117 if self.rate > max_rate: 

1118 scale_fac = int(np.ceil(self.rate/max_rate)) 

1119 scaled_rate = int(self.rate//scale_fac) 

1120 rates = [self.rate, scaled_rate, 44100, 48000, 22050, 8000] 

1121 scales = [1, scale_fac, None, None, None, None] 

1122 success = False 

1123 for rate, scale in zip(rates, scales): 

1124 set_rate = self.osshandle.speed(int(rate)) 

1125 if abs(set_rate - rate) < 2: 

1126 if scale is None: 

1127 scale = self.rate/float(set_rate) 

1128 success = True 

1129 break 

1130 else: 

1131 if self.verbose > 0: 

1132 print(f'invalid sampling rate of {rate}Hz') 

1133 if not success: 

1134 raise ValueError('No valid sampling rate found') 

1135 if channels != self.channels or scale != 1: 

1136 self._down_sample(channels, scale) 

1137 if blocking: 

1138 self.run = True 

1139 self.osshandle.writeall(self.data) 

1140 sleep(0.5) 

1141 self.osshandle.close() 

1142 self.run = False 

1143 self.osshandle = None 

1144 else: 

1145 self.play_thread = Process(target=self._run_play_ossaudiodev) 

1146 self.run = True 

1147 self.play_thread.start() 

1148 

1149 def _close_ossaudiodev(self): 

1150 """Close audio output using ossaudiodev module.""" 

1151 self._stop_ossaudiodev() 

1152 self._close() 

1153 

1154 

1155 def open_winsound(self, device_index=None): 

1156 """Initialize audio output via winsound module. 

1157 

1158 The winsound module is part of the python standard library. 

1159 

1160 Parameters 

1161 ---------- 

1162 device_index: int or None 

1163 Index of the playback device to be used. 

1164 If None take the default device. 

1165 Device selection is not supported by the winsound module. 

1166 

1167 Raises 

1168 ------ 

1169 ImportError 

1170 winsound module is not available. 

1171 

1172 Documentation 

1173 ------------- 

1174 https://docs.python.org/3.6/library/winsound.html 

1175 https://mail.python.org/pipermail/tutor/2012-September/091529.html 

1176 """ 

1177 if not audio_modules['winsound'] or not audio_modules['wave']: 

1178 raise ImportError 

1179 self.handle = True 

1180 self._do_play = self._play_winsound 

1181 self.close = self._close_winsound 

1182 self.stop = self._stop_winsound 

1183 self.audio_file = '' 

1184 self.lib = 'winsound' 

1185 return self 

1186 

1187 def _stop_winsound(self): 

1188 """Stop any ongoing activity of the winsound module.""" 

1189 try: 

1190 winsound.PlaySound(None, winsound.SND_MEMORY) 

1191 except Exception as e: 

1192 pass 

1193 

1194 def _play_winsound(self, blocking=True): 

1195 """Play audio data using the winsound module. 

1196 

1197 Parameters 

1198 ---------- 

1199 blocking: boolean 

1200 If False do not block.  

1201 """ 

1202 # play file: 

1203 if blocking: 

1204 # write data as wav file to memory: 

1205 self.data_buffer = BytesIO() 

1206 w = wave.open(self.data_buffer, 'w') 

1207 w.setnchannels(self.channels) 

1208 w.setsampwidth(2) 

1209 w.setframerate(int(self.rate)) 

1210 w.setnframes(len(self.data)) 

1211 try: 

1212 w.writeframes(self.data.tobytes()) 

1213 except AttributeError: 

1214 w.writeframes(self.data.tostring()) 

1215 w.close() 

1216 try: 

1217 winsound.PlaySound(self.data_buffer.getvalue(), winsound.SND_MEMORY) 

1218 except Exception as e: 

1219 if self.verbose > 0: 

1220 print(str(e)) 

1221 return 

1222 else: 

1223 if self.verbose > 0: 

1224 print('Warning: asynchronous playback is limited to playing wav files by the winsound module. Install an alternative package as recommended by the audiomodules script. ') 

1225 # write data as wav file to file: 

1226 self.audio_file = 'audioio-async_playback.wav' 

1227 w = wave.open(self.audio_file, 'w') 

1228 w.setnchannels(self.channels) 

1229 w.setsampwidth(2) 

1230 w.setframerate(int(self.rate)) 

1231 w.setnframes(len(self.data)) 

1232 try: 

1233 w.writeframes(self.data.tobytes()) 

1234 except AttributeError: 

1235 w.writeframes(self.data.tostring()) 

1236 w.close() 

1237 try: 

1238 winsound.PlaySound(self.audio_file, winsound.SND_ASYNC) 

1239 except Exception as e: 

1240 if self.verbose > 0: 

1241 print(str(e)) 

1242 return 

1243 

1244 def _close_winsound(self): 

1245 """Close audio output using winsound module.""" 

1246 self._stop_winsound() 

1247 self.handle = None 

1248 if len(self.audio_file) > 0 and os.path.isfile(self.audio_file): 

1249 os.remove(self.audio_file) 

1250 self._close() 

1251 

1252 

1253 def open(self, device_index=None, library=None): 

1254 """Initialize the PlayAudio class with the best module available. 

1255 

1256 Parameters 

1257 ---------- 

1258 device_index: int or None 

1259 Index of the playback device to be used. 

1260 If None take the default device. 

1261 library: str or None 

1262 If specified, open a specific sound library. 

1263 """ 

1264 # list of implemented play functions: 

1265 audio_open = [ 

1266 ['sounddevice', self.open_sounddevice], 

1267 ['pyaudio', self.open_pyaudio], 

1268 ['simpleaudio', self.open_simpleaudio], 

1269 ['soundcard', self.open_soundcard], 

1270 ['ossaudiodev', self.open_ossaudiodev], 

1271 ['winsound', self.open_winsound] 

1272 ] 

1273 if platform[0:3] == "win": 

1274 sa = audio_open.pop(2) 

1275 audio_open.insert(0, sa) 

1276 # open audio device by trying various modules: 

1277 success = False 

1278 for lib, open_device in audio_open: 

1279 if library and library != lib: 

1280 continue 

1281 if not audio_modules[lib]: 

1282 if self.verbose > 0: 

1283 print(f'module {lib} not available') 

1284 continue 

1285 try: 

1286 open_device(device_index) 

1287 success = True 

1288 if self.verbose > 0: 

1289 print(f'successfully opened {lib} module for playing') 

1290 break 

1291 except Exception as e: 

1292 if self.verbose > 0: 

1293 print(f'failed to open {lib} module for playing:', 

1294 type(e).__name__, str(e)) 

1295 if not success: 

1296 warnings.warn('cannot open any device for audio output') 

1297 return self 

1298 

1299 

1300def play(data, rate, scale=None, blocking=True, device_index=None, verbose=0): 

1301 """Playback audio data. 

1302 

1303 Create a `PlayAudio` instance on the global variable `handle`. 

1304 

1305 Parameters 

1306 ---------- 

1307 data: array 

1308 The data to be played, either 1-D array for single channel output, 

1309 or 2-D array with first axis time and second axis channel. 

1310 Data values range between -1 and 1. 

1311 rate: float 

1312 The sampling rate in Hertz. 

1313 scale: float 

1314 Multiply data with scale before playing. 

1315 If `None` scale it to the maximum value, if 1.0 do not scale. 

1316 blocking: boolean 

1317 If False do not block.  

1318 device_index: int or None 

1319 Index of the playback device to be used, 

1320 if not already openend. 

1321 If None take the default device. 

1322 verbose: int 

1323 Verbosity level.  

1324 """ 

1325 global handle 

1326 if handle is None: 

1327 handle = PlayAudio(device_index, verbose) 

1328 handle.verbose = verbose 

1329 handle.play(data, rate, scale, blocking, device_index) 

1330 

1331 

1332def beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, 

1333 fadetime=0.05, blocking=True, device_index=None, verbose=0): 

1334 """Playback a tone. 

1335 

1336 Create a `PlayAudio` instance on the global variable `handle`. 

1337 

1338 Parameters 

1339 ---------- 

1340 duration: float 

1341 The duration of the tone in seconds. 

1342 frequency: float or string 

1343 If float the frequency of the tone in Hertz. 

1344 If string, a musical note like 'f#5'. 

1345 See `note2freq()` for details 

1346 amplitude: float 

1347 The ampliude (volume) of the tone from 0.0 to 1.0. 

1348 rate: float 

1349 The sampling rate in Hertz. 

1350 fadetime: float 

1351 Time for fading in and out in seconds. 

1352 blocking: boolean 

1353 If False do not block. 

1354 device_index: int or None 

1355 Index of the playback device to be used, 

1356 if not already openend. 

1357 If None take the default device. 

1358 verbose: int 

1359 Verbosity level.  

1360 """ 

1361 global handle 

1362 if handle is None: 

1363 handle = PlayAudio(device_index, verbose) 

1364 handle.verbose = verbose 

1365 handle.beep(duration, frequency, amplitude, rate, fadetime, blocking, 

1366 device_index) 

1367 

1368 

1369def close(): 

1370 """Close the global PlayAudio instance. 

1371 """ 

1372 global handle 

1373 if handle is not None: 

1374 handle.close() 

1375 handle = None 

1376 

1377 

1378def speaker_devices_pyaudio(): 

1379 """Query available output devices of the pyaudio module. 

1380 

1381 Returns 

1382 ------- 

1383 indices: list of int 

1384 Device indices. 

1385 devices: list of str 

1386 Devices corresponding to `indices`. 

1387 default_device: int 

1388 Index of default device. 

1389 -1 if no default output device is available. 

1390 """ 

1391 if not audio_modules['pyaudio']: 

1392 raise ImportError 

1393 oldstderr = os.dup(2) 

1394 os.close(2) 

1395 tmpfile = 'tmpfile.tmp' 

1396 os.open(tmpfile, os.O_WRONLY | os.O_CREAT) 

1397 pa = pyaudio.PyAudio() 

1398 os.close(2) 

1399 os.dup(oldstderr) 

1400 os.close(oldstderr) 

1401 os.remove(tmpfile) 

1402 indices = [] 

1403 devices = [] 

1404 for i in range(pa.get_device_count()): 

1405 info = pa.get_device_info_by_index(i) 

1406 if info['maxOutputChannels'] > 0: 

1407 host = sounddevice.query_hostapis(info['hostApi'])['name'] 

1408 device = f'{info["name"]}, {host} ({info["maxInputChannels"]} in, {info["maxOutputChannels"]} out)' 

1409 indices.append(info['index']) 

1410 devices.append(device) 

1411 try: 

1412 default_device = pa.get_default_output_device_info()['index'] 

1413 except OSError: 

1414 default_device = -1 

1415 return indices, devices, default_device 

1416 

1417def speaker_devices_sounddevice(): 

1418 """Query available output devices of the sounddevice module. 

1419 

1420 Returns 

1421 ------- 

1422 indices: list of int 

1423 Device indices. 

1424 devices: list of str 

1425 Devices corresponding to `indices`. 

1426 default_device: int 

1427 Index of default device. 

1428 """ 

1429 if not audio_modules['sounddevice']: 

1430 raise ImportError 

1431 indices = [] 

1432 devices = [] 

1433 infos = sounddevice.query_devices() 

1434 for info in infos: 

1435 if info['max_output_channels'] > 0: 

1436 host = sounddevice.query_hostapis(info['hostapi'])['name'] 

1437 device = f'{info["name"]}, {host} ({info["max_input_channels"]} in, {info["max_output_channels"]} out)' 

1438 indices.append(info['index']) 

1439 devices.append(device) 

1440 try: 

1441 info_out = sounddevice.query_devices(kind='output') 

1442 except sounddevice.PortAudioError: 

1443 return indices, devices, -1 

1444 try: 

1445 info_in = sounddevice.query_devices(kind='input') 

1446 if info_in['index'] != info_out['index'] and \ 

1447 info_in['max_output_channels'] > info_out['max_output_channels']: 

1448 info_out = info_in 

1449 except sounddevice.PortAudioError: 

1450 pass 

1451 return indices, devices, info_out['index'] 

1452 

1453def speaker_devices_soundcard(): 

1454 """Query available output devices of the soundcard module. 

1455 

1456 Returns 

1457 ------- 

1458 indices: list of int 

1459 Device indices. 

1460 devices: list of str 

1461 Devices corresponding to `indices`. 

1462 default_device: int 

1463 Index of default device. 

1464 """ 

1465 if not audio_modules['soundcard']: 

1466 raise ImportError 

1467 indices = [] 

1468 devices = [] 

1469 infos = soundcard.all_speakers() 

1470 def_speaker = str(soundcard.default_speaker()) 

1471 default_device = -1 

1472 for i, info in enumerate(infos): 

1473 if str(info) == def_speaker: 

1474 default_device = i 

1475 indices.append(i) 

1476 devices.append(str(info).lstrip('<').rstrip('>')) 

1477 return indices, devices, default_device 

1478 

1479def speaker_devices(library=None, verbose=0): 

1480 """Query available output devices. 

1481 

1482 Parameters 

1483 ---------- 

1484 library: str or None 

1485 If specified, use specific sound library. 

1486 verbose: int 

1487 Verbosity level. 

1488 

1489 Returns 

1490 ------- 

1491 indices: list of int 

1492 Device indices. 

1493 devices: list of str 

1494 Devices corresponding to `indices`. 

1495 default_device: int 

1496 Index of default device. 

1497 """ 

1498 # list of implemented list functions: 

1499 audio_devices = [ 

1500 ['sounddevice', speaker_devices_sounddevice], 

1501 ['pyaudio', speaker_devices_pyaudio], 

1502 ['simpleaudio', None], 

1503 ['soundcard', speaker_devices_soundcard], 

1504 ['ossaudiodev', None], 

1505 ['winsound', None] 

1506 ] 

1507 if platform[0:3] == "win": 

1508 sa = audio_open.pop(2) 

1509 audio_open.insert(0, sa) 

1510 # query audio devices by trying various modules: 

1511 success = False 

1512 for lib, devices in audio_devices: 

1513 if library and library != lib: 

1514 continue 

1515 if not audio_modules[lib]: 

1516 if verbose > 0: 

1517 print(f'module {lib} not available') 

1518 continue 

1519 if devices is None: 

1520 return [0], ['default output device'], 0 

1521 else: 

1522 return devices() 

1523 warnings.warn('no library for audio output available for devices') 

1524 return [], [], -1 

1525 

1526 

1527def print_speaker_devices(library=None): 

1528 """Print available output devices. 

1529 

1530 Parameters 

1531 ---------- 

1532 library: str or None 

1533 If specified, use specific sound library. 

1534 """ 

1535 indices, devices, default_device = speaker_devices() 

1536 for i, d in zip(indices, devices): 

1537 if i == default_device: 

1538 print(f'* {i:2d}: {d}') 

1539 else: 

1540 print(f' {i:2d}: {d}') 

1541 

1542 

1543def demo(device_index=None): 

1544 """ Demonstrate the playaudio module.""" 

1545 print('play mono beep 1') 

1546 audio = PlayAudio(device_index, verbose=2) 

1547 audio.beep(1.0, 440.0) 

1548 audio.close() 

1549 

1550 print('play mono beep 2') 

1551 with PlayAudio(device_index) as audio: 

1552 audio.beep(1.0, 'b4', 0.75, blocking=False) 

1553 print(' done') 

1554 sleep(0.3) 

1555 sleep(0.5) 

1556 

1557 print('play mono beep 3') 

1558 beep(1.0, 'c5', 0.25, blocking=False, device_index=device_index) 

1559 print(' done') 

1560 sleep(0.5) 

1561 

1562 print('play stereo beep') 

1563 duration = 1.0 

1564 rate = 44100.0 

1565 t = np.arange(0.0, duration, 1.0/rate) 

1566 data = np.zeros((len(t),2)) 

1567 data[:,0] = np.sin(2.0*np.pi*note2freq('a4')*t) 

1568 data[:,1] = 0.25*np.sin(2.0*np.pi*note2freq('e5')*t) 

1569 fade(data, rate, 0.1) 

1570 play(data, rate, verbose=2, device_index=device_index) 

1571 

1572 

1573def main(*args): 

1574 """Call demo with command line arguments. 

1575 

1576 Parameters 

1577 ---------- 

1578 args: list of strings 

1579 Command line arguments as provided by sys.argv[1:] 

1580 """ 

1581 help = False 

1582 mod = False 

1583 dev = False 

1584 ldev = False 

1585 device_index = None 

1586 for arg in args: 

1587 if mod: 

1588 if not select_module(arg): 

1589 print(f'module {arg} not installed. Exit!') 

1590 return 

1591 mod = False 

1592 elif dev: 

1593 device_index = int(arg) 

1594 dev = False 

1595 elif arg == '-h': 

1596 help = True 

1597 break 

1598 elif arg == '-l': 

1599 ldev = True 

1600 break 

1601 elif arg == '-m': 

1602 mod = True 

1603 elif arg == '-d': 

1604 dev = True 

1605 else: 

1606 break 

1607 

1608 if help: 

1609 print() 

1610 print('Usage:') 

1611 print(' python -m src.audioio.playaudio [-m <module>] [-l] [-d <device index>]') 

1612 print(' -m: audio module to be used') 

1613 print(' -l: list available audio output devices') 

1614 print(' -d: set audio output device to be used') 

1615 return 

1616 

1617 if ldev: 

1618 print_speaker_devices() 

1619 return 

1620 

1621 demo(device_index) 

1622 

1623 

1624if __name__ == "__main__": 

1625 import sys 

1626 main(*sys.argv[1:])