Coverage for src/audioio/audiowriter.py: 81%

434 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-19 19:14 +0000

1"""Write numpy arrays of floats to audio files. 

2 

3- `write_audio()`: write audio data to file. 

4- `available_formats()`: audio file formats supported by any of the installed audio modules. 

5- `available_encodings()`: encodings of an audio file format supported by any of the installed audio modules. 

6- `format_from_extension()`: deduce audio file format from file extension. 

7 

8The data to be written are 1-D or 2-D numpy arrays of floats ranging 

9between -1 and 1 with first axis time and (optional) second axis channel. 

10 

11For support of more audio formats, you might need to install 

12additional packages. 

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

14for further instructions. 

15 

16For a demo, run the script as: 

17``` 

18python -m src.audioio.audiowriter 

19``` 

20 

21""" 

22 

23import os 

24import sys 

25import subprocess 

26import numpy as np 

27 

28from io import BytesIO 

29from pathlib import Path 

30from .audiomodules import * 

31from .riffmetadata import write_wave as audioio_write_wave 

32from .riffmetadata import append_riff, write_riff_chunk 

33from .riffmetadata import append_metadata_riff, append_markers_riff 

34 

35 

36def format_from_extension(filepath): 

37 """Deduce audio file format from file extension. 

38 

39 Parameters 

40 ---------- 

41 filepath: str or Path or None 

42 Name of the audio file. 

43 

44 Returns 

45 ------- 

46 format: str 

47 Audio format deduced from file extension. 

48 """ 

49 if filepath is None: 

50 return None 

51 filepath = Path(filepath) 

52 ext = filepath.suffix 

53 if not ext: 

54 return None 

55 if ext[0] == '.': 

56 ext = ext[1:] 

57 if not ext: 

58 return None 

59 ext = ext.upper() 

60 if ext == 'WAVE': 

61 return 'WAV' 

62 ext = ext.replace('MPEG' , 'MP') 

63 return ext 

64 

65 

66def formats_wave(): 

67 """Audio file formats supported by the wave module. 

68 

69 Returns 

70 ------- 

71 formats: list of str 

72 List of supported file formats as strings. 

73 """ 

74 if not audio_modules['wave']: 

75 return [] 

76 else: 

77 return ['WAV'] 

78 

79 

80def encodings_wave(format): 

81 """Encodings of an audio file format supported by the wave module. 

82 

83 Parameters 

84 ---------- 

85 format: str 

86 The file format. 

87 

88 Returns 

89 ------- 

90 encodings: list of str 

91 List of supported encodings as strings. 

92 """ 

93 if not audio_modules['wave']: 

94 return [] 

95 elif format.upper() != 'WAV': 

96 return [] 

97 else: 

98 return ['PCM_32', 'PCM_16', 'PCM_U8'] 

99 

100 

101def write_wave(filepath, data, rate, metadata=None, locs=None, 

102 labels=None, format=None, encoding=None, 

103 marker_hint='cue'): 

104 """Write audio data using the wave module from pythons standard libray. 

105  

106 Documentation 

107 ------------- 

108 https://docs.python.org/3.8/library/wave.html 

109 

110 Parameters 

111 ---------- 

112 filepath: str or Path 

113 Full path and name of the file to write. 

114 data: 1-D or 2-D array of floats 

115 Array with the data (first index time, second index channel, 

116 values within -1.0 and 1.0). 

117 rate: float 

118 Sampling rate of the data in Hertz. 

119 metadata: None or nested dict 

120 Metadata as key-value pairs. Values can be strings, integers, 

121 or dictionaries. 

122 locs: None or 1-D or 2-D array of ints 

123 Marker positions (first column) and spans (optional second column) 

124 for each marker (rows). 

125 labels: None or 2-D array of string objects 

126 Labels (first column) and texts (optional second column) 

127 for each marker (rows). 

128 format: str or None 

129 File format, only 'WAV' is supported. 

130 encoding: str or None 

131 Encoding of the data: 'PCM_32', 'PCM_16', or 'PCM_U8'. 

132 If None or empty string use 'PCM_16'. 

133 marker_hint: str 

134 - 'cue': store markers in cue and and adtl chunks. 

135 - 'lbl': store markers in avisoft lbl chunk. 

136 

137 Raises 

138 ------ 

139 ImportError 

140 The wave module is not installed. 

141 * 

142 Writing of the data failed. 

143 ValueError 

144 File format or encoding not supported. 

145 """ 

146 if not audio_modules['wave']: 

147 raise ImportError 

148 if not format: 

149 format = format_from_extension(filepath) 

150 if format and format.upper() != 'WAV': 

151 raise ValueError('file format %s not supported by wave module' % format) 

152 

153 wave_encodings = {'PCM_32': [4, 'i4'], 

154 'PCM_16': [2, 'i2'], 

155 'PCM_U8': [1, 'u1'] } 

156 if not encoding: 

157 encoding = 'PCM_16' 

158 encoding = encoding.upper() 

159 if encoding not in wave_encodings: 

160 raise ValueError('file encoding %s not supported by wave module' % encoding) 

161 sampwidth = wave_encodings[encoding][0] 

162 dtype = wave_encodings[encoding][1] 

163 

