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

811 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-16 18:31 +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 

62from sys import platform 

63import os 

64import warnings 

65import numpy as np 

66from scipy.signal import decimate 

67from time import sleep 

68from io import BytesIO 

69from multiprocessing import Process 

70from .audiomodules import * 

71 

72 

73handle = None 

74"""Default audio device handler. 

75 

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

77`play()` or `beep()`. 

78""" 

79 

80 

81def note2freq(note, a4freq=440.0): 

82 """Convert textual note to corresponding frequency. 

83 

84 Parameters 

85 ---------- 

86 note: string 

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

88 The first character is the note, it can be 

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

90 The optional second character is either a 'b' 

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

92 The last character specifies the octave. 

93 'a4' is defined by `a4freq`. 

94 a4freq: float 

95 The frequency of a4 in Hertz. 

96 

97 Returns 

98 ------- 

99 freq: float 

100 The frequency of the note in Hertz. 

101 

102 Raises 

103 ------ 

104 ValueError: 

105 No or an invalid note was specified. 

106 """ 

107 freq = a4freq 

108 tone = 0 

109 octave = 4 

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

111 raise ValueError('no note specified') 

112 # note: 

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

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

115 index = 0 

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

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

118 index += 1 

119 # flat or sharp: 

120 flat = False 

121 sharp = False 

122 if index < len(note): 

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

124 flat = True 

125 tone -= 1 

126 index += 1 

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

128 sharp = True 

129 tone += 1 

130 index += 1 

131 # octave: 

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

133 octave = 0 

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

135 octave *= 10 

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

137 index += 1 

138 # remaining characters: 

139 if index < len(note): 

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

141 # compute frequency: 

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

143 octave -= 1 

144 tone += 12*(octave-4) 

145 # frequency: 

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

147 return freq 

148 

149 

150def fade_in(data, rate, fadetime): 

151 """Fade in a signal in place. 

152 

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

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

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

156 duration. 

157  

158 Parameters 

159 ---------- 

160 data: array 

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

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

163 rate: float 

164 The sampling rate in Hertz. 

165 fadetime: float 

166 Time for fading in in seconds. 

167 """ 

168 if len(data) < 4: 

169 return 

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

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

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

173 if data.ndim > 1: 

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

175 else: 

176 data[:nr] *= y 

177 

178 

179def fade_out(data, rate, fadetime): 

180 """Fade out a signal in place. 

181 

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

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

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

185 duration. 

186  

187 Parameters 

188 ---------- 

189 data: array 

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

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

192 rate: float 

193 The sampling rate in Hertz. 

194 fadetime: float 

195 Time for fading out in seconds. 

196 """ 

197 if len(data) < 4: 

198 return 

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

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

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

202 if data.ndim > 1: 

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

204 else: 

205 data[-nr:] *= y 

206 

207 

208def fade(data, rate, fadetime): 

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

210 

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

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

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

214 duration. 

215  

216 Parameters 

217 ---------- 

218 data: array 

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

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

221 rate: float 

222 The sampling rate in Hertz. 

223 fadetime: float 

224 Time for fading in and out in seconds. 

225 """ 

226 fade_in(data, rate, fadetime) 

227 fade_out(data, rate, fadetime) 

228 

229 

230class PlayAudio(object): 

231 """ Audio playback. 

232 

233 Parameters 

234 ---------- 

235 device_index: int or None 

236 Index of the playback device to be used. 

237 If None take the default device. 

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

239 verbose: int 

240 Verbosity level. 

241 library: str or None 

242 If specified, open a specific sound library. 

243 

244 

245 Attributes 

246 ---------- 

247 lib: string 

248 The library used for playback. 

249 verbose: int 

250 Verbosity level. 

251 

252 Methods 

253 ------- 

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

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

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

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

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

259 

260 Examples 

261 -------- 

262 ``` 

263 from audioio import PlayAudio 

264  

265 with PlayAudio() as audio: 

266 audio.beep() 

267 ``` 

268 or without context management: 

269 ``` 

270 audio = PlayAudio() 

271 audio.beep(1.0, 'a4') 

272 audio.close() 

273 ``` 

274 """ 

275 

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

277 self.verbose = verbose 

278 self.handle = None 

279 self._do_play = self._play 

280 self.close = self._close 

281 self.stop = self._stop 

282 self.lib = None 

283 self.open(device_index, library) 

284 

285 def _close(self): 

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

287 self.handle = None 

288 self._do_play = self._play 

289 self.close = self._close 

290 self.stop = self._stop 

291 self.lib = None 

292 

293 def _stop(self): 

294 """Stop any playback in progress.""" 

295 pass 

296 

297 def _play(self, blocking=True): 

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

299 pass 

300 

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

302 """Playback audio data. 

