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

789 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 07:29 +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- `fade_in()`: fade in a signal in place. 

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

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

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

43 

44 

45## Installation 

46 

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

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

49for further instructions. 

50 

51 

52## Demo 

53 

54For a demo, run the script as: 

55``` 

56python -m src.audioio.playaudio 

57``` 

58 

59""" 

60 

61from sys import platform 

62import os 

63import warnings 

64import numpy as np 

65from scipy.signal import decimate 

66from time import sleep 

67from io import BytesIO 

68from multiprocessing import Process 

69from .audiomodules import * 

70 

71 

72handle = None 

73"""Default audio device handler. 

74 

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

76`play()` or `beep()`. 

77""" 

78 

79 

80def note2freq(note, a4freq=440.0): 

81 """Convert textual note to corresponding frequency. 

82 

83 Parameters 

84 ---------- 

85 note: string 

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

87 The first character is the note, it can be 

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

89 The optional second character is either a 'b' 

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

91 The last character specifies the octave. 

92 'a4' is defined by `a4freq`. 

93 a4freq: float 

94 The frequency of a4 in Hertz. 

95 

96 Returns 

97 ------- 

98 freq: float 

99 The frequency of the note in Hertz. 

100 

101 Raises 

102 ------ 

103 ValueError: 

104 No or an invalid note was specified. 

105 """ 

106 freq = a4freq 

107 tone = 0 

108 octave = 4 

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

110 raise ValueError('no note specified') 

111 # note: 

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

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

114 index = 0 

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

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

117 index += 1 

118 # flat or sharp: 

119 flat = False 

120 sharp = False 

121 if index < len(note): 

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

123 flat = True 

124 tone -= 1 

125 index += 1 

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

127 sharp = True 

128 tone += 1 

129 index += 1 

130 # octave: 

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

132 octave = 0 

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

134 octave *= 10 

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

136 index += 1 

137 # remaining characters: 

138 if index < len(note): 

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

140 # compute frequency: 

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

142 octave -= 1 

143 tone += 12*(octave-4) 

144 # frequency: 

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

146 return freq 

147 

148 

149def fade_in(data, rate, fadetime): 

150 """Fade in a signal in place. 

151 

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

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

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

155 duration. 

156  

157 Parameters 

158 ---------- 

159 data: array 

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

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

162 rate: float 

163 The sampling rate in Hertz. 

164 fadetime: float 

165 Time for fading in in seconds. 

166 """ 

167 if len(data) < 4: 

168 return 

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

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

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

172 if data.ndim > 1: 

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

174 else: 

175 data[:nr] *= y 

176 

177 

178def fade_out(data, rate, fadetime): 

179 """Fade out a signal in place. 

180 

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

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

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

184 duration. 

185  

186 Parameters 

187 ---------- 

188 data: array 

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

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

191 rate: float 

192 The sampling rate in Hertz. 

193 fadetime: float 

194 Time for fading out in seconds. 

195 """ 

196 if len(data) < 4: 

197 return 

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

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

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

201 if data.ndim > 1: 

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

203 else: 

204 data[-nr:] *= y 

205 

206 

207def fade(data, rate, fadetime): 

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

209 

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

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

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

213 duration. 

214  

215 Parameters 

216 ---------- 

217 data: array 

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

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

220 rate: float 

221 The sampling rate in Hertz. 

222 fadetime: float 

223 Time for fading in and out in seconds. 

224 """ 

225 fade_in(data, rate, fadetime) 

226 fade_out(data, rate, fadetime) 

227 

228 

229class PlayAudio(object): 

230 """ Audio playback. 

231 

232 Parameters 

233 ---------- 

234 device_index: int or None 

235 Index of the playback device to be used. 

236 If None take the default device. 

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

238 verbose: int 

239 Verbosity level. 

240 library: str or None 

241 If specified, open a specific sound library. 

242 

243 

244 Attributes 

245 ---------- 

246 lib: string 

247 The library used for playback. 

248 verbose: int 

249 Verbosity level. 

250 

251 Methods 

252 ------- 

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

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

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

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

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

258 

259 Examples 

260 -------- 

261 ``` 

262 from audioio import PlayAudio 

263  

264 with PlayAudio() as audio: 

265 audio.beep() 

266 ``` 

267 or without context management: 

268 ``` 

269 audio = PlayAudio() 

270 audio.beep(1.0, 'a4') 

271 audio.close() 

272 ``` 

273 """ 

274 

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

276 self.verbose = verbose 

277 self.handle = None 