164 wf = wave.open(os.fspath(filepath), 'w') # 'with' is not supported by wave 

165 channels = 1 

166 if len(data.shape) > 1: 

167 channels = data.shape[1] 

168 wf.setnchannels(channels) 

169 wf.setnframes(len(data)) 

170 wf.setframerate(int(rate)) 

171 wf.setsampwidth(sampwidth) 

172 factor = 2**(sampwidth*8-1) 

173 if sampwidth == 1: 

174 buffer = np.floor((data+1.0) * factor).astype(dtype) 

175 buffer[data >= 1.0] = 2*factor - 1 

176 else: 

177 buffer = np.floor(data * factor).astype(dtype) 

178 buffer[data >= 1.0] = factor - 1 

179 wf.writeframes(buffer.tobytes()) 

180 wf.close() 

181 append_riff(filepath, metadata, locs, labels, rate, marker_hint) 

182 

183 

184def formats_ewave(): 

185 """Audio file formats supported by the ewave module. 

186 

187 Returns 

188 ------- 

189 formats: list of str 

190 List of supported file formats as strings. 

191 """ 

192 if not audio_modules['ewave']: 

193 return [] 

194 else: 

195 return ['WAV', 'WAVEX'] 

196 

197 

198def encodings_ewave(format): 

199 """Encodings of an audio file format supported by the ewave module. 

200 

201 Parameters 

202 ---------- 

203 format: str 

204 The file format. 

205 

206 Returns 

207 ------- 

208 encodings: list of str 

209 List of supported encodings as strings. 

210 """ 

211 if not audio_modules['ewave']: 

212 return [] 

213 elif format.upper() != 'WAV' and format.upper() != 'WAVEX': 

214 return [] 

215 else: 

216 return ['PCM_64', 'PCM_32', 'PCM_16', 'FLOAT', 'DOUBLE'] 

217 

218 

219def write_ewave(filepath, data, rate, metadata=None, locs=None, 

220 labels=None, format=None, encoding=None, 

221 marker_hint='cue'): 

222 """Write audio data using the ewave module from pythons standard libray. 

223 

224 Documentation 

225 ------------- 

226 https://github.com/melizalab/py-ewave 

227 

228 Parameters 

229 ---------- 

230 filepath: str 

231 Full path and name of the file to write. 

232 data: 1-D or 2-D array of floats 

233 Array with the data (first index time, second index channel, 

234 values within -1.0 and 1.0). 

235 rate: float 

236 Sampling rate of the data in Hertz. 

237 metadata: None or nested dict 

238 Metadata as key-value pairs. Values can be strings, integers, 

239 or dictionaries. 

240 locs: None or 1-D or 2-D array of ints 

241 Marker positions (first column) and spans (optional second column) 

242 for each marker (rows). 

243 labels: None or 2-D array of string objects 

244 Labels (first column) and texts (optional second column) 

245 for each marker (rows). 

246 format: str or None 

247 File format, only 'WAV' and 'WAVEX' are supported. 

248 encoding: str or None 

249 Encoding of the data: 'PCM_64', 'PCM_32', PCM_16', 'FLOAT', 'DOUBLE' 

250 If None or empty string use 'PCM_16'. 

251 marker_hint: str 

252 - 'cue': store markers in cue and and adtl chunks. 

253 - 'lbl': store markers in avisoft lbl chunk. 

254 

255 Raises 

256 ------ 

257 ImportError 

258 The ewave module is not installed. 

259 * 

260 Writing of the data failed. 

261 ValueError 

262 File format or encoding not supported. 

263 """ 

264 if not audio_modules['ewave']: 

265 raise ImportError 

266 

267 if not format: 

268 format = format_from_extension(filepath) 

269 if format and format.upper() != 'WAV' and format.upper() != 'WAVEX': 

270 raise ValueError('file format %s not supported by ewave module' % format) 

271 

272 ewave_encodings = {'PCM_64': 'l', 

273 'PCM_32': 'i', 

274 'PCM_16': 'h', 

275 'FLOAT': 'f', 

276 'DOUBLE': 'd' } 

277 if not encoding: 

278 encoding = 'PCM_16' 

279 encoding = encoding.upper() 

280 if encoding not in ewave_encodings: 

281 raise ValueError('file encoding %s not supported by ewave module' % encoding) 

282 

283 channels = 1 

284 if len(data.shape) > 1: 

285 channels = data.shape[1] 

286 

287 with ewave.open(filepath, 'w', sampling_rate=int(rate), 

288 dtype=ewave_encodings[encoding], nchannels=channels) as wf: 

289 wf.write(data, scale=True) 

290 append_riff(filepath, metadata, locs, labels, rate, marker_hint) 

291 

292 

293def formats_wavfile(): 

294 """Audio file formats supported by the scipy.io.wavfile module. 

295 

296 Returns 

297 ------- 

298 formats: list of str 

299 List of supported file formats as strings. 

300 """ 

301 if not audio_modules['scipy.io.wavfile']: 

302 return [] 

303 else: 

304 return ['WAV'] 

305 

306 

307def encodings_wavfile(format): 

308 """Encodings of an audio file format supported by the scipy.io.wavfile module. 

309 

310 Parameters 

311 ---------- 

312 format: str 

313 The file format. 

314 

315 Returns 

316 ------- 

317 encodings: list of str 

318 List of supported encodings as strings. 

319 """ 