303 

304 Parameters 

305 ---------- 

306 data: array 

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

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

309 Data values range between -1 and 1. 

310 rate: float 

311 The sampling rate in Hertz. 

312 scale: float 

313 Multiply data with scale before playing. 

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

315 blocking: boolean 

316 If False do not block.  

317 device_index: int or None 

318 Index of the playback device to be used, 

319 if not already openend via the constructor. 

320 If None take the default device. 

321 

322 Raises 

323 ------ 

324 ValueError 

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

326 FileNotFoundError 

327 No audio device for playback. 

328 """ 

329 if self.handle is None: 

330 self.open(device_index) 

331 else: 

332 self.stop() 

333 self.rate = rate 

334 self.channels = 1 

335 if data.ndim > 1: 

336 self.channels = data.shape[1] 

337 # convert data: 

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

339 if scale is None: 

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

341 rawdata *= scale 

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

343 self.index = 0 

344 self._do_play(blocking) 

345 

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

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

348 """Playback a pure tone. 

349 

350 Parameters 

351 ---------- 

352 duration: float 

353 The duration of the tone in seconds. 

354 frequency: float or string 

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

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

357 See `note2freq()` for details. 

358 amplitude: float 

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

360 rate: float 

361 The sampling rate in Hertz. 

362 fadetime: float 

363 Time for fading in and out in seconds. 

364 blocking: boolean 

365 If False do not block. 

366 device_index: int or None 

367 Index of the playback device to be used, 

368 if not already openend via the constructor. 

369 If None take the default device. 

370 

371 Raises 

372 ------ 

373 ValueError 

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

375 FileNotFoundError 

376 No audio device for playback. 

377  

378 See also 

379 -------- 

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

381 for fourier series based construction of waveforms.  

382 """ 

383 # frequency 

384 if isinstance(frequency, str): 

385 frequency = note2freq(frequency) 

386 # sine wave: 

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

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

389 # fade in and out: 

390 fade(data, rate, fadetime) 

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

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

393 # play: 

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

395 device_index=device_index) 

396 

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

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

399 iscale = 1 

400 rscale = scale 

401 if isinstance(scale, int): 

402 iscale = scale 

403 rscale = 1.0 

404 elif scale > 2: 

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

406 rscale = scale/iscale 

407 

408 if iscale > 1: 

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

410 if self.data.ndim > 1: 

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

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

413 else: 

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

415 if self.verbose > 0: 

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

417 self.rate /= iscale 

418 

419 if rscale != 1.0: 

420 dt0 = 1.0/self.rate 

421 dt1 = rscale/self.rate 

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

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

424 if self.data.ndim > 1: 

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

426 for c in range(channels): 

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

428 else: 

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

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

431 if self.verbose > 0: 

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

433 self.rate /= rscale 

434 self.channels = channels 

435 

436 def __del__(self): 

437 """Terminate the audio module.""" 

438 self.close() 

439 

440 def __enter__(self): 

441 return self 

442 

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

444 self.__del__() 

445 return value 

446 

447 

448 def open_pyaudio(self, device_index=None): 

449 """Initialize audio output via PyAudio module. 

450 

451 Parameters 

452 ---------- 

453 device_index: int or None 

454 Index of the playback device to be used. 

455 If None take the default device. 

456 

457 Raises 

458 ------ 

459 ImportError 

460 PyAudio module is not available. 

461 FileNotFoundError 

462 Failed to open audio device. 

463 

464 Documentation 

465 ------------- 

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

467 http://www.portaudio.com/ 

468 

469 Installation 

470 ------------ 

471 ``` 

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

473 ``` 

474  

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

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

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

478 ``` 

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

480 ``` 

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