278 self._do_play = self._play 

279 self.close = self._close 

280 self.stop = self._stop 

281 self.lib = None 

282 self.open(device_index, library) 

283 

284 def _close(self): 

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

286 self.handle = None 

287 self._do_play = self._play 

288 self.close = self._close 

289 self.stop = self._stop 

290 self.lib = None 

291 

292 def _stop(self): 

293 """Stop any playback in progress.""" 

294 pass 

295 

296 def _play(self, blocking=True): 

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

298 pass 

299 

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

301 """Playback audio data. 

302 

303 Parameters 

304 ---------- 

305 data: array 

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

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

308 Data values range between -1 and 1. 

309 rate: float 

310 The sampling rate in Hertz. 

311 scale: float 

312 Multiply data with scale before playing. 

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

314 blocking: boolean 

315 If False do not block.  

316 device_index: int or None 

317 Index of the playback device to be used, 

318 if not already openend via the constructor. 

319 If None take the default device. 

320 

321 Raises 

322 ------ 

323 ValueError 

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

325 FileNotFoundError 

326 No audio device for playback. 

327 """ 

328 if self.handle is None: 

329 self.open(device_index) 

330 else: 

331 self.stop() 

332 self.rate = rate 

333 self.channels = 1 

334 if data.ndim > 1: 

335 self.channels = data.shape[1] 

336 # convert data: 

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

338 if scale is None: 

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

340 rawdata *= scale 

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

342 self.index = 0 

343 self._do_play(blocking) 

344 

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

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

347 """Playback a pure tone. 

348 

349 Parameters 

350 ---------- 

351 duration: float 

352 The duration of the tone in seconds. 

353 frequency: float or string 

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

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

356 See `note2freq()` for details. 

357 amplitude: float 

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

359 rate: float 

360 The sampling rate in Hertz. 

361 fadetime: float 

362 Time for fading in and out in seconds. 

363 blocking: boolean 

364 If False do not block. 

365 device_index: int or None 

366 Index of the playback device to be used, 

367 if not already openend via the constructor. 

368 If None take the default device. 

369 

370 Raises 

371 ------ 

372 ValueError 

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

374 FileNotFoundError 

375 No audio device for playback. 

376  

377 See also 

378 -------- 

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

380 for fourier series based construction of waveforms.  

381 """ 

382 # frequency 

383 if isinstance(frequency, str): 

384 frequency = note2freq(frequency) 

385 # sine wave: 

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

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

388 # fade in and out: 

389 fade(data, rate, fadetime) 

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

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

392 # play: 

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

394 device_index=device_index) 

395 

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

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

398 iscale = 1 

399 rscale = scale 

400 if isinstance(scale, int): 

401 iscale = scale 

402 rscale = 1.0 

403 elif scale > 2: 

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

405 rscale = scale/iscale 

406 

407 if iscale > 1: 

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

409 if self.data.ndim > 1: 

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

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

412 else: 

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

414 if self.verbose > 0: 

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

416 self.rate /= iscale 

417 

418 if rscale != 1.0: 

419 dt0 = 1.0/self.rate 

420 dt1 = rscale/self.rate 

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

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

423 if self.data.ndim > 1: 

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

425 for c in range(channels): 

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

427 else: 

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

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

430 if self.verbose > 0: 

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

432 self.rate /= rscale 

433 self.channels = channels 

434 

435 def __del__(self): 

436 """Terminate the audio module.""" 

437 self.close() 

438 

439 def __enter__(self): 

440 return self 

441 

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

443 self.__del__() 

444 return value 

445 

446 

447 def open_pyaudio(self, device_index=None): 

448 """Initialize audio output via PyAudio module. 

449 

450 Parameters 

451 ---------- 

452 device_index: int or None 

453 Index of the playback device to be used. 

454 If None take the default device. 

455 

456 Raises 

457 ------ 

458 ImportError 

459 PyAudio module is not available. 

460 FileNotFoundError 

461 Failed to open audio device. 

462 

463 Documentation 

464 ------------- 

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

466 http://www.portaudio.com/ 

467 

468 Installation 

469 ------------ 

470 ``` 

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

472 ``` 

473  

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

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

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

477 ``` 

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

479 ``` 

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

481 """ 

482 if not audio_modules['pyaudio']: 

483 raise ImportError 

484 oldstderr = os.dup(2) 

485 os.close(2) 

486 tmpfile = 'tmpfile.tmp' 

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

488 self.handle = pyaudio.PyAudio() 

489 self.stream = None 