320 if not audio_modules['scipy.io.wavfile']: 

321 return [] 

322 elif format.upper() != 'WAV': 

323 return [] 

324 else: 

325 return ['PCM_U8', 'PCM_16', 'PCM_32', 'PCM_64', 'FLOAT', 'DOUBLE'] 

326 

327 

328def write_wavfile(filepath, data, rate, metadata=None, locs=None, 

329 labels=None, format=None, encoding=None, 

330 marker_hint='cue'): 

331 """Write audio data using the scipy.io.wavfile module. 

332  

333 Documentation 

334 ------------- 

335 http://docs.scipy.org/doc/scipy/reference/io.html 

336 

337 Parameters 

338 ---------- 

339 filepath: str or Path 

340 Full path and name of the file to write. 

341 data: 1-D or 2-D array of floats 

342 Array with the data (first index time, second index channel, 

343 values within -1.0 and 1.0). 

344 rate: float 

345 Sampling rate of the data in Hertz. 

346 metadata: None or nested dict 

347 Metadata as key-value pairs. Values can be strings, integers, 

348 or dictionaries. 

349 locs: None or 1-D or 2-D array of ints 

350 Marker positions (first column) and spans (optional second column) 

351 for each marker (rows). 

352 labels: None or 2-D array of string objects 

353 Labels (first column) and texts (optional second column) 

354 for each marker (rows). 

355 format: str or None 

356 File format, only 'WAV' is supported. 

357 encoding: str or None 

358 Encoding of the data: 'PCM_64', 'PCM_32', PCM_16', 'PCM_U8', 'FLOAT', 'DOUBLE' 

359 If None or empty string use 'PCM_16'. 

360 marker_hint: str 

361 - 'cue': store markers in cue and and adtl chunks. 

362 - 'lbl': store markers in avisoft lbl chunk. 

363 

364 Raises 

365 ------ 

366 ImportError 

367 The wavfile module is not installed. 

368 ValueError 

369 File format or encoding not supported. 

370 * 

371 Writing of the data failed. 

372 """ 

373 if not audio_modules['scipy.io.wavfile']: 

374 raise ImportError 

375 

376 if not format: 

377 format = format_from_extension(filepath) 

378 if format and format.upper() != 'WAV': 

379 raise ValueError('file format %s not supported by scipy.io.wavfile module' % format) 

380 

381 wave_encodings = {'PCM_U8': [1, 'u1'], 

382 'PCM_16': [2, 'i2'], 

383 'PCM_32': [4, 'i4'], 

384 'PCM_64': [8, 'i8'], 

385 'FLOAT': [4, 'f'], 

386 'DOUBLE': [8, 'd']} 

387 if not encoding: 

388 encoding = 'PCM_16' 

389 encoding = encoding.upper() 

390 if encoding not in wave_encodings: 

391 raise ValueError('file encoding %s not supported by scipy.io.wavfile module' % encoding) 

392 sampwidth = wave_encodings[encoding][0] 

393 dtype = wave_encodings[encoding][1] 

394 if sampwidth == 1: 

395 factor = 2**(sampwidth*8-1) 

396 buffer = np.floor((data+1.0) * factor).astype(dtype) 

397 buffer[data >= 1.0] = 2*factor - 1 

398 elif dtype[0] == 'i': 

399 factor = 2**(sampwidth*8-1) 

400 buffer = np.floor(data * factor).astype(dtype) 

401 buffer[data >= 1.0] = factor - 1 

402 else: 

403 buffer = data.astype(dtype, copy=False) 

404 wavfile.write(filepath, int(rate), buffer) 

405 append_riff(filepath, metadata, locs, labels, rate, marker_hint) 

406 

407 

408def formats_soundfile(): 

409 """Audio file formats supported by the SoundFile module. 

410 

411 Returns 

412 ------- 

413 formats: list of str 

414 List of supported file formats as strings. 

415 """ 

416 if not audio_modules['soundfile']: 

417 return [] 

418 else: 

419 return sorted(list(soundfile.available_formats())) 

420 

421 

422def encodings_soundfile(format): 

423 """Encodings of an audio file format supported by the SoundFile module. 

424 

425 Parameters 

426 ---------- 

427 format: str 

428 The file format. 

429 

430 Returns 

431 ------- 

432 encodings: list of str 

433 List of supported encodings as strings. 

434 """ 

435 if not audio_modules['soundfile']: 

436 return [] 

437 else: 

438 return sorted(list(soundfile.available_subtypes(format))) 

439 

440 

441def write_soundfile(filepath, data, rate, metadata=None, locs=None, 

442 labels=None, format=None, encoding=None, 

443 marker_hint='cue'): 