482 """ 

483 if not audio_modules['pyaudio']: 

484 raise ImportError 

485 oldstderr = os.dup(2) 

486 os.close(2) 

487 tmpfile = 'tmpfile.tmp' 

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

489 self.handle = pyaudio.PyAudio() 

490 self.stream = None 

491 os.close(2) 

492 os.dup(oldstderr) 

493 os.close(oldstderr) 

494 os.remove(tmpfile) 

495 try: 

496 if device_index is None: 

497 info = self.handle.get_default_output_device_info() 

498 else: 

499 info = self.handle.get_device_info_by_index(device_index) 

500 self.max_channels = info['maxOutputChannels'] 

501 self.default_rate = info['defaultSampleRate'] 

502 self.device_index = info['index'] 

503 self.handle.is_format_supported(self.default_rate, 

504 output_device=self.device_index, 

505 output_channels=1, 

506 output_format=pyaudio.paInt16) 

507 except Exception as e: 

508 if self.verbose > 0: 

509 print(str(e)) 

510 self.handle.terminate() 

511 self._close() 

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

513 self.index = 0 

514 self.data = None 

515 self.close = self._close_pyaudio 

516 self.stop = self._stop_pyaudio 

517 self._do_play = self._play_pyaudio 

518 self.lib = 'pyaudio' 

519 return self 

520 

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

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

523 flag = pyaudio.paContinue 

524 if not self.run: 

525 flag = pyaudio.paComplete 

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

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

528 self.index += len(out_data) 

529 # zero padding: 

530 if len(out_data) < frames: 

531 if self.data.ndim > 1: 

532 out_data = np.vstack((out_data, 

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

534 else: 

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

536 return (out_data, flag) 

537 else: 

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

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

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

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

542 self.index += frames 

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

544 flag = pyaudio.paComplete 

545 return (out_data, flag) 

546 

547 def _stop_pyaudio(self): 

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

549 if self.stream is not None: 

550 if self.stream.is_active(): 

551 # fade out: 

552 fadetime = 0.1 

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

554 index = self.index+nr 

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

556 nr = len(self.data) - index 

557 else: 

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

559 if nr > 0: 

560 for k in range(nr) : 

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

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

563 try: 

564 sleep(2*fadetime) 

565 except SystemError: 

566 # pyaudio interferes with sleep in python 3.10 

567 pass 

568 if self.stream.is_active(): 

569 self.run = False 

570 while self.stream.is_active(): 

571 try: 

572 sleep(0.01) 

573 except SystemError: 

574 # pyaudio interferes with sleep in python 3.10 

575 pass 

576 self.stream.stop_stream() 

577 self.stream.close() 

578 self.stream = None 

579 

580 def _play_pyaudio(self, blocking=True): 

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

582 

583 Parameters 

584 ---------- 

585 blocking: boolean 

586 If False do not block. 

587 

588 Raises 

589 ------ 

590 ValueError 

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

592 """ 

593 # check channel count: 

594 channels = self.channels 

595 if self.channels > self.max_channels: 

596 channels = self.max_channels 

597 # check sampling rate: 

598 scale_fac = 1 

599 scaled_rate = self.rate 

600 max_rate = 48000.0 

601 if self.rate > max_rate: 

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

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

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

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

606 success = False 

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

608 try: 

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

610 output_device=self.device_index, 

611 output_channels=channels, 

612 output_format=pyaudio.paInt16): 

613 if scale is None: 

614 scale = self.rate/float(rate) 

615 success = True 

616 break 

617 except Exception as e: 

618 if self.verbose > 0: 

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

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

621 raise 

622 if not success: 

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

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

625 self._down_sample(channels, scale) 

626 

627 # play: 

628 self.run = True 

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

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

631 stream_callback=self._callback_pyaudio) 

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

633 self.stream.start_stream() 

634 if blocking: 

635 while self.stream.is_active(): 

636 try: 

637 sleep(0.01) 

638 except (ValueError, SystemError): 

639 # pyaudio interferes with sleep in python 3.10 

640 pass 

641 self.run = False 

642 self.stream.stop_stream() 

643 self.stream.close() 

644 self.stream = None 

645 

646 def _close_pyaudio(self): 

647 """Terminate pyaudio module.""" 

648 self._stop_pyaudio() 

649 if self.handle is not None: 

650 self.handle.terminate() 

651 self._close() 

652 

653 

654 def open_sounddevice(self, device_index=None): 

655 """Initialize audio output via sounddevice module. 

656 

657 Parameters 

658 ---------- 

659 device_index: int or None 

660 Index of the playback device to be used. 

661 If None take the default device. 

662 

663 Raises 

664 ------ 

665 ImportError 

666 sounddevice module is not available.  

667 FileNotFoundError 

668 Failed to open audio device. 

669 

670 Documentation 

671 ------------- 

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

673 

674 Installation 

675 ------------ 

676 ``` 

677 sudo apt install -y libportaudio2 portaudio19-dev 

678 sudo pip install sounddevice 

679 ``` 

680 """ 

681 if not audio_modules['sounddevice']: 

682 raise ImportError 

683 self.handle = True 

684 self.index = 0 

685 self.data = None 

686 self.stream = None 

687 try: 

688 if device_index is None: 

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

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

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

692 info = info_out 

693 else: 

694 info = info_out 

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

696 info = info_in 

697 else: 