490 os.close(2) 

491 os.dup(oldstderr) 

492 os.close(oldstderr) 

493 os.remove(tmpfile) 

494 try: 

495 if device_index is None: 

496 info = self.handle.get_default_output_device_info() 

497 else: 

498 info = self.handle.get_device_info_by_index(device_index) 

499 self.max_channels = info['maxOutputChannels'] 

500 self.default_rate = info['defaultSampleRate'] 

501 self.device_index = info['index'] 

502 self.handle.is_format_supported(self.default_rate, 

503 output_device=self.device_index, 

504 output_channels=1, 

505 output_format=pyaudio.paInt16) 

506 except Exception as e: 

507 if self.verbose > 0: 

508 print(str(e)) 

509 self.handle.terminate() 

510 self._close() 

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

512 self.index = 0 

513 self.data = None 

514 self.close = self._close_pyaudio 

515 self.stop = self._stop_pyaudio 

516 self._do_play = self._play_pyaudio 

517 self.lib = 'pyaudio' 

518 return self 

519 

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

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

522 flag = pyaudio.paContinue 

523 if not self.run: 

524 flag = pyaudio.paComplete 

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

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

527 self.index += len(out_data) 

528 # zero padding: 

529 if len(out_data) < frames: 

530 if self.data.ndim > 1: 