444 """Write audio data using the SoundFile module (based on libsndfile). 

445  

446 Documentation 

447 ------------- 

448 http://pysoundfile.readthedocs.org 

449 

450 Parameters 

451 ---------- 

452 filepath: str or Path 

453 Full path and name of the file to write. 

454 data: 1-D or 2-D array of floats 

455 Array with the data (first index time, second index channel, 

456 values within -1.0 and 1.0). 

457 rate: float 

458 Sampling rate of the data in Hertz. 

459 metadata: None or nested dict 

460 Metadata as key-value pairs. Values can be strings, integers, 

461 or dictionaries. 

462 locs: None or 1-D or 2-D array of ints 

463 Marker positions (first column) and spans (optional second column) 

464 for each marker (rows). 

465 labels: None or 2-D array of string objects 

466 Labels (first column) and texts (optional second column) 

467 for each marker (rows). 

468 format: str or None 

469 File format. 

470 encoding: str or None 

471 Encoding of the data. 

472 If None or empty string use 'PCM_16'. 

473 marker_hint: str 

474 - 'cue': store markers in cue and and adtl chunks. 

475 - 'lbl': store markers in avisoft lbl chunk. 

476 

477 Raises 

478 ------ 

479 ImportError 

480 The SoundFile module is not installed. 

481 * 

482 Writing of the data failed. 

483 """ 

484 if not audio_modules['soundfile']: 

485 raise ImportError 

486 

487 if not format: 

488 format = format_from_extension(filepath) 

489 if format: 

490 format = format.upper() 

491 

492 if not encoding: 

493 encoding = 'PCM_16' 

494 encoding = encoding.upper() 

495 

496 soundfile.write(filepath, data, int(rate), format=format, 

497 subtype=encoding) 

498 try: 

499 append_riff(filepath, metadata, locs, labels, rate, marker_hint) 

500 except ValueError: 

501 pass 

502 

503 

504def formats_wavefile(): 

505 """Audio file formats supported by the wavefile module. 

506 

507 Returns 

508 ------- 

509 formats: list of str 

510 List of supported file formats as strings. 

511 """ 

512 if not audio_modules['wavefile']: 

513 return [] 

514 formats = [] 

515 for attr in dir(wavefile.Format): 

516 v = getattr(wavefile.Format, attr) 

517 if ( isinstance(v, int) 

518 and v & wavefile.Format.TYPEMASK > 0 

519 and v != wavefile.Format.TYPEMASK ): 

520 formats.append(attr) 

521 return sorted(formats) 

522 

523 

524def encodings_wavefile(format): 

525 """Encodings supported by the wavefile module. 

526 

527 Parameters 

528 ---------- 

529 format: str 

530 The file format (ignored). 

531 

532 Returns 

533 ------- 

534 encodings: list of str 

535 List of supported encodings as strings. 

536 """ 

537 if not audio_modules['wavefile']: 

538 return [] 

539 if not format.upper() in formats_wavefile(): 

540 return [] 

541 encodings = [] 

542 for attr in dir(wavefile.Format): 

543 v = getattr(wavefile.Format, attr) 

544 if ( isinstance(v, int) 

545 and v & wavefile.Format.SUBMASK > 0 

546 and v != wavefile.Format.SUBMASK ): 

547 encodings.append(attr) 

548 return sorted(encodings) 

549 

550 

551def write_wavefile(filepath, data, rate, metadata=None, locs=None, 

552 labels=None, format=None, encoding=None, 

553 marker_hint='cue'): 

554 """Write audio data using the wavefile module (based on libsndfile). 

555  

556 Documentation 

557 ------------- 

558 https://github.com/vokimon/python-wavefile 

559 

560 Parameters 

561 ---------- 

562 filepath: str or Path 

563 Full path and name of the file to write. 

564 data: 1-D or 2-D array of floats 

565 Array with the data (first index time, second index channel, 

566 values within -1.0 and 1.0). 

567 rate: float 

568 Sampling rate of the data in Hertz. 

569 metadata: None or nested dict 

570 Metadata as key-value pairs. Values can be strings, integers, 

571 or dictionaries. 

572 locs: None or 1-D or 2-D array of ints 

573 Marker positions (first column) and spans (optional second column) 

574 for each marker (rows). 

575 labels: None or 2-D array of string objects 

576 Labels (first column) and texts (optional second column) 

577 for each marker (rows). 

578 format: str or None 

579 File format as in wavefile.Format. 

580 encoding: str or None 

581 Encoding of the data as in wavefile.Format. 

582 If None or empty string use 'PCM_16'. 

583 marker_hint: str 

584 - 'cue': store markers in cue and and adtl chunks. 

585 - 'lbl': store markers in avisoft lbl chunk. 

586 

587 Raises 

588 ------ 

589 ImportError 

590 The wavefile module is not installed. 

591 ValueError 

592 File format or encoding not supported. 

593 * 

594 Writing of the data failed. 

595 """ 

596 if not audio_modules['wavefile']: 

597 raise ImportError 

598 

599 if not format: 

600 format = format_from_extension(filepath) 

601 format = format.upper() 

602 try: 

603 format_value = getattr(wavefile.Format, format) 

604 except AttributeError: 

605 raise ValueError('file format %s not supported by wavefile module' % format) 

606 

607 if not encoding: 

608 encodings = encodings_wavefile(format) 

609 encoding = encodings[0] 

610 if 'PCM_16' in encodings: 

611 encoding = 'PCM_16' 

612 encoding = encoding.upper() 

613 try: 

614 encoding_value = getattr(wavefile.Format, encoding) 

615 except AttributeError: 

616 raise ValueError('file encoding %s not supported by wavefile module' % encoding) 

617 

618 channels = 1 