698 info = sounddevice.query_devices(device_index) 

699 self.device_index = info['index'] 

700 self.max_channels = info['max_output_channels'] 

701 self.default_rate = info['default_samplerate'] 

702 sounddevice.check_output_settings(device=self.device_index, 

703 channels=1, dtype=np.int16, 

704 samplerate=48000) 

705 except Exception as e: 

706 if self.verbose > 0: 

707 print(str(e)) 

708 self._close() 

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

710 self.close = self._close_sounddevice 

711 self.stop = self._stop_sounddevice 

712 self._do_play = self._play_sounddevice 

713 self.lib = 'sounddevice' 

714 return self 

715 

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

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

718 if status: 

719 print(status) 

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

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

722 if ndata >= frames : 

723 if self.data.ndim <= 1: 

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

725 else: 

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

727 self.index += frames 

728 else: 

729 if self.data.ndim <= 1: 

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

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

732 else: 

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

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

735 dtype=np.int16) 

736 self.index += frames 

737 else: 

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

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

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

741 if self.data.ndim <= 1: 

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

743 else: 

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

745 self.index += frames 

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

747 raise sounddevice.CallbackStop 

748 if not self.run: 

749 raise sounddevice.CallbackStop 

750 

751 def _stop_sounddevice(self): 

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

753 if self.stream is not None: 

754 if self.stream.active: 

755 # fade out: 

756 fadetime = 0.1 

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

758 index = self.index+nr 

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

760 nr = len(self.data) - index 

761 else: 

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

763 if nr > 0: 

764 for k in range(nr) : 

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

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

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

768 if self.stream.active: 

769 self.run = False 

770 while self.stream.active: 

771 sounddevice.sleep(10) 

772 self.stream.stop() 

773 self.stream.close() 

774 self.stream = None 

775 

776 def _play_sounddevice(self, blocking=True): 

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

778 

779 Parameters 

780 ---------- 

781 blocking: boolean 

782 If False do not block. 

783 

784 Raises 

785 ------ 

786 ValueError 

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

788 """ 

789 # check channel count: 

790 channels = self.channels 

791 if self.channels > self.max_channels: 

792 channels = self.max_channels 

793 # check sampling rate: 

794 scale_fac = 1 

795 scaled_rate = self.rate 

796 max_rate = 48000.0 

797 if self.rate > max_rate: 

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

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

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

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

802 success = False 

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

804 try: 

805 sounddevice.check_output_settings(device=self.device_index, 

806 channels=channels, 

807 dtype=np.int16, 

808 samplerate=rate) 

809 if scale is None: 

810 scale = self.rate/float(rate) 

811 success = True 

812 break 

813 except sounddevice.PortAudioError as pae: 

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

815 raise 

816 elif self.verbose > 0: 

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

818 if not success: 

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

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

821 self._down_sample(channels, scale) 

822 

823 # play: 

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

825 device=self.device_index, 

826 channels=self.channels, 

827 dtype=np.int16, 

828 callback=self._callback_sounddevice) 

829 self.latency = self.stream.latency*self.rate 

830 self.run = True 

831 self.stream.start() 

832 if blocking: 

833 while self.stream.active: 

834 sounddevice.sleep(10) 

835 self.run = False 

836 self.stream.stop() 

837 self.stream.close() 

838 self.stream = None 

839 

840 def _close_sounddevice(self): 

841 """Terminate sounddevice module.""" 

842 self._stop_sounddevice() 

843 self._close() 

844 

845 

846 def open_simpleaudio(self, device_index=None): 

847 """Initialize audio output via simpleaudio package. 

848 

849 Parameters 

850 ---------- 

851 device_index: int or None 

852 Index of the playback device to be used. 

853 If None take the default device. 

854 Not supported by simpleaudio. 

855 

856 Raises 

857 ------ 

858 ImportError 

859 simpleaudio module is not available. 

860 

861 Documentation 

862 ------------- 

863 https://simpleaudio.readthedocs.io 

864 """ 

865 if not audio_modules['simpleaudio']: 

866 raise ImportError 

867 self.handle = True 

868 self._do_play = self._play_simpleaudio 

869 self.close = self._close_simpleaudio 

870 self.stop = self._stop_simpleaudio 

871 self.lib = 'simpleaudio' 

872 return self 

873 

874 def _stop_simpleaudio(self): 

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

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

877 self.handle.stop() 

878 

879 def _play_simpleaudio(self, blocking=True): 

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

881 

882 Parameters 

883 ---------- 

884 blocking: boolean 

885 If False do not block.  

886 

887 Raises 

888 ------ 

889 ValueError 

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

891 FileNotFoundError 

892 No audio device for playback. 

893 """ 

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