531 out_data = np.vstack((out_data, 

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

533 else: 

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

535 return (out_data, flag) 

536 else: 

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

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

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

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

541 self.index += frames 

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

543 flag = pyaudio.paComplete 

544 return (out_data, flag) 

545 

546 def _stop_pyaudio(self): 

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

548 if self.stream is not None: 

549 if self.stream.is_active(): 

550 # fade out: 

551 fadetime = 0.1 

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

553 index = self.index+nr 

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

555 nr = len(self.data) - index 

556 else: 

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

558 if nr > 0: 

559 for k in range(nr) : 

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

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

562 try: 

563 sleep(2*fadetime) 

564 except SystemError: 

565 # pyaudio interferes with sleep in python 3.10 

566 pass 

567 if self.stream.is_active(): 

568 self.run = False 

569 while self.stream.is_active(): 

570 try: 

571 sleep(0.01) 

572 except SystemError: 

573 # pyaudio interferes with sleep in python 3.10 

574 pass 

575 self.stream.stop_stream() 

576 self.stream.close() 

577 self.stream = None 

578 

579 def _play_pyaudio(self, blocking=True): 

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

581 

582 Parameters 

583 ---------- 

584 blocking: boolean 

585 If False do not block. 

586 

587 Raises 

588 ------ 

589 ValueError 

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

591 """ 

592 # check channel count: 

593 channels = self.channels 

594 if self.channels > self.max_channels: 

595 channels = self.max_channels 

596 # check sampling rate: 

597 scale_fac = 1 

598 scaled_rate = self.rate 

599 max_rate = 48000.0 

600 if self.rate > max_rate: 

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

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

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

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

605 success = False 

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

607 try: 

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

609 output_device=self.device_index, 

610 output_channels=channels, 

611 output_format=pyaudio.paInt16): 

612 if scale is None: 

613 scale = self.rate/float(rate) 

614 success = True 

615 break 

616 except Exception as e: 

617 if self.verbose > 0: 

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

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

620 raise 

621 if not success: 

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

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

624 self._down_sample(channels, scale) 

625 

626 # play: 

627 self.run = True 

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

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

630 stream_callback=self._callback_pyaudio) 

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

632 self.stream.start_stream() 

633 if blocking: 

634 while self.stream.is_active(): 

635 try: 

636 sleep(0.01) 

637 except (ValueError, SystemError): 

638 # pyaudio interferes with sleep in python 3.10 

639 pass 

640 self.run = False 

641 self.stream.stop_stream() 

642 self.stream.close() 

643 self.stream = None 

644 

645 def _close_pyaudio(self): 

646 """Terminate pyaudio module.""" 

647 self._stop_pyaudio() 

648 if self.handle is not None: 

649 self.handle.terminate() 

650 self._close() 

651 

652 

653 def open_sounddevice(self, device_index=None): 

654 """Initialize audio output via sounddevice module. 

655 

656 Parameters 

657 ---------- 

658 device_index: int or None 

659 Index of the playback device to be used. 

660 If None take the default device. 

661 

662 Raises 

663 ------ 

664 ImportError 

665 sounddevice module is not available.  

666 FileNotFoundError 

667 Failed to open audio device. 

668 

669 Documentation 

670 ------------- 

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

672 

673 Installation 

674 ------------ 

675 ``` 

676 sudo apt install -y libportaudio2 portaudio19-dev 

677 sudo pip install sounddevice 

678 ``` 

679 """ 

680 if not audio_modules['sounddevice']: 

681 raise ImportError 

682 self.handle = True 

683 self.index = 0 

684 self.data = None 

685 self.stream = None 

686 try: 

687 if device_index is None: 

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

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

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

691 info = info_out 

692 else: 

693 info = info_out 

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

695 info = info_in 

696 else: 

697 info = sounddevice.query_devices(device_index) 

698 self.device_index = info['index'] 

699 self.max_channels = info['max_output_channels'] 

700 self.default_rate = info['default_samplerate'] 

701 sounddevice.check_output_settings(device=self.device_index, 

702 channels=1, dtype=np.int16, 

703 samplerate=48000) 

704 except Exception as e: 

705 if self.verbose > 0: 

706 print(str(e)) 

707 self._close() 

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

709 self.close = self._close_sounddevice 

710 self.stop = self._stop_sounddevice 

711 self._do_play = self._play_sounddevice 

712 self.lib = 'sounddevice' 

713 return self 

714 

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

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

717 if status: 

718 print(status) 

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

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

721 if ndata >= frames : 

722 if self.data.ndim <= 1: 

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

724 else: 

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

726 self.index += frames 

727 else: 

728 if self.data.ndim <= 1: 

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

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

731 else: 

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

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

734 dtype=np.int16) 

735 self.index += frames 

736 else: 

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

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

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

740 if self.data.ndim <= 1: 

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

742 else: 

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

744 self.index += frames 

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

746 raise sounddevice.CallbackStop 

747 if not self.run: 

748 raise sounddevice.CallbackStop 

749 

750 def _stop_sounddevice(self): 

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

752 if self.stream is not None: 

753 if self.stream.active: 

754 # fade out: 

755 fadetime = 0.1 

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

757 index = self.index+nr 

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

759 nr = len(self.data) - index 

760 else: 

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

762 if nr > 0: 

763 for k in range(nr) : 

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

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

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

767 if self.stream.active: 

768 self.run = False 

769 while self.stream.active: 

770 sounddevice.sleep(10) 

771 self.stream.stop() 

772 self.stream.close() 

773 self.stream = None 

774 

775 def _play_sounddevice(self, blocking=True): 

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

777 

778 Parameters 

779 ---------- 

780 blocking: boolean 

781 If False do not block. 

782 

783 Raises 

784 ------ 

785 ValueError 

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

787 """ 

788 # check channel count: 

789 channels = self.channels 

790 if self.channels > self.max_channels: 

791 channels = self.max_channels 

792 # check sampling rate: 

793 scale_fac = 1 

794 scaled_rate = self.rate 

795 max_rate = 48000.0 

796 if self.rate > max_rate: 

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

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

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

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

801 success = False 

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

803 try: 

804 sounddevice.check_output_settings(device=self.device_index, 

805 channels=channels, 

806 dtype=np.int16, 

807 samplerate=rate) 

808 if scale is None: 

809 scale = self.rate/float(rate) 

810 success = True 

811 break 

812 except sounddevice.PortAudioError as pae: 

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

814 raise 

815 elif self.verbose > 0: 

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

817 if not success: 

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

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

820 self._down_sample(channels, scale) 

821 

822 # play: 

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

824 device=self.device_index, 

825 channels=self.channels, 

826 dtype=np.int16, 

827 callback=self._callback_sounddevice) 

828 self.latency = self.stream.latency*self.rate 

829 self.run = True 

830 self.stream.start() 

831 if blocking: 

832 while self.stream.active: 

833 sounddevice.sleep(10) 

834 self.run = False 

835 self.stream.stop() 

836 self.stream.close() 

837 self.stream = None 

838 

839 def _close_sounddevice(self): 

840 """Terminate sounddevice module.""" 

841 self._stop_sounddevice() 

842 self._close() 

843 

844 

845 def open_simpleaudio(self, device_index=None): 

846 """Initialize audio output via simpleaudio package. 

847 

848 Parameters 

849 ---------- 

850 device_index: int or None 

851 Index of the playback device to be used. 

852 If None take the default device. 

853 Not supported by simpleaudio. 

854 

855 Raises 

856 ------ 

857 ImportError 

858 simpleaudio module is not available. 

859 

860 Documentation 

861 ------------- 

862 https://simpleaudio.readthedocs.io 

863 """ 

864 if not audio_modules['simpleaudio']: 

865 raise ImportError 

866 self.handle = True 

867 self._do_play = self._play_simpleaudio 

868 self.close = self._close_simpleaudio 

869 self.stop = self._stop_simpleaudio 

870 self.lib = 'simpleaudio' 

871 return self 

872 

873 def _stop_simpleaudio(self): 

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

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

876 self.handle.stop() 

877 

878 def _play_simpleaudio(self, blocking=True): 

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

880 

881 Parameters 

882 ---------- 

883 blocking: boolean 

884 If False do not block.  

885 

886 Raises 

887 ------ 

888 ValueError 

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

890 FileNotFoundError 

891 No audio device for playback. 

892 """ 

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

894 scales = [1, None, None, None] 

895 success = False 

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

897 if scale is None: 

898 scale = self.rate/float(rate) 

899 if scale != 1: 

900 self._down_sample(self.channels, scale) 

901 try: 

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

903 2, int(self.rate)) 

904 success = True 

905 break 

906 except ValueError as e: 

907 if self.verbose > 0: 

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

909 except simpleaudio._simpleaudio.SimpleaudioError as e: 

910 if self.verbose > 0: 

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

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

913 raise FileNotFoundError('No audio device found') 

914 except Exception as e: 

915 if self.verbose > 0: 

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

917 if not success: 

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

919 elif blocking: 

920 self.handle.wait_done() 

921 

922 def _close_simpleaudio(self): 

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

924 self._stop_simpleaudio() 

925 simpleaudio.stop_all() 

926 self._close() 

927 

928 

929 def open_soundcard(self, device_index=None): 

930 """Initialize audio output via soundcard package. 

931 

932 Parameters 

933 ---------- 

934 device_index: int or None 

935 Index of the playback device to be used. 

936 If None take the default device. 

937 

938 Raises 

939 ------ 

940 ImportError 

941 soundcard module is not available. 

942 FileNotFoundError 

943 Failed to open audio device. 

944 

945 Documentation 

946 ------------- 

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

948 """ 

949 if not audio_modules['soundcard']: 

950 raise ImportError 

951 try: 

952 if device_index is None: 

953 self.handle = soundcard.default_speaker() 

954 else: 

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

956 except IndexError: 

957 raise FileNotFoundError('No audio device found') 

958 except Exception as e: 

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

960 if self.handle is None: 

961 raise FileNotFoundError('No audio device found') 

962 self._do_play = self._play_soundcard 

963 self.close = self._close_soundcard 

964 self.stop = self._stop_soundcard 

965 self.lib = 'soundcard' 

966 return self 

967 

968 def _stop_soundcard(self): 

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

970 pass 

971 

972 def _play_soundcard(self, blocking=True): 

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

974 

975 Parameters 

976 ---------- 

977 blocking: boolean 

978 If False do not block. 

979 Non-blocking playback not supported by soundcard. 

980 Return immediately without playing sound. 

981 

982 Raises 

983 ------ 

984 ValueError 

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

986 """ 

987 if not blocking: 

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

989 return 

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

991 scales = [1, None, None, None] 

992 success = False 

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

994 if scale is None: 

995 scale = self.rate/float(rate) 

996 if scale != 1: 

997 self._down_sample(self.channels, scale) 

998 try: 

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

1000 success = True 

1001 break 

1002 except RuntimeError as e: 

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

1004 if self.verbose > 0: 

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

1006 else: 

1007 if self.verbose > 0: 

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

1009 except Exception as e: 

1010 if self.verbose > 0: 

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

1012 if not success: 

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

1014 

1015 def _close_soundcard(self): 

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

1017 self._stop_soundcard() 

1018 self._close() 

1019 

1020 

1021 def open_ossaudiodev(self, device_index=None): 

1022 """Initialize audio output via ossaudiodev module. 

1023 

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

1025 

1026 Parameters 

1027 ---------- 

1028 device_index: int or None 

1029 Index of the playback device to be used. 

1030 If None take the default device. 

1031 There is only a single OSS audio device. 

1032 

1033 Raises 

1034 ------ 

1035 ImportError 

1036 ossaudiodev module is not available. 

1037 FileNotFoundError 

1038 Failed to open audio device. 

1039 

1040 Documentation 

1041 ------------- 

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

1043 

1044 Installation 

1045 ------------ 

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

1047 Enable an oss emulation via alsa by installing 

1048 ``` 

1049 sudo apt install -y osspd 

1050 ``` 

1051 """ 

1052 if not audio_modules['ossaudiodev']: 

1053 raise ImportError 

1054 self.handle = True 

1055 self.osshandle = None 

1056 self.run = False 

1057 self.play_thread = None 

1058 try: 

1059 handle = ossaudiodev.open('w') 

1060 handle.close() 

1061 except Exception as e: 

1062 if self.verbose > 0: 

1063 print(str(e)) 

1064 self._close() 

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

1066 self.close = self._close_ossaudiodev 

1067 self.stop = self._stop_ossaudiodev 

1068 self._do_play = self._play_ossaudiodev 

1069 self.lib = 'ossaudiodev' 

1070 return self 

1071 

1072 def _stop_ossaudiodev(self): 

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

1074 if self.osshandle is not None: 

1075 self.run = False 

1076 self.osshandle.reset() 

1077 if self.play_thread is not None: 

1078 if self.play_thread.is_alive(): 

1079 self.play_thread.join() 

1080 self.play_thread = None 

1081 self.osshandle.close() 

1082 self.osshandle = None 

1083 

1084 def _run_play_ossaudiodev(self): 

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

1086 self.osshandle.writeall(self.data) 

1087 if self.run: 

1088 sleep(0.5) 

1089 self.osshandle.close() 

1090 self.osshandle = None 

1091 self.run = False 

1092 

1093 def _play_ossaudiodev(self, blocking=True): 

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

1095 

1096 Raises 

1097 ------ 

1098 ValueError 

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

1100 

1101 Parameters 

1102 ---------- 

1103 blocking: boolean 

1104 If False do not block.  

1105 """ 

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

1107 self.osshandle.setfmt(ossaudiodev.AFMT_S16_LE) 

1108 # set and check channel count: 

1109 channels = self.osshandle.channels(self.channels) 

1110 # check sampling rate: 

1111 scale_fac = 1 

1112 scaled_rate = self.rate 

1113 max_rate = 48000.0 

1114 if self.rate > max_rate: 

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

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

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

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

1119 success = False 

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

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

1122 if abs(set_rate - rate) < 2: 

1123 if scale is None: 

1124 scale = self.rate/float(set_rate) 

1125 success = True 

1126 break 

1127 else: 

1128 if self.verbose > 0: 

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

1130 if not success: 

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

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

1133 self._down_sample(channels, scale) 

1134 if blocking: 

1135 self.run = True 

1136 self.osshandle.writeall(self.data) 

1137 sleep(0.5) 

1138 self.osshandle.close() 

1139 self.run = False 

1140 self.osshandle = None 

1141 else: 

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

1143 self.run = True 

1144 self.play_thread.start() 

1145 

1146 def _close_ossaudiodev(self): 

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

1148 self._stop_ossaudiodev() 

1149 self._close() 

1150 

1151 

1152 def open_winsound(self, device_index=None): 

1153 """Initialize audio output via winsound module. 

1154 

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

1156 

1157 Parameters 

1158 ---------- 

1159 device_index: int or None 

1160 Index of the playback device to be used. 

1161 If None take the default device. 

1162 Device selection is not supported by the winsound module. 

1163 

1164 Raises 

1165 ------ 

1166 ImportError 

1167 winsound module is not available. 

1168 

1169 Documentation 

1170 ------------- 

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

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

1173 """ 

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

1175 raise ImportError 

1176 self.handle = True 

1177 self._do_play = self._play_winsound 

1178 self.close = self._close_winsound 

1179 self.stop = self._stop_winsound 

1180 self.audio_file = '' 

1181 self.lib = 'winsound' 

1182 return self 

1183 

1184 def _stop_winsound(self): 

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

1186 try: 

1187 winsound.PlaySound(None, winsound.SND_MEMORY) 

1188 except Exception as e: 

1189 pass 

1190 

1191 def _play_winsound(self, blocking=True): 

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

1193 

1194 Parameters 

1195 ---------- 

1196 blocking: boolean 

1197 If False do not block.  

1198 """ 

1199 # play file: 

1200 if blocking: 

1201 # write data as wav file to memory: 

1202 self.data_buffer = BytesIO() 

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

1204 w.setnchannels(self.channels) 

1205 w.setsampwidth(2) 

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

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

1208 try: 

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

1210 except AttributeError: 

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

1212 w.close() 

1213 try: 

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

1215 except Exception as e: 

1216 if self.verbose > 0: 

1217 print(str(e)) 

1218 return 

1219 else: 

1220 if self.verbose > 0: 

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

1222 # write data as wav file to file: 

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

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

1225 w.setnchannels(self.channels) 

1226 w.setsampwidth(2) 

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

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

1229 try: 

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

1231 except AttributeError: 

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

1233 w.close() 

1234 try: 

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

1236 except Exception as e: 

1237 if self.verbose > 0: 

1238 print(str(e)) 

1239 return 

1240 

1241 def _close_winsound(self): 

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

1243 self._stop_winsound() 

1244 self.handle = None 

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

1246 os.remove(self.audio_file) 

1247 self._close() 

1248 

1249 

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

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

1252 

1253 Parameters 

1254 ---------- 

1255 device_index: int or None 

1256 Index of the playback device to be used. 

1257 If None take the default device. 

1258 library: str or None 

1259 If specified, open a specific sound library. 

1260 """ 

1261 # list of implemented play functions: 

1262 audio_open = [ 

1263 ['sounddevice', self.open_sounddevice], 

1264 ['pyaudio', self.open_pyaudio], 

1265 ['simpleaudio', self.open_simpleaudio], 

1266 ['soundcard', self.open_soundcard], 

1267 ['ossaudiodev', self.open_ossaudiodev], 

1268 ['winsound', self.open_winsound] 

1269 ] 

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

1271 sa = audio_open.pop(2) 

1272 audio_open.insert(0, sa) 

1273 # open audio device by trying various modules: 

1274 success = False 

1275 for lib, open_device in audio_open: 

1276 if library and library != lib: 

1277 continue 

1278 if not audio_modules[lib]: 

1279 if self.verbose > 0: 

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

1281 continue 

1282 try: 

1283 open_device(device_index) 

1284 success = True 

1285 if self.verbose > 0: 

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

1287 break 

1288 except Exception as e: 

1289 if self.verbose > 0: 

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

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

1292 if not success: 

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

1294 return self 

1295 

1296 

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

1298 """Playback audio data. 

1299 

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

1301 

1302 Parameters 

1303 ---------- 

1304 data: array 

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

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

1307 Data values range between -1 and 1. 

1308 rate: float 

1309 The sampling rate in Hertz. 

1310 scale: float 

1311 Multiply data with scale before playing. 

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

1313 blocking: boolean 

1314 If False do not block.  

1315 device_index: int or None 

1316 Index of the playback device to be used, 

1317 if not already openend. 

1318 If None take the default device. 

1319 verbose: int 

1320 Verbosity level.  

1321 """ 

1322 global handle 

1323 if handle is None: 

1324 handle = PlayAudio(device_index, verbose) 

1325 handle.verbose = verbose 

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

1327 

1328 

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

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

1331 """Playback a tone. 

1332 

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

1334 

1335 Parameters 

1336 ---------- 

1337 duration: float 

1338 The duration of the tone in seconds. 

1339 frequency: float or string 

1340 If float the frequency of the tone in Hertz. 

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

1342 See `note2freq()` for details 

1343 amplitude: float 

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

1345 rate: float 

1346 The sampling rate in Hertz. 

1347 fadetime: float 

1348 Time for fading in and out in seconds. 

1349 blocking: boolean 

1350 If False do not block. 

1351 device_index: int or None 

1352 Index of the playback device to be used, 

1353 if not already openend. 

1354 If None take the default device. 

1355 verbose: int 

1356 Verbosity level.  

1357 """ 

1358 global handle 

1359 if handle is None: 

1360 handle = PlayAudio(device_index, verbose) 

1361 handle.verbose = verbose 

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

1363 device_index) 

1364 

1365 

1366def close(): 

1367 """Close the global PlayAudio instance. 

1368 """ 

1369 global handle 

1370 if handle is not None: 

1371 handle.close() 

1372 handle = None 

1373 

1374 

1375def speaker_devices_pyaudio(): 

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

1377 

1378 Returns 

1379 ------- 

1380 indices: list of int 

1381 Device indices. 

1382 devices: list of str 

1383 Devices corresponding to `indices`. 

1384 """ 

1385 if not audio_modules['pyaudio']: 

1386 raise ImportError 

1387 oldstderr = os.dup(2) 

1388 os.close(2) 

1389 tmpfile = 'tmpfile.tmp' 

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

1391 pa = pyaudio.PyAudio() 

1392 os.close(2) 

1393 os.dup(oldstderr) 

1394 os.close(oldstderr) 

1395 os.remove(tmpfile) 

1396 indices = [] 

1397 devices = [] 

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

1399 info = pa.get_device_info_by_index(i) 

1400 if info['maxOutputChannels'] > 0: 

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

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

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

1404 devices.append(device) 

1405 return indices, devices 

1406 

1407def speaker_devices_sounddevice(): 

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

1409 

1410 Returns 

1411 ------- 

1412 indices: list of int 

1413 Device indices. 

1414 devices: list of str 

1415 Devices corresponding to `indices`. 

1416 """ 

1417 if not audio_modules['sounddevice']: 

1418 raise ImportError 

1419 indices = [] 

1420 devices = [] 

1421 infos = sounddevice.query_devices() 

1422 for info in infos: 

1423 if info['max_output_channels'] > 0: 

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

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

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

1427 devices.append(device) 

1428 return indices, devices 

1429 

1430def speaker_devices_soundcard(): 

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

1432 

1433 Returns 

1434 ------- 

1435 indices: list of int 

1436 Device indices. 

1437 devices: list of str 

1438 Devices corresponding to `indices`. 

1439 """ 

1440 if not audio_modules['soundcard']: 

1441 raise ImportError 

1442 indices = [] 

1443 devices = [] 

1444 infos = soundcard.all_speakers() 

1445 for i, info in enumerate(infos): 

1446 indices.append(i) 

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

1448 return indices, devices 

1449 

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

1451 """Query available output devices. 

1452 

1453 Parameters 

1454 ---------- 

1455 library: str or None 

1456 If specified, use specific sound library. 

1457 verbose: int 

1458 Verbosity level. 

1459 

1460 Returns 

1461 ------- 

1462 indices: list of int 

1463 Device indices. 

1464 devices: list of str 

1465 Devices corresponding to `indices`. 

1466 """ 

1467 # list of implemented list functions: 

1468 audio_devices = [ 

1469 ['sounddevice', speaker_devices_sounddevice], 

1470 ['pyaudio', speaker_devices_pyaudio], 

1471 ['simpleaudio', None], 

1472 ['soundcard', speaker_devices_soundcard], 

1473 ['ossaudiodev', None], 

1474 ['winsound', None] 

1475 ] 

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

1477 sa = audio_open.pop(2) 

1478 audio_open.insert(0, sa) 

1479 # query audio devices by trying various modules: 

1480 success = False 

1481 for lib, devices in audio_devices: 

1482 if library and library != lib: 

1483 continue 

1484 if not audio_modules[lib]: 

1485 if verbose > 0: 

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

1487 continue 

1488 if devices is None: 

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

1490 else: 

1491 return devices() 

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

1493 return [], [] 

1494 

1495 

1496def demo(device_index=None): 

1497 """ Demonstrate the playaudio module.""" 

1498 print('play mono beep 1') 

1499 audio = PlayAudio(device_index, verbose=2) 

1500 audio.beep(1.0, 440.0) 

1501 audio.close() 

1502 

1503 print('play mono beep 2') 

1504 with PlayAudio(device_index) as audio: 

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

1506 print(' done') 

1507 sleep(0.3) 

1508 sleep(0.5) 

1509 

1510 print('play mono beep 3') 

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

1512 print(' done') 

1513 sleep(0.5) 

1514 

1515 print('play stereo beep') 

1516 duration = 1.0 

1517 rate = 44100.0 

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

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

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

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

1522 fade(data, rate, 0.1) 

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

1524 

1525 

1526def main(*args): 

1527 """Call demo with command line arguments. 

1528 

1529 Parameters 

1530 ---------- 

1531 args: list of strings 

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

1533 """ 

1534 help = False 

1535 mod = False 

1536 dev = False 

1537 ldev = False 

1538 device_index = None 

1539 for arg in args: 

1540 if mod: 

1541 if not select_module(arg): 

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

1543 return 

1544 mod = False 

1545 elif dev: 

1546 device_index = int(arg) 

1547 dev = False 

1548 elif arg == '-h': 

1549 help = True 

1550 break 

1551 elif arg == '-l': 

1552 ldev = True 

1553 break 

1554 elif arg == '-m': 

1555 mod = True 

1556 elif arg == '-d': 

1557 dev = True 

1558 else: 

1559 break 

1560 

1561 if help: 

1562 print() 

1563 print('Usage:') 

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

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

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

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

1568 return 

1569 

1570 if ldev: 

1571 indices, devices = speaker_devices() 

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

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

1574 return 

1575 

1576 demo(device_index) 

1577 

1578 

1579if __name__ == "__main__": 

1580 import sys 

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