619 if len(data.shape) > 1: 

620 channels = data.shape[1] 

621 else: 

622 data = data.reshape((-1, 1)) 

623 with wavefile.WaveWriter(os.fspath(filepath), channels=channels, 

624 samplerate=int(rate), 

625 format=format_value|encoding_value) as w: 

626 w.write(data.T) 

627 try: 

628 append_riff(filepath, metadata, locs, labels, rate, marker_hint) 

629 except ValueError: 

630 pass 

631 

632 

633def formats_wavpack(): 

634 """Audio file formats supported by the wavpack module. 

635 

636 Returns 

637 ------- 

638 formats: list of str 

639 List of supported file formats as strings. 

640 """ 

641 if not audio_modules['wavpack']: 

642 return [] 

643 else: 

644 return ['WV'] 

645 

646def encodings_wavpack(format): 

647 """Encodings of an audio file format supported by the wavpack module. 

648 

649 Parameters 

650 ---------- 

651 format: str 

652 The file format. 

653 

654 Returns 

655 ------- 

656 encodings: list of str 

657 List of supported encodings as strings. 

658 """ 

659 if not audio_modules['wavpack']: 

660 return [] 

661 elif format.upper() != 'WV': 

662 return [] 

663 else: 

664 return ['PCM_FLOAT'] 

665 

666def write_wavpack(filepath, data, rate, metadata=None, locs=None, 

667 labels=None, format=None, encoding=None, 

668 marker_hint='cue'): 

669 """Write audio data using the wavpack module. 

670  

671 Documentation 

672 ------------- 

673 https://www.wavpack.com 

674 

675 Parameters 

676 ---------- 

677 filepath: str or Path 

678 Full path and name of the file to write. 

679 data: 1-D or 2-D array of floats 

680 Array with the data (first index time, second index channel, 

681 values within -1.0 and 1.0). 

682 rate: float 

683 Sampling rate of the data in Hertz. 

684 metadata: None or nested dict 

685 Metadata as key-value pairs. Values can be strings, integers, 

686 or dictionaries. 

687 locs: None or 1-D or 2-D array of ints 

688 Marker positions (first column) and spans (optional second column) 

689 for each marker (rows). 

690 labels: None or 2-D array of string objects 

691 Labels (first column) and texts (optional second column) 

692 for each marker (rows). 

693 format: str or None 

694 File format, only 'WAV' is supported. 

695 encoding: str or None 

696 Encoding of the data: 'PCM_32', 'PCM_16', or 'PCM_U8'. 

697 If None or empty string use 'PCM_16'. 

698 marker_hint: str 

699 - 'cue': store markers in cue and and adtl chunks. 

700 - 'lbl': store markers in avisoft lbl chunk. 

701 

702 Raises 

703 ------ 

704 ImportError 

705 The wavpack library is not installed. 

706 * 

707 Writing of the data failed. 

708 ValueError 

709 File format or encoding not supported. 

710 """ 

711 if not audio_modules['wavpack']: 

712 raise ImportError 

713 

714 @wavpack.WavpackBlockOutput 

715 def _wavpack_write_block(out_file, data_ptr, bcount): 

716 if not out_file or not data_ptr or bcount <= 0: 

717 return 0 

718 try: 

719 out_file.write(ctypes.string_at(data_ptr, bcount)) 

720 return 1 

721 except Exception: 

722 return 0 

723 

724 if not format: 

725 format = format_from_extension(filepath) 

726 if format and format.upper() != 'WV': 

727 raise ValueError(f'file format {format} not supported by wavpack module') 

728 if not encoding: 

729 encoding = 'PCM_FLOAT' 

730 encoding = encoding.upper() 

731 if encoding != 'PCM_FLOAT': 

732 raise ValueError(f'file encoding {encoding} not supported by wavpack module') 

733 nchannels = 1 

734 if len(data.shape) > 1: 

735 nchannels = data.shape[1] 

736 else: 

737 data = data.reshape((-1, 1)) 

738 # WavPack float mode expects the raw IEEE-754 bit patterns as int32: 

739 data = np.clip(data, -1.0, 1.0).astype(np.float32) 

740 interleaved = np.ascontiguousarray(data.reshape(-1)) 

741 buffer = interleaved.view(np.int32) 

742 # metadata: 

743 meta_buffer = BytesIO() 

744 write_riff_chunk(meta_buffer) 

745 append_metadata_riff(meta_buffer, metadata) 

746 append_markers_riff(meta_buffer, locs, labels, marker_hint) 

747 meta_bytes = meta_buffer.getvalue() 

748 # write wavpack file: 

749 with open(filepath, 'wb') as dest: 

750 wpc = wavpack.WavpackOpenFileOutput(_wavpack_write_block, dest, None) 

751 if not wpc: 

752 raise IOError(f'failed to open "{filepath}" for writing wavpack file') 

753 config = WavpackConfig() 

754 config.bits_per_sample = 32 

755 config.bytes_per_sample = 4 

756 config.num_channels = nchannels 

757 config.sample_rate = int(rate) 

758 config.flags = wavpack.CONFIG_FLOAT_DATA 

759 config.float_norm_exp = 127 # floats normalized to +/-1 

760 

761 if not wavpack.WavpackSetConfiguration64(wpc, ctypes.byref(config), 

762 len(data), None): 