895 scales = [1, None, None, None] 

896 success = False 

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

898 if scale is None: 

899 scale = self.rate/float(rate) 

900 if scale != 1: 

901 self._down_sample(self.channels, scale) 

902 try: 

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

904 2, int(self.rate)) 

905 success = True 

906 break 

907 except ValueError as e: 

908 if self.verbose > 0: 

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

910 except simpleaudio._simpleaudio.SimpleaudioError as e: 

911 if self.verbose > 0: 

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

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

914 raise FileNotFoundError('No audio device found') 

915 except Exception as e: 

916 if self.verbose > 0: 

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

918 if not success: 

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

920 elif blocking: 

921 self.handle.wait_done() 

922 

923 def _close_simpleaudio(self): 

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

925 self._stop_simpleaudio() 

926 simpleaudio.stop_all() 

927 self._close() 

928 

929 

930 def open_soundcard(self, device_index=None): 

931 """Initialize audio output via soundcard package. 

932 

933 Parameters 

934 ---------- 

935 device_index: int or None 

936 Index of the playback device to be used. 

937 If None take the default device. 

938 

939 Raises 

940 ------ 

941 ImportError 

942 soundcard module is not available. 

943 FileNotFoundError 

944 Failed to open audio device. 

945 

946 Documentation 

947 ------------- 

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

949 """ 

950 if not audio_modules['soundcard']: 

951 raise ImportError 

952 try: 

953 if device_index is None: 

954 self.handle = soundcard.default_speaker() 

955 else: 

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

957 except IndexError: 

958 raise FileNotFoundError('No audio device found') 

959 except Exception as e: 

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

961 if self.handle is None: 

962 raise FileNotFoundError('No audio device found') 

963 self._do_play = self._play_soundcard 

964 self.close = self._close_soundcard 

965 self.stop = self._stop_soundcard 

966 self.lib = 'soundcard' 

967 return self 

968 

969 def _stop_soundcard(self): 

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

971 pass 

972 

973 def _play_soundcard(self, blocking=True): 

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

975 

976 Parameters 

977 ---------- 

978 blocking: boolean 

979 If False do not block. 

980 Non-blocking playback not supported by soundcard. 

981 Return immediately without playing sound. 

982 

983 Raises 

984 ------ 

985 ValueError 

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

987 """ 

988 if not blocking: 

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

990 return 

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

992 scales = [1, None, None, None] 

993 success = False 

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

995 if scale is None: 

996 scale = self.rate/float(rate) 

997 if scale != 1: 

998 self._down_sample(self.channels, scale) 

999 try: 

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

1001 success = True 

1002 break 

1003 except RuntimeError as e: 

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

1005 if self.verbose > 0: 

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

1007 else: 

1008 if self.verbose > 0: 

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

1010 except Exception as e: 

1011 if self.verbose > 0: 

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

1013 if not success: 

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

1015 

1016 def _close_soundcard(self): 

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

1018 self._stop_soundcard() 

1019 self._close() 

1020 

1021 

1022 def open_ossaudiodev(self, device_index=None): 

1023 """Initialize audio output via ossaudiodev module. 

1024 

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

1026 

1027 Parameters 

1028 ---------- 

1029 device_index: int or None 

1030 Index of the playback device to be used. 

1031 If None take the default device. 

1032 There is only a single OSS audio device. 

1033 

1034 Raises 

1035 ------ 

1036 ImportError 

1037 ossaudiodev module is not available. 

1038 FileNotFoundError 

1039 Failed to open audio device. 

1040 

1041 Documentation 

1042 ------------- 

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

1044 

1045 Installation 

1046 ------------ 

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

1048 Enable an oss emulation via alsa by installing 

1049 ``` 

1050 sudo apt install -y osspd 

1051 ``` 

1052 """ 

1053 if not audio_modules['ossaudiodev']: 

1054 raise ImportError 

1055 self.handle = True 

1056 self.osshandle = None 

1057 self.run = False 

1058 self.play_thread = None 

1059 try: 

1060 handle = ossaudiodev.open('w') 

1061 handle.close() 

1062 except Exception as e: 

1063 if self.verbose > 0: 

1064 print(str(e)) 

1065 self._close() 

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

1067 self.close = self._close_ossaudiodev 

1068 self.stop = self._stop_ossaudiodev 

1069 self._do_play = self._play_ossaudiodev 

1070 self.lib = 'ossaudiodev' 

1071 return self 

1072 

1073 def _stop_ossaudiodev(self): 

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

1075 if self.osshandle is not None: 

1076 self.run = False 

1077 self.osshandle.reset() 

1078 if self.play_thread is not None: 

1079 if self.play_thread.is_alive(): 

1080 self.play_thread.join() 

1081 self.play_thread = None 

1082 self.osshandle.close() 

1083 self.osshandle = None 

1084 

1085 def _run_play_ossaudiodev(self): 

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

1087 self.osshandle.writeall(self.data) 

1088 if self.run: 

1089 sleep(0.5) 

1090 self.osshandle.close() 

1091 self.osshandle = None 

1092 self.run = False 

1093 

1094 def _play_ossaudiodev(self, blocking=True): 

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

1096 

1097 Raises 

1098 ------ 

1099 ValueError 

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

1101 

1102 Parameters 

1103 ---------- 

1104 blocking: boolean 

1105 If False do not block.  

1106 """ 

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

1108 self.osshandle.setfmt(ossaudiodev.AFMT_S16_LE) 

1109 # set and check channel count: 

1110 channels = self.osshandle.channels(self.channels) 

1111 # check sampling rate: 

1112 scale_fac = 1 

1113 scaled_rate = self.rate 

1114 max_rate = 48000.0 

1115 if self.rate > max_rate: 

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

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

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

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

1120 success = False 

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

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

1123 if abs(set_rate - rate) < 2: 

1124 if scale is None: 

1125 scale = self.rate/float(set_rate) 

1126 success = True 

1127 break 

1128 else: 

1129 if self.verbose > 0: 

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

1131 if not success: 

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

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

1134 self._down_sample(channels, scale) 

1135 if blocking: 

1136 self.run = True 

1137 self.osshandle.writeall(self.data) 

1138 sleep(0.5) 

1139 self.osshandle.close() 

1140 self.run = False 

1141 self.osshandle = None 

1142 else: 

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

1144 self.run = True 

1145 self.play_thread.start() 

1146 

1147 def _close_ossaudiodev(self): 

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

1149 self._stop_ossaudiodev() 

1150 self._close() 

1151 

1152 

1153 def open_winsound(self, device_index=None): 

1154 """Initialize audio output via winsound module. 

1155 

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

1157 

1158 Parameters 

1159 ---------- 

1160 device_index: int or None 

1161 Index of the playback device to be used. 

1162 If None take the default device. 

1163 Device selection is not supported by the winsound module. 

1164 

1165 Raises 

1166 ------ 

1167 ImportError 

1168 winsound module is not available. 

1169 

1170 Documentation 

1171 ------------- 

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

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

1174 """ 

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

1176 raise ImportError 

1177 self.handle = True 

1178 self._do_play = self._play_winsound 

1179 self.close = self._close_winsound 

1180 self.stop = self._stop_winsound 

1181 self.audio_file = '' 

1182 self.lib = 'winsound' 

1183 return self 

1184 

1185 def _stop_winsound(self): 

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

1187 try: 

1188 winsound.PlaySound(None, winsound.SND_MEMORY) 

1189 except Exception as e: 

1190 pass 

1191 

1192 def _play_winsound(self, blocking=True): 

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

1194 

1195 Parameters 

1196 ---------- 

1197 blocking: boolean 

1198 If False do not block.  

1199 """ 

1200 # play file: 

1201 if blocking: 

1202 # write data as wav file to memory: 

1203 self.data_buffer = BytesIO() 

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

1205 w.setnchannels(self.channels) 

1206 w.setsampwidth(2) 

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

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

1209 try: 

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

1211 except AttributeError: 

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

1213 w.close() 

1214 try: 

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

1216 except Exception as e: 

1217 if self.verbose > 0: 

1218 print(str(e)) 

1219 return 

1220 else: 

1221 if self.verbose > 0: 

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

1223 # write data as wav file to file: 

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

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

1226 w.setnchannels(self.channels) 

1227 w.setsampwidth(2) 

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

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

1230 try: 

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

1232 except AttributeError: 

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

1234 w.close() 

1235 try: 

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

1237 except Exception as e: 

1238 if self.verbose > 0: 

1239 print(str(e)) 

1240 return 

1241 

1242 def _close_winsound(self): 

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

1244 self._stop_winsound() 

1245 self.handle = None 

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

1247 os.remove(self.audio_file) 

1248 self._close() 

1249 

1250 

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

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

1253 

1254 Parameters 

1255 ---------- 

1256 device_index: int or None 

1257 Index of the playback device to be used. 

1258 If None take the default device. 

1259 library: str or None 

1260 If specified, open a specific sound library. 

1261 """ 