763 raise RuntimeError("wavpack: WavPackSetConfiguration() failed.") 

764 if not wavpack.WavpackAddWrapper(wpc, meta_bytes, len(meta_bytes)): 

765 raise RuntimeError("wavpack: WavpackAddWrapper() failed.") 

766 if not wavpack.WavpackPackInit(wpc): 

767 raise RuntimeError("wavpack: WavpackPackInit() failed.") 

768 if not wavpack.WavpackPackSamples(wpc, 

769 buffer.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)), 

770 len(data)): 

771 raise RuntimeError("wavpack: WavpackPackSamples() failed.") 

772 wavpack.WavpackFlushSamples(wpc) 

773 wavpack.WavpackCloseFile(wpc) 

774 

775 

776pydub_formats = None 

777 

778def formats_pydub(): 

779 """Audio file formats supported by the Pydub module. 

780 

781 Returns 

782 ------- 

783 formats: list of str 

784 List of supported file formats as strings. 

785 """ 

786 if not audio_modules['pydub']: 

787 return [] 

788 global pydub_formats 

789 if pydub_formats is not None: 

790 return pydub_formats 

791 formats = [] 

792 command = [pydub.AudioSegment.converter, '-formats'] 

793 with open(os.devnull, 'rb') as devnull: 

794 p = subprocess.Popen(command, stdin=devnull, stdout=subprocess.PIPE, 

795 stderr=subprocess.PIPE, universal_newlines=True) 

796 skip = True 

797 for line in p.communicate()[0].split('\n'): 

798 if '--' in line[:3]: 

799 skip = False 

800 continue 

801 if skip: 

802 continue 

803 cols = line.split() 

804 if len(cols) > 2 and 'E' in cols[0]: 

805 formats.append(cols[1].upper()) 

806 pydub_formats = formats 

807 return formats 

808 

809pydub_encodings = None 

810 

811def encodings_pydub(format): 

812 """Encodings of an audio file format supported by the Pydub module. 

813 

814 Parameters 

815 ---------- 

816 format: str 

817 The file format. 

818 

819 Returns 

820 ------- 

821 encodings: list of str 

822 List of supported encodings as strings. 

823 """ 

824 pydub_encodings = {'pcm_s16le': 'PCM_16', 

825 'pcm_s24le': 'PCM_24', 

826 'pcm_s32le': 'PCM_32', 

827 'pcm_f32le': 'FLOAT', 

828 'pcm_f64le': 'DOUBLE', 

829 } 

830 if not audio_modules['pydub']: 

831 return [] 

832 if format.upper() not in formats_pydub(): 

833 return [] 

834 global pydup_encodings 

835 if pydub_encodings is not None: 

836 return pydub_encodings 

837 encodings = [] 

838 command = [pydub.AudioSegment.converter, '-encoders'] 

839 with open(os.devnull, 'rb') as devnull: 

840 p = subprocess.Popen(command, stdin=devnull, stdout=subprocess.PIPE, 

841 stderr=subprocess.PIPE, universal_newlines=True) 

842 skip = True 

843 for line in p.communicate()[0].split('\n'): 

844 if '--' in line[:3]: 

845 skip = False 

846 continue 

847 if skip: 

848 continue 

849 cols = line.split() 

850 if len(cols) > 2 and cols[0][0] == 'A': 

851 encoding = cols[1] 

852 if encoding in pydub_encodings: 

853 encoding = pydub_encodings[encoding] 

854 encodings.append(encoding.upper()) 

855 pydup_encodings = encodings 

856 return encodings 

857 

858def write_pydub(filepath, data, rate, metadata=None, locs=None, 

859 labels=None, format=None, encoding=None, 

860 marker_hint='cue'): 

861 """Write audio data using the Pydub module. 

862  

863 Documentation 

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

865 https://github.com/jiaaro/pydub 

866 

867 Parameters 

868 ---------- 

869 filepath: str or Path 

870 Full path and name of the file to write. 

871 data: 1-D or 2-D array of floats 

872 Array with the data (first index time, second index channel, 

873 values within -1.0 and 1.0). 

874 rate: float 

875 Sampling rate of the data in Hertz. 

876 metadata: None or nested dict 

877 Metadata as key-value pairs. Values can be strings, integers, 

878 or dictionaries. 

879 locs: None or 1-D or 2-D array of ints 

880 Marker positions (first column) and spans (optional second column) 

881 for each marker (rows). 

882 labels: None or 2-D array of string objects 

883 Labels (first column) and texts (optional second column) 

884 for each marker (rows). 

885 format: str or None 

886 File format, everything ffmpeg or avtools are supporting. 

887 encoding: str or None 

888 Encoding of the data. 

889 If None or empty string use 'PCM_16'. 

890 marker_hint: str 

891 - 'cue': store markers in cue and and adtl chunks. 

892 - 'lbl': store markers in avisoft lbl chunk. 

893 

894 Raises 

895 ------ 

896 ImportError 

897 The Pydub module is not installed. 

898 * 

899 Writing of the data failed. 

900 ValueError 

901 File format or encoding not supported. 

902 """ 

903 if not audio_modules['pydub']: 

904 raise ImportError 

905 

906 if not format: 

907 format = format_from_extension(filepath) 

908 if format and format.upper() not in formats_pydub(): 

909 raise ValueError('file format %s not supported by Pydub module' % format) 

910 

911 pydub_encodings = {'PCM_16': 'pcm_s16le', 

912 'PCM_24': 'pcm_s24le', 

913 'PCM_32': 'pcm_s32le', 

914 'DOUBLE': 'pcm_f32le', 

915 'FLOAT': 'pcm_f64le', 

916 } 

917 if encoding: 

918 encoding = encoding.upper() 

919 if encoding in pydub_encodings: 

920 encoding = pydub_encodings[encoding] 

921 if encoding not in encodings_pydub(format): 

922 raise ValueError('file encoding %s not supported by Pydub module' % encoding) 

923 encoding = encoding.lower() 

924 else: 

925 encoding = None 

926 

927 channels = 1 

928 if len(data.shape) > 1: 

929 channels = data.shape[1] 

930 int_data = (data*(2**31-1)).astype(np.int32) 

931 sound = pydub.AudioSegment(int_data.ravel(), sample_width=4, 

932 frame_rate=rate, channels=channels) 

933 sound.export(filepath, format=format.lower(), codec=encoding) 

934 try: 

935 append_riff(filepath, metadata, locs, labels, rate, marker_hint) 

936 except ValueError: 

937 pass 

938 

939 

940audio_formats_funcs = ( 

941 ('soundfile', formats_soundfile), 

942 ('wavpack', formats_wavpack), 

943 ('wavefile', formats_wavefile), 

944 ('wave', formats_wave), 

945 ('ewave', formats_ewave), 

946 ('scipy.io.wavfile', formats_wavfile), 

947 ('pydub', formats_pydub) 

948 ) 

949""" List of implemented formats() functions. 

950 

951Each element of the list is a tuple with the module's name and the formats() function. 

952""" 

953 

954 

955def available_formats(): 

956 """Audio file formats supported by any of the installed audio modules. 

957 

958 Returns 

959 ------- 

960 formats: list of str 

961 List of supported file formats as strings. 

962 

963 Examples 

964 -------- 

965 ``` 

966 >>> from audioio import available_formats 

967 >>> f = available_formats() 

968 >>> printf(f) 

969 ['3G2', '3GP', 'A64', 'AC3', 'ADTS', 'ADX', 'AIFF', ..., 'WAV', 'WAVEX', 'WEBM', 'WEBM_CHUNK', 'WEBM_DASH_MANIFEST', 'WEBP', 'WEBVTT', 'WTV', 'WV', 'WVE', 'XI', 'XV', 'YUV4MPEGPIPE'] 

970 ``` 

971 """ 

972 formats = set() 

973 for module, formats_func in audio_formats_funcs: 

974 formats |= set(formats_func()) 

975 return sorted(list(formats)) 

976 

977 

978audio_encodings_funcs = ( 

979 ('soundfile', encodings_soundfile), 

980 ('wavpack', encodings_wavpack), 

981 ('wavefile', encodings_wavefile), 

982 ('wave', encodings_wave), 

983 ('ewave', encodings_ewave), 

984 ('scipy.io.wavfile', encodings_wavfile), 

985 ('pydub', encodings_pydub) 

986 ) 

987""" List of implemented encodings() functions. 

988 

989Each element of the list is a tuple with the module's name and the encodings() function. 

990""" 

991 

992 

993def available_encodings(format): 

994 """Encodings of an audio file format supported by any of the installed audio modules. 

995 

996 Parameters 

997 ---------- 

998 format: str 

999 The file format. 

1000 

1001 Returns 

1002 ------- 

1003 encodings: list of str 

1004 List of supported encodings as strings. 

1005 

1006 Example 

1007 ------- 

1008 ``` 

1009 >>> from audioio import available_encodings 

1010 >>> e = available_encodings('WAV') 

1011 >>> printf(e) 

1012 ['ALAW', 'DOUBLE', 'FLOAT', 'G721_32', 'GSM610', 'IMA_ADPCM', 'MS_ADPCM', 'PCM_16', 'PCM_24', 'PCM_32', 'PCM_U8', 'ULAW'] 

1013 ``` 

1014 """ 

1015 for module, encodings_func in audio_encodings_funcs: 

1016 encs = encodings_func(format) 

1017 if len(encs) > 0: 

1018 return encs 

1019 return [] 

1020 

1021 

1022audio_writer_funcs = ( 

1023 ('soundfile', write_soundfile), 

1024 ('wavpack', write_wavpack), 

1025 ('wavefile', write_wavefile), 

1026 ('wave', write_wave), 

1027 ('ewave', write_ewave), 

1028 ('scipy.io.wavfile', write_wavfile), 

1029 ('pydub', write_pydub) 

1030 ) 

1031""" List of implemented write() functions. 

1032 

1033Each element of the list is a tuple with the module's name and the write() function. 

1034""" 

1035 

1036 

1037def write_audio(filepath, data, rate, metadata=None, locs=None, 

1038 labels=None, format=None, encoding=None, 

1039 marker_hint='cue', verbose=0): 