1262 # list of implemented play functions: 

1263 audio_open = [ 

1264 ['sounddevice', self.open_sounddevice], 

1265 ['pyaudio', self.open_pyaudio], 

1266 ['simpleaudio', self.open_simpleaudio], 

1267 ['soundcard', self.open_soundcard], 

1268 ['ossaudiodev', self.open_ossaudiodev], 

1269 ['winsound', self.open_winsound] 

1270 ] 

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

1272 sa = audio_open.pop(2) 

1273 audio_open.insert(0, sa) 

1274 # open audio device by trying various modules: 

1275 success = False 

1276 for lib, open_device in audio_open: 

1277 if library and library != lib: 

1278 continue 

1279 if not audio_modules[lib]: 

1280 if self.verbose > 0: 

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

1282 continue 

1283 try: 

1284 open_device(device_index) 

1285 success = True 

1286 if self.verbose > 0: 

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

1288 break 

1289 except Exception as e: 

1290 if self.verbose > 0: 

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

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

1293 if not success: 

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

1295 return self 

1296 

1297 

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

1299 """Playback audio data. 

1300 

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

1302 

1303 Parameters 

1304 ---------- 

1305 data: array 

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

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

1308 Data values range between -1 and 1. 

1309 rate: float 

1310 The sampling rate in Hertz. 

1311 scale: float 

1312 Multiply data with scale before playing. 

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

1314 blocking: boolean 

1315 If False do not block.  

1316 device_index: int or None 

1317 Index of the playback device to be used, 

1318 if not already openend. 

1319 If None take the default device. 

1320 verbose: int 

1321 Verbosity level.  

1322 """ 

1323 global handle 

1324 if handle is None: 

1325 handle = PlayAudio(device_index, verbose) 

1326 handle.verbose = verbose 

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

1328 

1329 

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

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

1332 """Playback a tone. 

1333 

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

1335 

1336 Parameters 

1337 ---------- 

1338 duration: float 

1339 The duration of the tone in seconds. 

1340 frequency: float or string 

1341 If float the frequency of the tone in Hertz. 

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

1343 See `note2freq()` for details 

1344 amplitude: float 

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

1346 rate: float 

1347 The sampling rate in Hertz. 

1348 fadetime: float 

1349 Time for fading in and out in seconds. 

1350 blocking: boolean 

1351 If False do not block. 

1352 device_index: int or None 

1353 Index of the playback device to be used, 

1354 if not already openend. 

1355 If None take the default device. 

1356 verbose: int 

1357 Verbosity level.  

1358 """ 

1359 global handle 

1360 if handle is None: 

1361 handle = PlayAudio(device_index, verbose) 

1362 handle.verbose = verbose 

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

1364 device_index) 

1365 

1366 

1367def close(): 

1368 """Close the global PlayAudio instance. 

1369 """ 

1370 global handle 

1371 if handle is not None: 

1372 handle.close() 

1373 handle = None 

1374 

1375 

1376def speaker_devices_pyaudio(): 

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

1378 

1379 Returns 

1380 ------- 

1381 indices: list of int 

1382 Device indices. 

1383 devices: list of str 

1384 Devices corresponding to `indices`. 

1385 default_device: int 

1386 Index of default device. 

1387 -1 if no default output device is available. 

1388 """ 

1389 if not audio_modules['pyaudio']: 

1390 raise ImportError 

1391 oldstderr = os.dup(2) 

1392 os.close(2) 

1393 tmpfile = 'tmpfile.tmp' 

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

1395 pa = pyaudio.PyAudio() 

1396 os.close(2) 

1397 os.dup(oldstderr) 

1398 os.close(oldstderr) 

1399 os.remove(tmpfile) 

1400 indices = [] 

1401 devices = [] 

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

1403 info = pa.get_device_info_by_index(i) 

1404 if info['maxOutputChannels'] > 0: 

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

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

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

1408 devices.append(device) 

1409 try: 

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

1411 except OSError: 

1412 default_device = -1 

1413 return indices, devices, default_device 

1414 

1415def speaker_devices_sounddevice(): 

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

1417 

1418 Returns 

1419 ------- 

1420 indices: list of int 

1421 Device indices. 

1422 devices: list of str 

1423 Devices corresponding to `indices`. 

1424 default_device: int 

1425 Index of default device. 

1426 """ 

1427 if not audio_modules['sounddevice']: 

1428 raise ImportError 

1429 indices = [] 

1430 devices = [] 

1431 infos = sounddevice.query_devices() 

1432 for info in infos: 

1433 if info['max_output_channels'] > 0: 

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

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

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

1437 devices.append(device) 