1040 """Write audio data, metadata, and marker to file. 

1041 

1042 Parameters 

1043 ---------- 

1044 filepath: str or Path 

1045 Full path and name of the file to write. 

1046 data: 1-D or 2-D array of floats 

1047 Array with the data (first index time, second index channel, 

1048 values within -1.0 and 1.0). 

1049 rate: float 

1050 Sampling rate of the data in Hertz. 

1051 metadata: None or nested dict 

1052 Metadata as key-value pairs. Values can be strings, integers, 

1053 or dictionaries. 

1054 locs: None or 1-D or 2-D array of ints 

1055 Marker positions (first column) and spans (optional second column) 

1056 for each marker (rows). 

1057 labels: None or 2-D array of string objects 

1058 Labels (first column) and texts (optional second column) 

1059 for each marker (rows). 

1060 format: str or None 

1061 File format. If None deduce file format from filepath. 

1062 See `available_formats()` for possible values. 

1063 encoding: str or None 

1064 Encoding of the data. See `available_encodings()` for possible values. 

1065 If None or empty string use 'PCM_16'. 

1066 marker_hint: str 

1067 - 'cue': store markers in cue and and adtl chunks. 

1068 - 'lbl': store markers in avisoft lbl chunk. 

1069 verbose: int 

1070 If >0 show detailed error/warning messages. 

1071 

1072 Raises 

1073 ------ 

1074 ValueError 

1075 `filepath` is empty string. 

1076 IOError 

1077 Writing of the data failed. 

1078 

1079 Examples 

1080 -------- 

1081 ``` 

1082 import numpy as np 

1083 from audioio import write_audio 

1084  

1085 rate = 28000.0 

1086 freq = 800.0 

1087 time = np.arange(0.0, 1.0, 1/rate) # one second 

1088 data = np.sin(2.0*np.p*freq*time) # 800Hz sine wave 

1089 md = dict(Artist='underscore_') # metadata 

1090 

1091 write_audio('audio/file.wav', data, rate, md) 

1092 ``` 

1093 """ 

1094 # write audio with metadata and markers: 

1095 if not format: 

1096 format = format_from_extension(filepath) 

1097 if format == 'WAV' and (metadata is not None or locs is not None): 

1098 try: 

1099 audioio_write_wave(filepath, data, rate, metadata, 

1100 locs, labels, encoding, marker_hint) 

1101 return 

1102 except ValueError: 

1103 pass 

1104 # write audio file by trying available modules: 

1105 errors = [f'failed to write data to file "{filepath}":'] 

1106 for lib, write_file in audio_writer_funcs: 

1107 if not audio_modules[lib]: 

1108 continue 

1109 try: 

1110 write_file(filepath, data, rate, metadata, locs, 

1111 labels, format, encoding, marker_hint) 

1112 success = True 

1113 if verbose > 0: 

1114 print('wrote data to file "%s" using %s module' % 

1115 (filepath, lib)) 

1116 if verbose > 1: 

1117 print(' sampling rate: %g Hz' % rate) 

1118 print(' channels : %d' % (data.shape[1] if len(data.shape) > 1 else 1)) 

1119 print(' frames : %d' % len(data)) 

1120 return 

1121 except Exception as e: 

1122 errors.append(f' {lib} failed: {str(e)}') 

1123 raise IOError('\n'.join(errors)) 

1124 

1125 

1126def demo(file_path, channels=2, encoding=''): 

1127 """Demo of the audiowriter functions. 

1128 

1129 Parameters 

1130 ---------- 

1131 file_path: str 

1132 File path of an audio file. 

1133 encoding: str 

1134 Encoding to be used. 

1135 """ 

1136 print('generate data ...') 

1137 rate = 44100.0 

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

1139 data = np.zeros((len(t), channels)) 

1140 for c in range(channels): 

1141 data[:,c] = (1 - c*0.1)*np.sin(2.0*np.pi*(440.0+c*110.0)*t) 

1142 

1143 print("write_audio(%s) ..." % file_path) 

1144 write_audio(file_path, data, rate, encoding=encoding, verbose=2) 

1145 

1146 print('done.') 

1147 

1148 

1149def main(*args): 

1150 """Call demo with command line arguments. 

1151 

1152 Parameters 

1153 ---------- 

1154 args: list of str 

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

1156 """ 

1157 help = False 

1158 file_path = None 

1159 encoding = '' 

1160 mod = False 

1161 nchan = False 

1162 channels = 2 

1163 for arg in args: 

1164 if mod: 

1165 select_module(arg) 

1166 mod = False 

1167 elif nchan: 

1168 channels = int(arg) 

1169 nchan = False 

1170 elif arg == '-h' or arg == '--help': 

1171 help = True 

1172 break 

1173 elif arg == '-m': 

1174 mod = True 

1175 elif arg == '-n': 

1176 nchan = True 

1177 elif file_path is None: 

1178 file_path = arg 

1179 else: 

1180 encoding = arg 

1181 break 

1182 if file_path is None: 

1183 file_path = 'test.wav' 

1184 

1185 if help: 

1186 print('') 

1187 print('Usage:') 

1188 print(' python -m src.audioio.audiowriter [-m module] [-n channels] [<filename>] [<encoding>]') 

1189 return 

1190 

1191 demo(file_path, channels=channels, encoding=encoding) 

1192 

1193 

1194if __name__ == "__main__": 

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