1438 try: 

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

1440 except sounddevice.PortAudioError: 

1441 return indices, devices, -1 

1442 try: 

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

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

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

1446 info_out = info_in 

1447 except sounddevice.PortAudioError: 

1448 pass 

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

1450 

1451def speaker_devices_soundcard(): 

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

1453 

1454 Returns 

1455 ------- 

1456 indices: list of int 

1457 Device indices. 

1458 devices: list of str 

1459 Devices corresponding to `indices`. 

1460 default_device: int 

1461 Index of default device. 

1462 """ 

1463 if not audio_modules['soundcard']: 

1464 raise ImportError 

1465 indices = [] 

1466 devices = [] 

1467 infos = soundcard.all_speakers() 

1468 def_speaker = str(soundcard.default_speaker()) 

1469 default_device = -1 

1470 for i, info in enumerate(infos): 

1471 if str(info) == def_speaker: 

1472 default_device = i 

1473 indices.append(i) 

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

1475 return indices, devices, default_device 

1476 

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

1478 """Query available output devices. 

1479 

1480 Parameters 

1481 ---------- 

1482 library: str or None 

1483 If specified, use specific sound library. 

1484 verbose: int 

1485 Verbosity level. 

1486 

1487 Returns 

1488 ------- 

1489 indices: list of int 

1490 Device indices. 

1491 devices: list of str 

1492 Devices corresponding to `indices`. 

1493 default_device: int 

1494 Index of default device. 

1495 """ 

1496 # list of implemented list functions: 

1497 audio_devices = [ 

1498 ['sounddevice', speaker_devices_sounddevice], 

1499 ['pyaudio', speaker_devices_pyaudio], 

1500 ['simpleaudio', None], 

1501 ['soundcard', speaker_devices_soundcard], 

1502 ['ossaudiodev', None], 

1503 ['winsound', None] 

1504 ] 

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

1506 sa = audio_open.pop(2) 

1507 audio_open.insert(0, sa) 

1508 # query audio devices by trying various modules: 

1509 success = False 

1510 for lib, devices in audio_devices: 

1511 if library and library != lib: 

1512 continue 

1513 if not audio_modules[lib]: 

1514 if verbose > 0: 

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

1516 continue 

1517 if devices is None: 

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

1519 else: 

1520 return devices() 

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

1522 return [], [], -1 

1523 

1524 

1525def print_speaker_devices(library=None): 

1526 """Print available output devices. 

1527 

1528 Parameters 

1529 ---------- 

1530 library: str or None 

1531 If specified, use specific sound library. 

1532 """ 

1533 indices, devices, default_device = speaker_devices() 

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

1535 if i == default_device: 

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

1537 else: 

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

1539 

1540 

1541def demo(device_index=None): 

1542 """ Demonstrate the playaudio module.""" 

1543 print('play mono beep 1') 

1544 audio = PlayAudio(device_index, verbose=2) 

1545 audio.beep(1.0, 440.0) 

1546 audio.close() 

1547 

1548 print('play mono beep 2') 

1549 with PlayAudio(device_index) as audio: 

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

1551 print(' done') 

1552 sleep(0.3) 

1553 sleep(0.5) 

1554 

1555 print('play mono beep 3') 

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

1557 print(' done') 

1558 sleep(0.5) 

1559 

1560 print('play stereo beep') 

1561 duration = 1.0 

1562 rate = 44100.0 

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

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

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

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

1567 fade(data, rate, 0.1) 

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

1569 

1570 

1571def main(*args): 

1572 """Call demo with command line arguments. 

1573 

1574 Parameters 

1575 ---------- 

1576 args: list of strings 

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

1578 """ 

1579 help = False 

1580 mod = False 

1581 dev = False 

1582 ldev = False 

1583 device_index = None 

1584 for arg in args: 

1585 if mod: 

1586 if not select_module(arg): 

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

1588 return 

1589 mod = False 

1590 elif dev: 

1591 device_index = int(arg) 

1592 dev = False 

1593 elif arg == '-h': 

1594 help = True 

1595 break 

1596 elif arg == '-l': 

1597 ldev = True 

1598 break 

1599 elif arg == '-m': 

1600 mod = True 

1601 elif arg == '-d': 

1602 dev = True 

1603 else: 

1604 break 

1605 

1606 if help: 

1607 print() 

1608 print('Usage:') 

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

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

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

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

1613 return 

1614 

1615 if ldev: 

1616 print_speaker_devices() 

1617 return 

1618 

1619 demo(device_index) 

1620 

1621 

1622if __name__ == "__main__": 

1623 import sys 

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