Coverage for src/audioio/audiometadata.py: 92%

612 statements  

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

1"""Working with metadata. 

2 

3To interface the various ways metadata are stored in audio files, the 

4`audioio` package uses nested dictionaries. The keys are always 

5strings. Values are strings, integers, floats, datetimes, or other 

6types. Value strings can also be numbers followed by a unit, 

7e.g. "4.2mV". For defining subsections of key-value pairs, values can 

8be dictionaries. The dictionaries can be nested to arbitrary depth. 

9 

10```py 

11>>> from audioio import print_metadata 

12>>> md = dict(Recording=dict(Experimenter='John Doe', 

13 DateTimeOriginal='2023-10-01T14:10:02', 

14 Count=42), 

15 Hardware=dict(Amplifier='Teensy_Amp 4.1', 

16 Highpass='10Hz', 

17 Gain='120mV')) 

18>>> print_metadata(md) 

19``` 

20results in 

21```txt 

22Recording: 

23 Experimenter : John Doe 

24 DateTimeOriginal: 2023-10-01T14:10:02 

25 Count : 42 

26Hardware: 

27 Amplifier: Teensy_Amp 4.1 

28 Highpass : 10Hz 

29 Gain : 120mV 

30``` 

31 

32Often, audio files have very specific ways to store metadata. You can 

33enforce using these by putting them into a dictionary that is added to 

34the metadata with a key having the name of the metadata type you want, 

35e.g. the "INFO", "BEXT", "iXML", and "GUAN" chunks of RIFF/WAVE files. 

36 

37## Functions 

38 

39The `audiometadata` module provides functions for handling and 

40manipulating these nested dictionaries. Many functions take keys as 

41arguments for finding or setting specific key-value pairs. These keys 

42can be the key of a specific item of a (sub-) dictionary, no matter on 

43which level of the metadata hierarchy it is. For example, simply 

44searching for "Highpass" retrieves the corrseponding value "10Hz", 

45although "Highpass" is contained in the sub-dictionary (or "section") 

46with key "Hardware". The same item can also be specified together with 

47its parent keys: "Hardware.Highpass". Parent keys (or section keys) 

48are by default separated by '.', but all functions have a `sep` 

49key-word that specifies the string separating section names in 

50keys. Key matching is case insensitive. 

51 

52Since the same items are named by many different keys in the different 

53types of metadata data models, the functions also take lists of keys 

54as arguments. 

55 

56Do not forget that you can easily manipulate the metadata by means of 

57the standard functions of dictionaries. 

58 

59If you need to make a copy of the metadata use `deepcopy`: 

60``` 

61from copy import deepcopy 

62md_orig = deepcopy(md) 

63``` 

64 

65### Output 

66 

67Write and load nested dictionaries as texts: 

68 

69- `write_metadata_text()`: write meta data into a text/yaml file. 

70- `print_metadata()`: write meta data to standard output. 

71- `load_metadata()`: load meta data from a text/yaml file. 

72 

73### Flatten 

74 

75Conversion between nested and flat dictionaries: 

76 

77- `flatten_metadata()`: flatten hierachical metadata to a single dictionary. 

78- `unflatten_metadata()`: unflatten a previously flattened metadata dictionary. 

79 

80### Parse numbers with units 

81 

82- `parse_number()`: parse string with number and unit. 

83- `change_unit()`: scale numerical value to a new unit. 

84 

85### Find and get values 

86 

87Find keys and get their values parsed and converted to various types: 

88 

89- `find_key()`: find dictionary in metadata hierarchy containing the specified key. 

90- `get_number_unit()`: find a key in metadata and return its number and unit. 

91- `get_number()`: find a key in metadata and return its value in a given unit. 

92- `get_int()`: find a key in metadata and return its integer value. 

93- `get_bool()`: find a key in metadata and return its boolean value. 

94- `get_datetime()`: find keys in metadata and return a datetime. 

95- `get_str()`: find a key in metadata and return its string value. 

96 

97### Organize metadata 

98 

99Add and remove metadata: 

100 

101- `strlist_to_dict()`: convert list of key-value-pair strings to dictionary. 

102- `add_sections()`: add sections to metadata dictionary. 

103- `set_metadata()`: set values of existing metadata. 

104- `add_metadata()`: add or modify key-value pairs. 

105- `move_metadata()`: remove a key from metadata and add it to a dictionary. 

106- `remove_metadata()`: remove key-value pairs or sections from metadata. 

107- `cleanup_metadata()`: remove empty sections from metadata. 

108 

109### Special metadata fields 

110 

111Retrieve and set specific metadata: 

112 

113- `get_gain()`: get gain and unit from metadata. 

114- `update_gain()`: update gain setting in metadata. 

115- `set_starttime()`: set all start-of-recording times in metadata. 

116- `update_starttime()`: update start-of-recording times in metadata. 

117- `bext_history_str()`: assemble a string for the BEXT CodingHistory field. 

118- `add_history()`: add a string describing coding history to metadata. 

119- `add_unwrap()`: add unwrap infos to metadata. 

120 

121Lists of standard keys: 

122 

123- `default_starttime_keys`: keys of times of start of the recording. 

124- `default_timeref_keys`: keys of integer time references. 

125- `default_gain_keys`: keys of gain settings. 

126- `default_history_keys`: keys of strings describing coding history. 

127 

128 

129## Command line script 

130 

131The module can be run as a script from the command line to display the 

132metadata and markers contained in an audio file: 

133 

134```sh 

135> audiometadata logger.wav 

136``` 

137prints 

138```text 

139file: 

140 filepath : logger.wav 

141 samplingrate: 96000Hz 

142 channels : 16 

143 frames : 17280000 

144 duration : 180.000s 

145 

146metadata: 

147 INFO: 

148 Bits : 32 

149 Pins : 1-CH2R,1-CH2L,1-CH3R,1-CH3L,2-CH2R,2-CH2L,2-CH3R,2-CH3L,3-CH2R,3-CH2L,3-CH3R,3-CH3L,4-CH2R,4-CH2L,4-CH3R,4-CH3L 

150 Gain : 165.00mV 

151 uCBoard : Teensy 4.1 

152 MACAdress : 04:e9:e5:15:3e:95 

153 DateTimeOriginal: 2023-10-01T14:10:02 

154 Software : TeeGrid R4-senors-logger v1.0 

155``` 

156 

157 

158Alternatively, the script can be run from within the audioio source tree as: 

159``` 

160python -m src.audioio.audiometadata audiofile.wav 

161``` 

162 

163Running 

164```sh 

165audiometadata --help 

166``` 

167prints 

168```text 

169usage: audiometadata [-h] [--version] [-f] [-m] [-c] [-t] files [files ...] 

170 

171Convert audio file formats. 

172 

173positional arguments: 

174 files audio file 

175 

176options: 

177 -h, --help show this help message and exit 

178 --version show program's version number and exit 

179 -f list file format only 

180 -m list metadata only 

181 -c list cues/markers only 

182 -t list tags of all riff/wave chunks contained in the file 

183 

184version 2.0.0 by Benda-Lab (2020-2024) 

185``` 

186 

187""" 

188 

189import os 

190import sys 

191import glob 

192import argparse 

193import numpy as np 

194import datetime as dt 

195 

196from pathlib import Path 

197from .version import __version__, __year__ 

198 

199 

200def write_metadata_text(fh, meta, prefix='', indent=4, replace=None): 

201 """Write meta data into a text/yaml file or stream. 

202 

203 With the default parameters, the output is a valid yaml file. 

204 

205 Parameters 

206 ---------- 

207 fh: filename or stream 

208 If not a stream, the file with name `fh` is opened. 

209 Otherwise `fh` is used as a stream for writing. 

210 meta: nested dict 

211 Key-value pairs of metadata to be written into the file. 

212 prefix: str 

213 This string is written at the beginning of each line. 

214 indent: int 

215 Number of characters used for indentation of sections. 

216 replace: char or None 

217 If specified, replace special characters by this character. 

218 

219 Examples 

220 -------- 

221 ``` 

222 >>> from audioio import write_metadata_text 

223 >>> md = dict(aaaa=2, bbbb=dict(ccc=3, ddd=4, eee=dict(hh=5))) 

224 >>> write_metadata_text('info.txt', md) 

225 ``` 

226 """ 

227 

228 def write_dict(df, md, level, smap): 

229 w = 0 

230 for k in md: 

231 if not isinstance(md[k], dict) and w < len(k): 

232 w = len(k) 

233 for k in md: 

234 clevel = level*indent 

235 if isinstance(md[k], dict): 

236 df.write(f'{prefix}{"":>{clevel}}{k}:\n') 

237 write_dict(df, md[k], level+1, smap) 

238 else: 

239 value = md[k] 

240 if isinstance(value, (list, tuple)): 

241 value = ', '.join([f'{v}' for v in value]) 

242 else: 

243 value = f'{value}' 

244 value = value.replace('\r\n', r'\n') 

245 value = value.replace('\n', r'\n') 

246 if len(smap) > 0: 

247 value = value.translate(smap) 

248 df.write(f'{prefix}{"":>{clevel}}{k:<{w}}: {value}\n') 

249 

250 if not meta: 

251 return 

252 if hasattr(fh, 'write'): 

253 own_file = False 

254 else: 

255 own_file = True 

256 fh = open(fh, 'w') 

257 smap = {} 

258 if replace: 

259 smap = str.maketrans('\r\n\t\x00', ''.join([replace]*4)) 

260 write_dict(fh, meta, 0, smap) 

261 if own_file: 

262 fh.close() 

263 

264 

265def print_metadata(meta, prefix='', indent=4, replace=None): 

266 """Write meta data to standard output. 

267 

268 Parameters 

269 ---------- 

270 meta: nested dict 

271 Key-value pairs of metadata to be written into the file. 

272 prefix: str 

273 This string is written at the beginning of each line. 

274 indent: int 

275 Number of characters used for indentation of sections. 

276 replace: char or None 

277 If specified, replace special characters by this character. 

278 

279 Examples 

280 -------- 

281 ``` 

282 >>> from audioio import print_metadata 

283 >>> md = dict(aaaa=2, bbbb=dict(ccc=3, ddd=4, eee=dict(hh=5)), iiii=dict(jjj=6)) 

284 >>> print_metadata(md) 

285 aaaa: 2 

286 bbbb: 

287 ccc: 3 

288 ddd: 4 

289 eee: 

290 hh: 5 

291 iiii: 

292 jjj: 6 

293 ``` 

294 """ 

295 write_metadata_text(sys.stdout, meta, prefix, indent, replace) 

296 

297 

298def load_metadata(fh, prefix=''): 

299 """Load meta data from a text/yaml file or stream. 

300 

301 Parameters 

302 ---------- 

303 fh: filename or stream 

304 If not a stream, the file with name `fh` is opened. 

305 Otherwise `fh` is used as a stream from which the metadata are read in. 

306 prefix: str 

307 This string is removed from the beginning of each line before parsing. 

308 

309 Returns 

310 ------- 

311 meta: nested dict 

312 Key-value pairs of metadata loaded from the file. 

313 

314 Examples 

315 -------- 

316 ``` 

317 >>> from audioio import load_metadata, write_metadata_text, print_metadata 

318 >>> md = dict(aaaa=2, bbbb=dict(ccc=3, ddd=4, eee=dict(hh=5))) 

319 >>> write_metadata_text('info.txt', md) 

320 >>> md = load_metadata('info.txt') 

321 >>> print_metadata(md) 

322 aaaa: 2 

323 bbbb: 

324 ccc: 3 

325 ddd: 4 

326 eee: 

327 hh: 5 

328 iiii: 

329 jjj: 6 

330 ``` 

331 """ 

332 # read metadata from file: 

333 own_file = False 

334 if not hasattr(fh, 'readline'): 

335 fh = open(Path(fh), 'r') 

336 own_file = True 

337 lines = [] 

338 for line in fh: 

339 if prefix: 

340 line = line.lstrip(prefix) 

341 line = line.rstrip() 

342 if len(line) > 0: 

343 lines.append(line) 

344 if own_file: 

345 fh.close() 

346 # parse: 

347 data = {} 

348 cdatas = [data] 

349 sections = [''] 

350 ident_offs = None 

351 ident = None 

352 for line in lines: 

353 words = line.split(':') 

354 value = ':'.join(words[1:]).strip() if len(words) > 1 else '' 

355 if len(words) == 0: 

356 continue 

357 key = words[0] 

358 # get section level: 

359 level = 0 

360 if len(value) == 0: 

361 nident = len(key) - len(key.lstrip()) 

362 if ident_offs is None: 

363 ident_offs = nident 

364 elif ident is None: 

365 if nident > ident_offs: 

366 ident = nident - ident_offs 

367 level = 1 

368 else: 

369 level = (nident - ident_offs)//ident 

370 # close sections: 

371 while len(cdatas) > level + 1: 

372 cdatas[-1][sections.pop()] = cdatas.pop() 

373 # key: 

374 key = key.strip().strip('"') 

375 if len(value) == 0: 

376 # new sub-section: 

377 cdatas.append({}) 

378 sections.append(key) 

379 else: 

380 # key-value pair: 

381 value = value.strip('"') 

382 if len(value) > 0 and value[0] == '[' and value[-1] == ']': 

383 value = [v.strip() for v in value.lstrip('[').rstrip(']').split(',')] 

384 cdatas[-1][key] = value 

385 while len(cdatas) > 1 and len(sections) > 0: 

386 cdatas[-1][sections.pop()] = cdatas.pop() 

387 return data 

388 

389 

390def flatten_metadata(md, keep_sections=False, sep='.'): 

391 """Flatten hierarchical metadata to a single dictionary. 

392 

393 Parameters 

394 ---------- 

395 md: nested dict 

396 Metadata as returned by `metadata()`. 

397 keep_sections: bool 

398 If `True`, then prefix keys with section names, separated by `sep`. 

399 sep: str 

400 String for separating section names. 

401 

402 Returns 

403 ------- 

404 d: dict 

405 Non-nested dict containing all key-value pairs of `md`. 

406 

407 Examples 

408 -------- 

409 ``` 

410 >>> from audioio import print_metadata, flatten_metadata 

411 >>> md = dict(aaaa=2, bbbb=dict(ccc=3, ddd=4, eee=dict(hh=5)), iiii=dict(jjj=6)) 

412 >>> print_metadata(md) 

413 aaaa: 2 

414 bbbb: 

415 ccc: 3 

416 ddd: 4 

417 eee: 

418 hh: 5 

419 iiii: 

420 jjj: 6 

421  

422 >>> fmd = flatten_metadata(md, keep_sections=True) 

423 >>> print_metadata(fmd) 

424 aaaa : 2 

425 bbbb.ccc : 3 

426 bbbb.ddd : 4 

427 bbbb.eee.hh: 5 

428 iiii.jjj : 6 

429 ``` 

430 """ 

431 def flatten(cd, section): 

432 df = {} 

433 for k in cd: 

434 if isinstance(cd[k], dict): 

435 df.update(flatten(cd[k], section + k + sep)) 

436 else: 

437 if keep_sections: 

438 df[section+k] = cd[k] 

439 else: 

440 df[k] = cd[k] 

441 return df 

442 

443 return flatten(md, '') 

444 

445 

446def unflatten_metadata(md, sep='.'): 

447 """Unflatten a previously flattened metadata dictionary. 

448 

449 Parameters 

450 ---------- 

451 md: dict 

452 Flat dictionary with key-value pairs as obtained from 

453 `flatten_metadata()` with `keep_sections=True`. 

454 sep: str 

455 String that separates section names. 

456 

457 Returns 

458 ------- 

459 d: nested dict 

460 Hierarchical dictionary with sub-dictionaries and key-value pairs. 

461 

462 Examples 

463 -------- 

464 ``` 

465 >>> from audioio import print_metadata, unflatten_metadata 

466 >>> fmd = {'aaaa': 2, 'bbbb.ccc': 3, 'bbbb.ddd': 4, 'bbbb.eee.hh': 5, 'iiii.jjj': 6} 

467 >>> print_metadata(fmd) 

468 aaaa : 2 

469 bbbb.ccc : 3 

470 bbbb.ddd : 4 

471 bbbb.eee.hh: 5 

472 iiii.jjj : 6 

473  

474 >>> md = unflatten_metadata(fmd) 

475 >>> print_metadata(md) 

476 aaaa: 2 

477 bbbb: 

478 ccc: 3 

479 ddd: 4 

480 eee: 

481 hh: 5 

482 iiii: 

483 jjj: 6 

484 ``` 

485 """ 

486 umd = {} # unflattened metadata 

487 cmd = [umd] # current metadata dicts for each level of the hierarchy 

488 csk = [] # current section keys 

489 for k in md: 

490 ks = k.split(sep) 

491 # go up the hierarchy: 

492 for i in range(len(csk) - len(ks)): 

493 csk.pop() 

494 cmd.pop() 

495 for kss in reversed(ks[:len(csk)]): 

496 if kss == csk[-1]: 

497 break 

498 csk.pop() 

499 cmd.pop() 

500 # add new sections: 

501 for kss in ks[len(csk):-1]: 

502 csk.append(kss) 

503 cmd[-1][kss] = {} 

504 cmd.append(cmd[-1][kss]) 

505 # add key-value pair: 

506 cmd[-1][ks[-1]] = md[k] 

507 return umd 

508 

509 

510def parse_number(s): 

511 """Parse string with number and unit. 

512 

513 Parameters 

514 ---------- 

515 s: str, float, or int 

516 String to be parsed. The initial part of the string is 

517 expected to be a number, the part following the number is 

518 interpreted as the unit. If float or int, then return this 

519 as the value with empty unit. 

520 

521 Returns 

522 ------- 

523 v: None, int, or float 

524 Value of the string as float. Without decimal point, an int is returned. 

525 If the string does not contain a number, None is returned. 

526 u: str 

527 Unit that follows the initial number. 

528 n: int 

529 Number of digits behind the decimal point. 

530 

531 Examples 

532 -------- 

533 

534 ``` 

535 >>> from audioio import parse_number 

536 

537 # integer: 

538 >>> parse_number('42') 

539 (42, '', 0) 

540 

541 # integer with unit: 

542 >>> parse_number('42ms') 

543 (42, 'ms', 0) 

544 

545 # float with unit: 

546 >>> parse_number('42.ms') 

547 (42.0, 'ms', 0) 

548 

549 # float with unit: 

550 >>> parse_number('42.3ms') 

551 (42.3, 'ms', 1) 

552 

553 # float with space and unit: 

554 >>> parse_number('423.17 Hz') 

555 (423.17, 'Hz', 2) 

556 ``` 

557 

558 """ 

559 if not isinstance(s, str): 

560 if isinstance(s, int): 

561 return s, '', 0 

562 if isinstance(s, float): 

563 return s, '', 5 

564 else: 

565 return None, '', 0 

566 n = len(s) 

567 ip = n 

568 have_point = False 

569 for i in range(len(s)): 

570 if s[i] == '.': 

571 if have_point: 

572 n = i 

573 break 

574 have_point = True 

575 ip = i + 1 

576 if not s[i] in '0123456789.+-': 

577 n = i 

578 break 

579 if n == 0: 

580 return None, s, 0 

581 v = float(s[:n]) if have_point else int(s[:n]) 

582 u = s[n:].strip() 

583 nd = n - ip if n >= ip else 0 

584 return v, u, nd 

585 

586 

587unit_prefixes = {'Deka': 1e1, 'deka': 1e1, 'Hekto': 1e2, 'hekto': 1e2, 

588 'kilo': 1e3, 'Kilo': 1e3, 'Mega': 1e6, 'mega': 1e6, 

589 'Giga': 1e9, 'giga': 1e9, 'Tera': 1e12, 'tera': 1e12, 

590 'Peta': 1e15, 'peta': 1e15, 'Exa': 1e18, 'exa': 1e18, 

591 'Dezi': 1e-1, 'dezi': 1e-1, 'Zenti': 1e-2, 'centi': 1e-2, 

592 'Milli': 1e-3, 'milli': 1e-3, 'Micro': 1e-6, 'micro': 1e-6, 

593 'Nano': 1e-9, 'nano': 1e-9, 'Piko': 1e-12, 'piko': 1e-12, 

594 'Femto': 1e-15, 'femto': 1e-15, 'Atto': 1e-18, 'atto': 1e-18, 

595 'da': 1e1, 'h': 1e2, 'K': 1e3, 'k': 1e3, 'M': 1e6, 

596 'G': 1e9, 'T': 1e12, 'P': 1e15, 'E': 1e18, 

597 'd': 1e-1, 'c': 1e-2, 'mu': 1e-6, 'u': 1e-6, 'm': 1e-3, 

598 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18} 

599""" SI prefixes for units with corresponding factors. """ 

600 

601 

602def change_unit(val, old_unit, new_unit): 

603 """Scale numerical value to a new unit. 

604 

605 Adapted from https://github.com/relacs/relacs/blob/1facade622a80e9f51dbf8e6f8171ac74c27f100/options/src/parameter.cc#L1647-L1703 

606 

607 Parameters 

608 ---------- 

609 val: float 

610 Value given in `old_unit`. 

611 old_unit: str 

612 Unit of `val`. 

613 new_unit: str 

614 Requested unit of return value. 

615 

616 Returns 

617 ------- 

618 new_val: float 

619 The input value `val` scaled to `new_unit`. 

620 

621 Examples 

622 -------- 

623 

624 ``` 

625 >>> from audioio import change_unit 

626 >>> change_unit(5, 'mm', 'cm') 

627 0.5 

628 

629 >>> change_unit(5, '', 'cm') 

630 5.0 

631 

632 >>> change_unit(5, 'mm', '') 

633 5.0 

634 

635 >>> change_unit(5, 'cm', 'mm') 

636 50.0 

637 

638 >>> change_unit(4, 'kg', 'g') 

639 4000.0 

640 

641 >>> change_unit(12, '%', '') 

642 0.12 

643 

644 >>> change_unit(1.24, '', '%') 

645 124.0 

646 

647 >>> change_unit(2.5, 'min', 's') 

648 150.0 

649 

650 >>> change_unit(3600, 's', 'h') 

651 1.0 

652 

653 ``` 

654 

655 """ 

656 # missing unit? 

657 if not old_unit and not new_unit: 

658 return val 

659 if not old_unit and new_unit != '%': 

660 return val 

661 if not new_unit and old_unit != '%': 

662 return val 

663 

664 # special units that directly translate into factors: 

665 unit_factors = {'%': 0.01, 'hour': 60.0*60.0, 'h': 60.0*60.0, 'min': 60.0} 

666 

667 # parse old unit: 

668 f1 = 1.0 

669 if old_unit in unit_factors: 

670 f1 = unit_factors[old_unit] 

671 else: 

672 for k in unit_prefixes: 

673 if len(old_unit) > len(k) and old_unit[:len(k)] == k: 

674 f1 = unit_prefixes[k]; 

675 

676 # parse new unit: 

677 f2 = 1.0 

678 if new_unit in unit_factors: 

679 f2 = unit_factors[new_unit] 

680 else: 

681 for k in unit_prefixes: 

682 if len(new_unit) > len(k) and new_unit[:len(k)] == k: 

683 f2 = unit_prefixes[k]; 

684 

685 return val*f1/f2 

686 

687 

688def find_key(metadata, key, sep='.'): 

689 """Find dictionary in metadata hierarchy containing the specified key. 

690 

691 Parameters 

692 ---------- 

693 metadata: nested dict 

694 Metadata. 

695 key: str 

696 Key to be searched for (case insensitive). 

697 May contain section names separated by `sep`, i.e. 

698 "aaa.bbb.ccc" searches "ccc" (can be key-value pair or section) 

699 in section "bbb" that needs to be a subsection of section "aaa". 

700 sep: str 

701 String that separates section names in `key`. 

702 

703 Returns 

704 ------- 

705 md: dict 

706 The innermost dictionary matching some sections of the search key. 

707 If `key` is not at all contained in the metadata, 

708 the top-level dictionary is returned. 

709 key: str 

710 The part of the search key that was not found in `md`, or the 

711 the final part of the search key, found in `md`. 

712 

713 Examples 

714 -------- 

715 

716 Independent of whether found or not found, you can assign to the 

717 returned dictionary with the returned key. 

718 

719 ``` 

720 >>> from audioio import print_metadata, find_key 

721 >>> md = dict(aaaa=2, bbbb=dict(ccc=3, ddd=4, eee=dict(ff=5)), gggg=dict(hhh=6)) 

722 >>> print_metadata(md) 

723 aaaa: 2 

724 bbbb: 

725 ccc: 3 

726 ddd: 4 

727 eee: 

728 ff: 5 

729 gggg: 

730 hhh: 6 

731 

732 >>> m, k = find_key(md, 'bbbb.ddd') 

733 >>> m[k] = 10 

734 >>> print_metadata(md) 

735 aaaa: 2 

736 bbbb: 

737 ccc: 3 

738 ddd: 10 

739 ... 

740 

741 >>> m, k = find_key(md, 'hhh') 

742 >>> m[k] = 12 

743 >>> print_metadata(md) 

744 ... 

745 gggg: 

746 hhh: 12 

747 

748 >>> m, k = find_key(md, 'bbbb.eee.xx') 

749 >>> m[k] = 42 

750 >>> print_metadata(md) 

751 ... 

752 eee: 

753 ff: 5 

754 xx: 42 

755 ... 

756 ``` 

757 

758 When searching for sections, the one conaining the searched section 

759 is returned: 

760 ```py 

761 >>> m, k = find_key(md, 'eee') 

762 >>> m[k]['yy'] = 46 

763 >>> print_metadata(md) 

764 ... 

765 eee: 

766 ff: 5 

767 xx: 42 

768 yy: 46 

769 ... 

770 ``` 

771 

772 """ 

773 def find_keys(metadata, keys): 

774 key = keys[0].strip().upper() 

775 for k in metadata: 

776 if k.upper() == key: 

777 if len(keys) == 1: 

778 # found key: 

779 return True, metadata, k 

780 elif isinstance(metadata[k], dict): 

781 # keep searching within the next section: 

782 return find_keys(metadata[k], keys[1:]) 

783 # search in subsections: 

784 for k in metadata: 

785 if isinstance(metadata[k], dict): 

786 found, mm, kk = find_keys(metadata[k], keys) 

787 if found: 

788 return True, mm, kk 

789 # nothing found: 

790 return False, metadata, sep.join(keys) 

791 

792 if metadata is None: 

793 return {}, None 

794 ks = key.strip().split(sep) 

795 found, mm, kk = find_keys(metadata, ks) 

796 return mm, kk 

797 

798 

799def get_number_unit(metadata, keys, sep='.', default=None, 

800 default_unit='', remove=False): 

801 """Find a key in metadata and return its number and unit. 

802 

803 Parameters 

804 ---------- 

805 metadata: nested dict 

806 Metadata. 

807 keys: str or list of str 

808 Keys in the metadata to be searched for (case insensitive). 

809 Value of the first key found is returned. 

810 May contain section names separated by `sep`.  

811 See `audiometadata.find_key()` for details. 

812 sep: str 

813 String that separates section names in `key`. 

814 default: None, int, or float 

815 Returned value if `key` is not found or the value does 

816 not contain a number. 

817 default_unit: str 

818 Returned unit if `key` is not found or the key's value does 

819 not have a unit. 

820 remove: bool 

821 If `True`, remove the found key from `metadata`. 

822 

823 Returns 

824 ------- 

825 v: None, int, or float 

826 Value referenced by `key` as float. 

827 Without decimal point, an int is returned. 

828 If none of the `keys` was found or 

829 the key`s value does not contain a number, 

830 then `default` is returned. 

831 u: str 

832 Corresponding unit. 

833 

834 Examples 

835 -------- 

836 

837 ``` 

838 >>> from audioio import get_number_unit 

839 >>> md = dict(aaaa='42', bbbb='42.3ms') 

840 

841 # integer: 

842 >>> get_number_unit(md, 'aaaa') 

843 (42, '') 

844 

845 # float with unit: 

846 >>> get_number_unit(md, 'bbbb') 

847 (42.3, 'ms') 

848 

849 # two keys: 

850 >>> get_number_unit(md, ['cccc', 'bbbb']) 

851 (42.3, 'ms') 

852 

853 # not found: 

854 >>> get_number_unit(md, 'cccc') 

855 (None, '') 

856 

857 # not found with default value: 

858 >>> get_number_unit(md, 'cccc', default=1.0, default_unit='a.u.') 

859 (1.0, 'a.u.') 

860 ``` 

861 

862 """ 

863 if not metadata: 

864 return default, default_unit 

865 if not isinstance(keys, (list, tuple, np.ndarray)): 

866 keys = (keys,) 

867 value = default 

868 unit = default_unit 

869 for key in keys: 

870 m, k = find_key(metadata, key, sep) 

871 if k in m: 

872 v, u, _ = parse_number(m[k]) 

873 if v is not None: 

874 if not u: 

875 u = default_unit 

876 if remove: 

877 del m[k] 

878 return v, u 

879 elif u and unit == default_unit: 

880 unit = u 

881 return value, unit 

882 

883 

884def get_number(metadata, unit, keys, sep='.', default=None, remove=False): 

885 """Find a key in metadata and return its value in a given unit. 

886 

887 Parameters 

888 ---------- 

889 metadata: nested dict 

890 Metadata. 

891 unit: str 

892 Unit in which to return numerical value referenced by one of the `keys`. 

893 keys: str or list of str 

894 Keys in the metadata to be searched for (case insensitive). 

895 Value of the first key found is returned. 

896 May contain section names separated by `sep`.  

897 See `audiometadata.find_key()` for details. 

898 sep: str 

899 String that separates section names in `key`. 

900 default: None, int, or float 

901 Returned value if `key` is not found or the value does 

902 not contain a number. 

903 remove: bool 

904 If `True`, remove the found key from `metadata`. 

905 

906 Returns 

907 ------- 

908 v: None or float 

909 Value referenced by `key` as float scaled to `unit`. 

910 If none of the `keys` was found or 

911 the key`s value does not contain a number, 

912 then `default` is returned. 

913 

914 Examples 

915 -------- 

916 

917 ``` 

918 >>> from audioio import get_number 

919 >>> md = dict(aaaa='42', bbbb='42.3ms') 

920 

921 # milliseconds to seconds: 

922 >>> get_number(md, 's', 'bbbb') 

923 0.0423 

924 

925 # milliseconds to microseconds: 

926 >>> get_number(md, 'us', 'bbbb') 

927 42300.0 

928 

929 # value without unit is not scaled: 

930 >>> get_number(md, 'Hz', 'aaaa') 

931 42 

932 

933 # two keys: 

934 >>> get_number(md, 's', ['cccc', 'bbbb']) 

935 0.0423 

936 

937 # not found: 

938 >>> get_number(md, 's', 'cccc') 

939 None 

940 

941 # not found with default value: 

942 >>> get_number(md, 's', 'cccc', default=1.0) 

943 1.0 

944 ``` 

945 

946 """ 

947 v, u = get_number_unit(metadata, keys, sep, None, unit, remove) 

948 if v is None: 

949 return default 

950 else: 

951 return change_unit(v, u, unit) 

952 

953 

954def get_int(metadata, keys, sep='.', default=None, remove=False): 

955 """Find a key in metadata and return its integer value. 

956 

957 Parameters 

958 ---------- 

959 metadata: nested dict 

960 Metadata. 

961 keys: str or list of str 

962 Keys in the metadata to be searched for (case insensitive). 

963 Value of the first key found is returned. 

964 May contain section names separated by `sep`.  

965 See `audiometadata.find_key()` for details. 

966 sep: str 

967 String that separates section names in `key`. 

968 default: None or int 

969 Return value if `key` is not found or the value does 

970 not contain an integer. 

971 remove: bool 

972 If `True`, remove the found key from `metadata`. 

973 

974 Returns 

975 ------- 

976 v: None or int 

977 Value referenced by `key` as integer. 

978 If none of the `keys` was found, 

979 the key's value does not contain a number or represents 

980 a floating point value, then `default` is returned. 

981 

982 Examples 

983 -------- 

984 

985 ``` 

986 >>> from audioio import get_int 

987 >>> md = dict(aaaa='42', bbbb='42.3ms') 

988 

989 # integer: 

990 >>> get_int(md, 'aaaa') 

991 42 

992 

993 # two keys: 

994 >>> get_int(md, ['cccc', 'aaaa']) 

995 42 

996 

997 # float: 

998 >>> get_int(md, 'bbbb') 

999 None 

1000 

1001 # not found: 

1002 >>> get_int(md, 'cccc') 

1003 None 

1004 

1005 # not found with default value: 

1006 >>> get_int(md, 'cccc', default=0) 

1007 0 

1008 ``` 

1009 

1010 """ 

1011 if not metadata: 

1012 return default 

1013 if not isinstance(keys, (list, tuple, np.ndarray)): 

1014 keys = (keys,) 

1015 for key in keys: 

1016 m, k = find_key(metadata, key, sep) 

1017 if k in m: 

1018 v, _, n = parse_number(m[k]) 

1019 if v is not None and n == 0: 

1020 if remove: 

1021 del m[k] 

1022 return int(v) 

1023 return default 

1024 

1025 

1026def get_bool(metadata, keys, sep='.', default=None, remove=False): 

1027 """Find a key in metadata and return its boolean value. 

1028 

1029 Parameters 

1030 ---------- 

1031 metadata: nested dict 

1032 Metadata. 

1033 keys: str or list of str 

1034 Keys in the metadata to be searched for (case insensitive). 

1035 Value of the first key found is returned. 

1036 May contain section names separated by `sep`.  

1037 See `audiometadata.find_key()` for details. 

1038 sep: str 

1039 String that separates section names in `key`. 

1040 default: None or bool 

1041 Return value if `key` is not found or the value does 

1042 not specify a boolean value. 

1043 remove: bool 

1044 If `True`, remove the found key from `metadata`. 

1045 

1046 Returns 

1047 ------- 

1048 v: None or bool 

1049 Value referenced by `key` as boolean. 

1050 True if 'true', 'yes' (case insensitive) or any number larger than zero. 

1051 False if 'false', 'no' (case insensitive) or any number equal to zero. 

1052 If none of the `keys` was found or 

1053 the key's value does specify a boolean value, 

1054 then `default` is returned. 

1055 

1056 Examples 

1057 -------- 

1058 

1059 ``` 

1060 >>> from audioio import get_bool 

1061 >>> md = dict(aaaa='TruE', bbbb='No', cccc=0, dddd=1, eeee=True, ffff='ui') 

1062 

1063 # case insensitive: 

1064 >>> get_bool(md, 'aaaa') 

1065 True 

1066 

1067 >>> get_bool(md, 'bbbb') 

1068 False 

1069 

1070 >>> get_bool(md, 'cccc') 

1071 False 

1072 

1073 >>> get_bool(md, 'dddd') 

1074 True 

1075 

1076 >>> get_bool(md, 'eeee') 

1077 True 

1078 

1079 # not found: 

1080 >>> get_bool(md, 'ffff') 

1081 None 

1082 

1083 # two keys (string is preferred over number): 

1084 >>> get_bool(md, ['cccc', 'aaaa']) 

1085 True 

1086 

1087 # two keys (take first match): 

1088 >>> get_bool(md, ['cccc', 'ffff']) 

1089 False 

1090 

1091 # not found with default value: 

1092 >>> get_bool(md, 'ffff', default=False) 

1093 False 

1094 ``` 

1095 

1096 """ 

1097 if not metadata: 

1098 return default 

1099 if not isinstance(keys, (list, tuple, np.ndarray)): 

1100 keys = (keys,) 

1101 val = default 

1102 mv = None 

1103 kv = None 

1104 for key in keys: 

1105 m, k = find_key(metadata, key, sep) 

1106 if k in m and not isinstance(m[k], dict): 

1107 vs = m[k] 

1108 v, _, _ = parse_number(vs) 

1109 if v is not None: 

1110 val = abs(v) > 1e-8 

1111 mv = m 

1112 kv = k 

1113 elif isinstance(vs, str): 

1114 if vs.upper() in ['TRUE', 'T', 'YES', 'Y']: 

1115 if remove: 

1116 del m[k] 

1117 return True 

1118 if vs.upper() in ['FALSE', 'F', 'NO', 'N']: 

1119 if remove: 

1120 del m[k] 

1121 return False 

1122 if not mv is None and not kv is None and remove: 

1123 del mv[kv] 

1124 return val 

1125 

1126 

1127default_starttime_keys = [['DateTimeOriginal'], 

1128 ['OriginationDate', 'OriginationTime'], 

1129 ['Location_Time'], 

1130 ['Timestamp']] 

1131"""Default keys of times of start of the recording in metadata. 

1132Used by `get_datetime()` and `update_starttime()` functions. 

1133""" 

1134 

1135def get_datetime(metadata, keys=default_starttime_keys, 

1136 sep='.', default=None, remove=False): 

1137 """Find keys in metadata and return a datetime. 

1138 

1139 Parameters 

1140 ---------- 

1141 metadata: nested dict 

1142 Metadata. 

1143 keys: tuple of str or list of tuple of str 

1144 Datetimes can be stored in metadata as two separate key-value pairs, 

1145 one for the date and one for the time. Or by a single key-value pair 

1146 for a date-time value. This is why the keys need to be specified in 

1147 tuples with one or two keys. 

1148 The value of the first tuple of keys found is returned. 

1149 Keys may contain section names separated by `sep`.  

1150 See `audiometadata.find_key()` for details. 

1151 The default values for the `keys` find the start time of a recording. 

1152 You can modify the default keys via the `default_starttime_keys` list 

1153 of the `audiometadata` module. 

1154 sep: str 

1155 String that separates section names in `key`. 

1156 default: None or str 

1157 Return value if `key` is not found or the value does 

1158 not contain a string. 

1159 remove: bool 

1160 If `True`, remove the found key from `metadata`. 

1161 

1162 Returns 

1163 ------- 

1164 v: None or datetime 

1165 Datetime referenced by `keys`. 

1166 If none of the `keys` was found, then `default` is returned. 

1167 

1168 Examples 

1169 -------- 

1170 

1171 ``` 

1172 >>> from audioio import get_datetime 

1173 >>> import datetime as dt 

1174 >>> md = dict(date='2024-03-02', time='10:42:24', 

1175 datetime='2023-04-15T22:10:00') 

1176 

1177 # separate date and time: 

1178 >>> get_datetime(md, ('date', 'time')) 

1179 datetime.datetime(2024, 3, 2, 10, 42, 24) 

1180 

1181 # single datetime: 

1182 >>> get_datetime(md, ('datetime',)) 

1183 datetime.datetime(2023, 4, 15, 22, 10) 

1184 

1185 # two alternative key tuples: 

1186 >>> get_datetime(md, [('aaaa',), ('date', 'time')]) 

1187 datetime.datetime(2024, 3, 2, 10, 42, 24) 

1188 

1189 # not found: 

1190 >>> get_datetime(md, ('cccc',)) 

1191 None 

1192 

1193 # not found with default value: 

1194 >>> get_datetime(md, ('cccc', 'dddd'), 

1195 default=dt.datetime(2022, 2, 22, 22, 2, 12)) 

1196 datetime.datetime(2022, 2, 22, 22, 2, 12) 

1197 ``` 

1198 

1199 """ 

1200 if not metadata: 

1201 return default 

1202 if len(keys) > 0 and isinstance(keys[0], str): 

1203 keys = (keys,) 

1204 for keyp in keys: 

1205 if len(keyp) == 1: 

1206 m, k = find_key(metadata, keyp[0], sep) 

1207 if k in m: 

1208 v = m[k] 

1209 if isinstance(v, dt.datetime): 

1210 if remove: 

1211 del m[k] 

1212 return v 

1213 elif isinstance(v, str): 

1214 if remove: 

1215 del m[k] 

1216 return dt.datetime.fromisoformat(v) 

1217 else: 

1218 md, kd = find_key(metadata, keyp[0], sep) 

1219 if not kd in md: 

1220 continue 

1221 if isinstance(md[kd], dt.date): 

1222 date = md[kd] 

1223 elif isinstance(md[kd], str): 

1224 date = dt.date.fromisoformat(md[kd]) 

1225 else: 

1226 continue 

1227 mt, kt = find_key(metadata, keyp[1], sep) 

1228 if not kt in mt: 

1229 continue 

1230 if isinstance(mt[kt], dt.time): 

1231 time = mt[kt] 

1232 elif isinstance(mt[kt], str): 

1233 time = dt.time.fromisoformat(mt[kt]) 

1234 else: 

1235 continue 

1236 if remove: 

1237 del md[kd] 

1238 del mt[kt] 

1239 return dt.datetime.combine(date, time) 

1240 return default 

1241 

1242 

1243def get_str(metadata, keys, sep='.', default=None, remove=False): 

1244 """Find a key in metadata and return its string value. 

1245 

1246 Parameters 

1247 ---------- 

1248 metadata: nested dict 

1249 Metadata. 

1250 keys: str or list of str 

1251 Keys in the metadata to be searched for (case insensitive). 

1252 Value of the first key found is returned. 

1253 May contain section names separated by `sep`.  

1254 See `audiometadata.find_key()` for details. 

1255 sep: str 

1256 String that separates section names in `key`. 

1257 default: None or str 

1258 Return value if `key` is not found or the value does 

1259 not contain a string. 

1260 remove: bool 

1261 If `True`, remove the found key from `metadata`. 

1262 

1263 Returns 

1264 ------- 

1265 v: None or str 

1266 String value referenced by `key`. 

1267 If none of the `keys` was found, then `default` is returned. 

1268 

1269 Examples 

1270 -------- 

1271 

1272 ``` 

1273 >>> from audioio import get_str 

1274 >>> md = dict(aaaa=42, bbbb='hello') 

1275 

1276 # string: 

1277 >>> get_str(md, 'bbbb') 

1278 'hello' 

1279 

1280 # int as str: 

1281 >>> get_str(md, 'aaaa') 

1282 '42' 

1283 

1284 # two keys: 

1285 >>> get_str(md, ['cccc', 'bbbb']) 

1286 'hello' 

1287 

1288 # not found: 

1289 >>> get_str(md, 'cccc') 

1290 None 

1291 

1292 # not found with default value: 

1293 >>> get_str(md, 'cccc', default='-') 

1294 '-' 

1295 ``` 

1296 

1297 """ 

1298 if not metadata: 

1299 return default 

1300 if not isinstance(keys, (list, tuple, np.ndarray)): 

1301 keys = (keys,) 

1302 for key in keys: 

1303 m, k = find_key(metadata, key, sep) 

1304 if k in m and not isinstance(m[k], dict): 

1305 v = m[k] 

1306 if remove: 

1307 del m[k] 

1308 return str(v) 

1309 return default 

1310 

1311 

1312def add_sections(metadata, sections, value=False, sep='.'): 

1313 """Add sections to metadata dictionary. 

1314 

1315 Parameters 

1316 ---------- 

1317 metadata: nested dict 

1318 Metadata. 

1319 key: str 

1320 Names of sections to be added to `metadata`. 

1321 Section names separated by `sep`.  

1322 value: bool 

1323 If True, then the last element in `key` is a key for a value, 

1324 not a section. 

1325 sep: str 

1326 String that separates section names in `key`. 

1327 

1328 Returns 

1329 ------- 

1330 md: dict 

1331 Dictionary of the last added section. 

1332 key: str 

1333 Last key. Only returned if `value` is set to `True`. 

1334 

1335 Examples 

1336 -------- 

1337 

1338 Add a section and a sub-section to the metadata: 

1339 ``` 

1340 >>> from audioio import print_metadata, add_sections 

1341 >>> md = dict() 

1342 >>> m = add_sections(md, 'Recording.Location') 

1343 >>> m['Country'] = 'Lummerland' 

1344 >>> print_metadata(md) 

1345 Recording: 

1346 Location: 

1347 Country: Lummerland 

1348 ``` 

1349 

1350 Add a section with a key-value pair: 

1351 ``` 

1352 >>> md = dict() 

1353 >>> m, k = add_sections(md, 'Recording.Location', True) 

1354 >>> m[k] = 'Lummerland' 

1355 >>> print_metadata(md) 

1356 Recording: 

1357 Location: Lummerland 

1358 ``` 

1359 

1360 Adds well to `find_key()`: 

1361 ``` 

1362 >>> md = dict(Recording=dict()) 

1363 >>> m, k = find_key(md, 'Recording.Location.Country') 

1364 >>> m, k = add_sections(m, k, True) 

1365 >>> m[k] = 'Lummerland' 

1366 >>> print_metadata(md) 

1367 Recording: 

1368 Location: 

1369 Country: Lummerland 

1370 ``` 

1371 

1372 """ 

1373 mm = metadata 

1374 ks = sections.split(sep) 

1375 n = len(ks) 

1376 if value: 

1377 n -= 1 

1378 for k in ks[:n]: 

1379 if len(k) == 0: 

1380 continue 

1381 mm[k] = dict() 

1382 mm = mm[k] 

1383 if value: 

1384 return mm, ks[-1] 

1385 else: 

1386 return mm 

1387 

1388 

1389def strlist_to_dict(mds): 

1390 """Convert list of key-value-pair strings to dictionary. 

1391 

1392 Parameters 

1393 ---------- 

1394 mds: None or dict or str or list of str 

1395 - None - returns empty dictionary. 

1396 - Flat dictionary - returned as is. 

1397 - String with key and value separated by '='. 

1398 - List of strings with keys and values separated by '='. 

1399 Keys may contain section names. 

1400 

1401 Returns 

1402 ------- 

1403 md_dict: dict 

1404 Flat dictionary with key-value pairs. 

1405 Keys may contain section names. 

1406 Values are strings, other types or dictionaries. 

1407 """ 

1408 if mds is None: 

1409 return {} 

1410 if isinstance(mds, dict): 

1411 return mds 

1412 if not isinstance(mds, (list, tuple, np.ndarray)): 

1413 mds = (mds,) 

1414 md_dict = {} 

1415 for md in mds: 

1416 k, v = md.split('=') 

1417 k = k.strip() 

1418 v = v.strip() 

1419 md_dict[k] = v 

1420 return md_dict 

1421 

1422 

1423def set_metadata(metadata, mds, sep='.'): 

1424 """Set values of existing metadata. 

1425 

1426 Only if a key is found in the metadata, its value is updated. 

1427 

1428 Parameters 

1429 ---------- 

1430 metadata: nested dict 

1431 Metadata. 

1432 mds: dict or str or list of str 

1433 - Flat dictionary with key-value pairs for updating the metadata. 

1434 Values can be strings, other types or dictionaries. 

1435 - String with key and value separated by '='. 

1436 - List of strings with key and value separated by '='. 

1437 Keys may contain section names separated by `sep`. 

1438 sep: str 

1439 String that separates section names in the keys of `md_dict`. 

1440 

1441 Examples 

1442 -------- 

1443 ``` 

1444 >>> from audioio import print_metadata, set_metadata 

1445 >>> md = dict(Recording=dict(Time='early')) 

1446 >>> print_metadata(md) 

1447 Recording: 

1448 Time: early 

1449 

1450 >>> set_metadata(md, {'Artist': 'John Doe', # new key-value pair 

1451 'Recording.Time': 'late'}) # change value of existing key 

1452 >>> print_metadata(md) 

1453 Recording: 

1454 Time : late 

1455 ``` 

1456 

1457 See also 

1458 -------- 

1459 add_metadata() 

1460 strlist_to_dict() 

1461 

1462 """ 

1463 if metadata is None: 

1464 return 

1465 md_dict = strlist_to_dict(mds) 

1466 for k in md_dict: 

1467 mm, kk = find_key(metadata, k, sep) 

1468 if kk in mm: 

1469 mm[kk] = md_dict[k] 

1470 

1471 

1472def add_metadata(metadata, mds, sep='.'): 

1473 """Add or modify key-value pairs. 

1474 

1475 If a key does not exist, it is added to the metadata. 

1476 

1477 Parameters 

1478 ---------- 

1479 metadata: nested dict 

1480 Metadata. 

1481 mds: dict or str or list of str 

1482 - Flat dictionary with key-value pairs for updating the metadata. 

1483 Values can be strings or other types. 

1484 - String with key and value separated by '='. 

1485 - List of strings with key and value separated by '='. 

1486 Keys may contain section names separated by `sep`. 

1487 sep: str 

1488 String that separates section names in the keys of `md_list`. 

1489 

1490 Examples 

1491 -------- 

1492 ``` 

1493 >>> from audioio import print_metadata, add_metadata 

1494 >>> md = dict(Recording=dict(Time='early')) 

1495 >>> print_metadata(md) 

1496 Recording: 

1497 Time: early 

1498 

1499 >>> add_metadata(md, {'Artist': 'John Doe', # new key-value pair 

1500 'Recording.Time': 'late', # change value of existing key  

1501 'Recording.Quality': 'amazing', # new key-value pair in existing section 

1502 'Location.Country': 'Lummerland']) # new key-value pair in new section 

1503 >>> print_metadata(md) 

1504 Recording: 

1505 Time : late 

1506 Quality: amazing 

1507 Artist: John Doe 

1508 Location: 

1509 Country: Lummerland 

1510 ``` 

1511 

1512 See also 

1513 -------- 

1514 set_metadata() 

1515 strlist_to_dict() 

1516 

1517 """ 

1518 if metadata is None: 

1519 return 

1520 md_dict = strlist_to_dict(mds) 

1521 for k in md_dict: 

1522 mm, kk = find_key(metadata, k, sep) 

1523 mm, kk = add_sections(mm, kk, True, sep) 

1524 mm[kk] = md_dict[k] 

1525 

1526 

1527def move_metadata(src_md, dest_md, keys, new_key=None, sep='.'): 

1528 """Remove a key from metadata and add it to a dictionary. 

1529 

1530 Parameters 

1531 ---------- 

1532 src_md: nested dict 

1533 Metadata from which a key is removed. 

1534 dest_md: dict 

1535 Dictionary to which the found key and its value are added. 

1536 keys: str or list of str 

1537 List of keys to be searched for in `src_md`. 

1538 Move the first one found to `dest_md`. 

1539 See the `audiometadata.find_key()` function for details. 

1540 new_key: None or str 

1541 If specified add the value of the found key as `new_key` to 

1542 `dest_md`. Otherwise, use the search key. 

1543 sep: str 

1544 String that separates section names in `keys`. 

1545 

1546 Returns 

1547 ------- 

1548 moved: bool 

1549 `True` if key was found and moved to dictionary. 

1550  

1551 Examples 

1552 -------- 

1553 ``` 

1554 >>> from audioio import print_metadata, move_metadata 

1555 >>> md = dict(Artist='John Doe', Recording=dict(Gain='1.42mV')) 

1556 >>> move_metadata(md, md['Recording'], 'Artist', 'Experimentalist') 

1557 >>> print_metadata(md) 

1558 Recording: 

1559 Gain : 1.42mV 

1560 Experimentalist: John Doe 

1561 ``` 

1562  

1563 """ 

1564 if not src_md: 

1565 return False 

1566 if not isinstance(keys, (list, tuple, np.ndarray)): 

1567 keys = (keys,) 

1568 for key in keys: 

1569 m, k = find_key(src_md, key, sep) 

1570 if k in m: 

1571 dest_key = new_key if new_key else k 

1572 dest_md[dest_key] = m.pop(k) 

1573 return True 

1574 return False 

1575 

1576 

1577def remove_metadata(metadata, key_list, sep='.'): 

1578 """Remove key-value pairs or sections from metadata. 

1579 

1580 Parameters 

1581 ---------- 

1582 metadata: nested dict 

1583 Metadata. 

1584 key_list: str or list of str 

1585 List of keys to key-value pairs or sections to be removed 

1586 from the metadata. 

1587 sep: str 

1588 String that separates section names in the keys of `key_list`. 

1589 

1590 Examples 

1591 -------- 

1592 ``` 

1593 >>> from audioio import print_metadata, remove_metadata 

1594 >>> md = dict(aaaa=2, bbbb=dict(ccc=3, ddd=4)) 

1595 >>> remove_metadata(md, ('ccc',)) 

1596 >>> print_metadata(md) 

1597 aaaa: 2 

1598 bbbb: 

1599 ddd: 4 

1600 ``` 

1601 

1602 """ 

1603 if not metadata: 

1604 return 

1605 if not isinstance(key_list, (list, tuple, np.ndarray)): 

1606 key_list = (key_list,) 

1607 for k in key_list: 

1608 mm, kk = find_key(metadata, k, sep) 

1609 if kk in mm: 

1610 del mm[kk] 

1611 

1612 

1613def cleanup_metadata(metadata): 

1614 """Remove empty sections from metadata. 

1615 

1616 Parameters 

1617 ---------- 

1618 metadata: nested dict 

1619 Metadata. 

1620 

1621 Examples 

1622 -------- 

1623 ``` 

1624 >>> from audioio import print_metadata, cleanup_metadata 

1625 >>> md = dict(aaaa=2, bbbb=dict()) 

1626 >>> cleanup_metadata(md) 

1627 >>> print_metadata(md) 

1628 aaaa: 2 

1629 ``` 

1630 

1631 """ 

1632 if not metadata: 

1633 return 

1634 for k in list(metadata): 

1635 if isinstance(metadata[k], dict): 

1636 if len(metadata[k]) == 0: 

1637 del metadata[k] 

1638 else: 

1639 cleanup_metadata(metadata[k]) 

1640 

1641 

1642default_gain_keys = ['gain'] 

1643"""Default keys of gain settings in metadata. Used by `get_gain()` function. 

1644""" 

1645 

1646def get_gain(metadata, gain_key=default_gain_keys, sep='.', 

1647 default=None, default_unit='', remove=False): 

1648 """Get gain and unit from metadata. 

1649 

1650 Parameters 

1651 ---------- 

1652 metadata: nested dict 

1653 Metadata with key-value pairs. 

1654 gain_key: str or list of str 

1655 Key in the file's metadata that holds some gain information. 

1656 If found, the data will be multiplied with the gain, 

1657 and if available, the corresponding unit is returned. 

1658 See the `audiometadata.find_key()` function for details. 

1659 You can modify the default keys via the `default_gain_keys` list 

1660 of the `audiometadata` module. 

1661 sep: str 

1662 String that separates section names in `gain_key`. 

1663 default: None or float 

1664 Returned value if no valid gain was found in `metadata`. 

1665 default_unit: str 

1666 Returned unit if no valid gain was found in `metadata`. 

1667 remove: bool 

1668 If `True`, remove the found key from `metadata`. 

1669 

1670 Returns 

1671 ------- 

1672 fac: float 

1673 Gain factor. If not found in metadata return 1. 

1674 unit: string 

1675 Unit of the data if found in the metadata, otherwise "a.u.". 

1676 """ 

1677 v, u = get_number_unit(metadata, gain_key, sep, default, 

1678 default_unit, remove) 

1679 # fix some TeeGrid gains: 

1680 if len(u) >= 2 and u[-2:] == '/V': 

1681 u = u[:-2] 

1682 return v, u 

1683 

1684 

1685def update_gain(metadata, fac, gain_key=default_gain_keys, sep='.'): 

1686 """Update gain setting in metadata. 

1687 

1688 Searches for the first appearance of a gain key in the metadata 

1689 hierarchy. If found, divide the gain value by `fac`. 

1690 

1691 Parameters 

1692 ---------- 

1693 metadata: nested dict 

1694 Metadata to be updated. 

1695 fac: float 

1696 Factor that was used to scale the data. 

1697 gain_key: str or list of str 

1698 Key in the file's metadata that holds some gain information. 

1699 If found, the data will be multiplied with the gain, 

1700 and if available, the corresponding unit is returned. 

1701 See the `audiometadata.find_key()` function for details. 

1702 You can modify the default keys via the `default_gain_keys` list 

1703 of the `audiometadata` module. 

1704 sep: str 

1705 String that separates section names in `gain_key`. 

1706 

1707 Returns 

1708 ------- 

1709 done: bool 

1710 True if gain has been found and set. 

1711 

1712 

1713 Examples 

1714 -------- 

1715 

1716 ``` 

1717 >>> from audioio import print_metadata, update_gain 

1718 >>> md = dict(Artist='John Doe', Recording=dict(gain='1.4mV')) 

1719 >>> update_gain(md, 2) 

1720 >>> print_metadata(md) 

1721 Artist: John Doe 

1722 Recording: 

1723 gain: 0.70mV 

1724 ``` 

1725 

1726 """ 

1727 if not metadata: 

1728 return False 

1729 if not isinstance(gain_key, (list, tuple, np.ndarray)): 

1730 gain_key = (gain_key,) 

1731 for gk in gain_key: 

1732 m, k = find_key(metadata, gk, sep) 

1733 if k in m and not isinstance(m[k], dict): 

1734 vs = m[k] 

1735 if isinstance(vs, (int, float)): 

1736 m[k] = vs/fac 

1737 else: 

1738 v, u, n = parse_number(vs) 

1739 if not v is None: 

1740 # fix some TeeGrid gains: 

1741 if len(u) >= 2 and u[-2:] == '/V': 

1742 u = u[:-2] 

1743 m[k] = f'{v/fac:.{n+1}f}{u}' 

1744 return True 

1745 return False 

1746 

1747 

1748def set_starttime(metadata, datetime_value, 

1749 time_keys=default_starttime_keys): 

1750 """Set all start-of-recording times in metadata. 

1751 

1752 Parameters 

1753 ---------- 

1754 metadata: nested dict 

1755 Metadata to be updated. 

1756 datetime_value: datetime 

1757 Start date and time of the recording. 

1758 time_keys: tuple of str or list of tuple of str 

1759 Keys to fields denoting calender times, i.e. dates and times. 

1760 Datetimes can be stored in metadata as two separate key-value pairs, 

1761 one for the date and one for the time. Or by a single key-value pair 

1762 for a date-time values. This is why the keys need to be specified in 

1763 tuples with one or two keys. 

1764 Keys may contain section names separated by `sep`.  

1765 See `audiometadata.find_key()` for details. 

1766 You can modify the default time keys via the `default_starttime_keys` 

1767 list of the `audiometadata` module. 

1768 

1769 Returns 

1770 ------- 

1771 success: bool 

1772 True if at least one time has been set. 

1773 

1774 Example 

1775 ------- 

1776 ``` 

1777 >>> from audioio import print_metadata, set_starttime 

1778 >>> md = dict(DateTimeOriginal='2023-04-15T22:10:00', 

1779 OtherTime='2023-05-16T23:20:10', 

1780 BEXT=dict(OriginationDate='2024-03-02', 

1781 OriginationTime='10:42:24')) 

1782 >>> set_starttime(md, '2024-06-17T22:10:05') 

1783 >>> print_metadata(md) 

1784 DateTimeOriginal: 2024-06-17T22:10:05 

1785 OtherTime : 2024-06-17T22:10:05 

1786 BEXT: 

1787 OriginationDate: 2024-06-17 

1788 OriginationTime: 22:10:05 

1789 ``` 

1790 

1791 """ 

1792 if not metadata: 

1793 return False 

1794 if isinstance(datetime_value, str): 

1795 datetime_value = dt.datetime.fromisoformat(datetime_value) 

1796 success = False 

1797 if len(time_keys) > 0 and isinstance(time_keys[0], str): 

1798 time_keys = (time_keys,) 

1799 for key in time_keys: 

1800 if len(key) == 1: 

1801 # datetime: 

1802 m, k = find_key(metadata, key[0]) 

1803 if k in m and not isinstance(m[k], dict): 

1804 if isinstance(m[k], dt.datetime): 

1805 m[k] = datetime_value 

1806 else: 

1807 m[k] = datetime_value.isoformat(timespec='seconds') 

1808 success = True 

1809 else: 

1810 # separate date and time: 

1811 md, kd = find_key(metadata, key[0]) 

1812 if not kd in md or isinstance(md[kd], dict): 

1813 continue 

1814 if isinstance(md[kd], dt.date): 

1815 md[kd] = datetime_value.date() 

1816 else: 

1817 md[kd] = datetime_value.date().isoformat() 

1818 mt, kt = find_key(metadata, key[1]) 

1819 if not kt in mt or isinstance(mt[kt], dict): 

1820 continue 

1821 if isinstance(mt[kt], dt.time): 

1822 mt[kt] = datetime_value.time() 

1823 else: 

1824 mt[kt] = datetime_value.time().isoformat(timespec='seconds') 

1825 success = True 

1826 return success 

1827 

1828 

1829default_timeref_keys = ['TimeReference'] 

1830"""Default keys of integer time references in metadata. 

1831Used by `update_starttime()` function. 

1832""" 

1833 

1834def update_starttime(metadata, deltat, rate, 

1835 time_keys=default_starttime_keys, 

1836 ref_keys=default_timeref_keys): 

1837 """Update start-of-recording times in metadata. 

1838 

1839 Add `deltat` to `time_keys`and `ref_keys` fields in the metadata. 

1840 

1841 Parameters 

1842 ---------- 

1843 metadata: nested dict 

1844 Metadata to be updated. 

1845 deltat: float 

1846 Time in seconds to be added to start times. 

1847 rate: float 

1848 Sampling rate of the data in Hertz. 

1849 time_keys: tuple of str or list of tuple of str 

1850 Keys to fields denoting calender times, i.e. dates and times. 

1851 Datetimes can be stored in metadata as two separate key-value pairs, 

1852 one for the date and one for the time. Or by a single key-value pair 

1853 for a date-time values. This is why the keys need to be specified in 

1854 tuples with one or two keys. 

1855 Keys may contain section names separated by `sep`.  

1856 See `audiometadata.find_key()` for details. 

1857 You can modify the default time keys via the `default_starttime_keys` 

1858 list of the `audiometadata` module. 

1859 ref_keys: str or list of str 

1860 Keys to time references, i.e. integers in seconds relative to 

1861 a reference time. 

1862 Keys may contain section names separated by `sep`.  

1863 See `audiometadata.find_key()` for details. 

1864 You can modify the default reference keys via the 

1865 `default_timeref_keys` list of the `audiometadata` module. 

1866 

1867 Returns 

1868 ------- 

1869 success: bool 

1870 True if at least one time has been updated. 

1871 

1872 Example 

1873 ------- 

1874 ``` 

1875 >>> from audioio import print_metadata, update_starttime 

1876 >>> md = dict(DateTimeOriginal='2023-04-15T22:10:00', 

1877 OtherTime='2023-05-16T23:20:10', 

1878 BEXT=dict(OriginationDate='2024-03-02', 

1879 OriginationTime='10:42:24', 

1880 TimeReference=123456)) 

1881 >>> update_starttime(md, 4.2, 48000) 

1882 >>> print_metadata(md) 

1883 DateTimeOriginal: 2023-04-15T22:10:04 

1884 OtherTime : 2023-05-16T23:20:10 

1885 BEXT: 

1886 OriginationDate: 2024-03-02 

1887 OriginationTime: 10:42:28 

1888 TimeReference : 325056 

1889 ``` 

1890 

1891 """ 

1892 if not metadata: 

1893 return False 

1894 if not isinstance(deltat, dt.timedelta): 

1895 deltat = dt.timedelta(seconds=deltat) 

1896 success = False 

1897 if len(time_keys) > 0 and isinstance(time_keys[0], str): 

1898 time_keys = (time_keys,) 

1899 for key in time_keys: 

1900 if len(key) == 1: 

1901 # datetime: 

1902 m, k = find_key(metadata, key[0]) 

1903 if k in m and not isinstance(m[k], dict): 

1904 if isinstance(m[k], dt.datetime): 

1905 m[k] += deltat 

1906 else: 

1907 datetime = dt.datetime.fromisoformat(m[k]) + deltat 

1908 m[k] = datetime.isoformat(timespec='seconds') 

1909 success = True 

1910 else: 

1911 # separate date and time: 

1912 md, kd = find_key(metadata, key[0]) 

1913 if not kd in md or isinstance(md[kd], dict): 

1914 continue 

1915 if isinstance(md[kd], dt.date): 

1916 date = md[kd] 

1917 is_date = True 

1918 else: 

1919 date = dt.date.fromisoformat(md[kd]) 

1920 is_date = False 

1921 mt, kt = find_key(metadata, key[1]) 

1922 if not kt in mt or isinstance(mt[kt], dict): 

1923 continue 

1924 if isinstance(mt[kt], dt.time): 

1925 time = mt[kt] 

1926 is_time = True 

1927 else: 

1928 time = dt.time.fromisoformat(mt[kt]) 

1929 is_time = False 

1930 datetime = dt.datetime.combine(date, time) + deltat 

1931 md[kd] = datetime.date() if is_date else datetime.date().isoformat() 

1932 mt[kt] = datetime.time() if is_time else datetime.time().isoformat(timespec='seconds') 

1933 success = True 

1934 # time reference in samples: 

1935 if isinstance(ref_keys, str): 

1936 ref_keys = (ref_keys,) 

1937 for key in ref_keys: 

1938 m, k = find_key(metadata, key) 

1939 if k in m and not isinstance(m[k], dict): 

1940 is_int = isinstance(m[k], int) 

1941 tref = int(m[k]) 

1942 tref += int(np.round(deltat.total_seconds()*rate)) 

1943 m[k] = tref if is_int else f'{tref}' 

1944 success = True 

1945 return success 

1946 

1947 

1948def bext_history_str(encoding, rate, channels, text=None): 

1949 """ Assemble a string for the BEXT CodingHistory field. 

1950 

1951 Parameters 

1952 ---------- 

1953 encoding: str or None 

1954 Encoding of the data. 

1955 rate: int or float 

1956 Sampling rate in Hertz. 

1957 channels: int 

1958 Number of channels. 

1959 text: str or None 

1960 Optional free text. 

1961 

1962 Returns 

1963 ------- 

1964 s: str 

1965 String for the BEXT CodingHistory field, 

1966 something like "A=PCM_16,F=44100,W=16,M=stereo,T=cut out" 

1967 """ 

1968 codes = [] 

1969 bits = None 

1970 if encoding is not None: 

1971 if encoding[:3] == 'PCM': 

1972 bits = int(encoding[4:]) 

1973 encoding = 'PCM' 

1974 codes.append(f'A={encoding}') 

1975 codes.append(f'F={rate:.0f}') 

1976 if bits is not None: 

1977 codes.append(f'W={bits}') 

1978 mode = None 

1979 if channels == 1: 

1980 mode = 'mono' 

1981 elif channels == 2: 

1982 mode = 'stereo' 

1983 if mode is not None: 

1984 codes.append(f'M={mode}') 

1985 if text is not None: 

1986 codes.append(f'T={text.rstrip()}') 

1987 return ','.join(codes) 

1988 

1989 

1990default_history_keys = ['History', 

1991 'CodingHistory', 

1992 'BWF_CODING_HISTORY'] 

1993"""Default keys of strings describing coding history in metadata. 

1994Used by `add_history()` function. 

1995""" 

1996 

1997def add_history(metadata, history, new_key=None, pre_history=None, 

1998 history_keys=default_history_keys, sep='.'): 

1999 """Add a string describing coding history to metadata. 

2000  

2001 Add `history` to the `history_keys` fields in the metadata. If 

2002 none of these fields are present but `new_key` is specified, then 

2003 assign `pre_history` and `history` to this key. If this key does 

2004 not exist in the metadata, it is created. 

2005 

2006 Parameters 

2007 ---------- 

2008 metadata: nested dict 

2009 Metadata to be updated. 

2010 history: str 

2011 String to be added to the history. 

2012 new_key: str or None 

2013 Sections and name of a history key to be added to `metadata`. 

2014 Section names are separated by `sep`. 

2015 pre_history: str or None 

2016 If a new key `new_key` is created, then assign this string followed 

2017 by `history`. 

2018 history_keys: str or list of str 

2019 Keys to fields where to add `history`. 

2020 Keys may contain section names separated by `sep`.  

2021 See `audiometadata.find_key()` for details. 

2022 You can modify the default history keys via the `default_history_keys` 

2023 list of the `audiometadata` module. 

2024 sep: str 

2025 String that separates section names in `new_key` and `history_keys`. 

2026 

2027 Returns 

2028 ------- 

2029 success: bool 

2030 True if the history string has beend added to the metadata. 

2031 

2032 Example 

2033 ------- 

2034 Add string to existing history key-value pair: 

2035 ``` 

2036 >>> from audioio import add_history 

2037 >>> md = dict(aaa='xyz', BEXT=dict(CodingHistory='original recordings')) 

2038 >>> add_history(md, 'just a snippet') 

2039 >>> print(md['BEXT']['CodingHistory']) 

2040 original recordings 

2041 just a snippet 

2042 ``` 

2043 

2044 Assign string to new key-value pair: 

2045 ``` 

2046 >>> md = dict(aaa='xyz', BEXT=dict(OriginationDate='2024-02-12')) 

2047 >>> add_history(md, 'just a snippet', 'BEXT.CodingHistory', 'original data') 

2048 >>> print(md['BEXT']['CodingHistory']) 

2049 original data 

2050 just a snippet 

2051 ``` 

2052 

2053 """ 

2054 if not metadata: 

2055 return False 

2056 if isinstance(history_keys, str): 

2057 history_keys = (history_keys,) 

2058 success = False 

2059 for keys in history_keys: 

2060 m, k = find_key(metadata, keys) 

2061 if k in m and not isinstance(m[k], dict): 

2062 s = m[k] 

2063 if len(s) >= 1 and s[-1] != '\n' and s[-1] != '\r': 

2064 s += '\r\n' 

2065 s += history 

2066 m[k] = s 

2067 success = True 

2068 if not success and new_key: 

2069 m, k = find_key(metadata, new_key, sep) 

2070 m, k = add_sections(m, k, True, sep) 

2071 s = '' 

2072 if pre_history is not None: 

2073 s = pre_history 

2074 if len(s) >= 1 and s[-1] != '\n' and s[-1] != '\r': 

2075 s += '\r\n' 

2076 s += history 

2077 m[k] = s 

2078 success = True 

2079 return success 

2080 

2081 

2082def add_unwrap(metadata, thresh, clip=0, unit=''): 

2083 """Add unwrap infos to metadata. 

2084 

2085 If `audiotools.unwrap()` was applied to the data, then this 

2086 function adds relevant infos to the metadata. If there is an INFO 

2087 section in the metadata, the unwrap infos are added to this 

2088 section, otherwise they are added to the top level of the metadata 

2089 hierarchy. 

2090 

2091 The threshold `thresh` used for unwrapping is saved under the key 

2092 'UnwrapThreshold' as a string. If `clip` is larger than zero, then 

2093 the clip level is saved under the key 'UnwrapClippedAmplitude' as 

2094 a string. 

2095 

2096 Parameters 

2097 ---------- 

2098 md: nested dict 

2099 Metadata to be updated. 

2100 thresh: float 

2101 Threshold used for unwrapping. 

2102 clip: float 

2103 Level at which unwrapped data have been clipped. 

2104 unit: str 

2105 Unit of `thresh` and `clip`. 

2106 

2107 Examples 

2108 -------- 

2109 

2110 ``` 

2111 >>> from audioio import print_metadata, add_unwrap 

2112 >>> md = dict(INFO=dict(Time='early')) 

2113 >>> add_unwrap(md, 0.6, 1.0) 

2114 >>> print_metadata(md) 

2115 INFO: 

2116 Time : early 

2117 UnwrapThreshold : 0.60 

2118 UnwrapClippedAmplitude: 1.00 

2119 ``` 

2120 

2121 """ 

2122 if metadata is None: 

2123 return 

2124 md = metadata 

2125 for k in metadata: 

2126 if k.strip().upper() == 'INFO': 

2127 md = metadata['INFO'] 

2128 break 

2129 md['UnwrapThreshold'] = f'{thresh:.2f}{unit}' 

2130 if clip > 0: 

2131 md['UnwrapClippedAmplitude'] = f'{clip:.2f}{unit}' 

2132 

2133 

2134def demo(file_pathes, list_format, list_metadata, list_cues, list_chunks): 

2135 """Print metadata and markers of audio files. 

2136 

2137 Parameters 

2138 ---------- 

2139 file_pathes: list of str 

2140 Pathes of audio files. 

2141 list_format: bool 

2142 If True, list file format only. 

2143 list_metadata: bool 

2144 If True, list metadata only. 

2145 list_cues: bool 

2146 If True, list markers/cues only. 

2147 list_chunks: bool 

2148 If True, list all chunks contained in a riff/wave file. 

2149 """ 

2150 from .audioloader import AudioLoader 

2151 from .audiomarkers import print_markers 

2152 from .riffmetadata import read_chunk_tags 

2153 for filepath in file_pathes: 

2154 if len(file_pathes) > 1 and (list_cues or list_metadata or 

2155 list_format or list_chunks): 

2156 print(filepath) 

2157 if list_chunks: 

2158 chunks = read_chunk_tags(filepath) 

2159 print(f' {"chunk tag":10s} {"position":10s} {"size":10s}') 

2160 for tag in chunks: 

2161 pos = chunks[tag][0] - 8 

2162 size = chunks[tag][1] + 8 

2163 print(f' {tag:9s} {pos:10d} {size:10d}') 

2164 if len(file_pathes) > 1: 

2165 print() 

2166 continue 

2167 with AudioLoader(filepath, 1, 0, verbose=0) as sf: 

2168 fmt_md = sf.format_dict() 

2169 meta_data = sf.metadata() 

2170 locs, labels = sf.markers() 

2171 if list_cues: 

2172 if len(locs) > 0: 

2173 print_markers(locs, labels) 

2174 elif list_metadata: 

2175 print_metadata(meta_data, replace='.') 

2176 elif list_format: 

2177 print_metadata(fmt_md) 

2178 else: 

2179 print('file:') 

2180 print_metadata(fmt_md, ' ') 

2181 if len(meta_data) > 0: 

2182 print() 

2183 print('metadata:') 

2184 print_metadata(meta_data, ' ', replace='.') 

2185 if len(locs) > 0: 

2186 print() 

2187 print('markers:') 

2188 print_markers(locs, labels) 

2189 if len(file_pathes) > 1: 

2190 print() 

2191 if len(file_pathes) > 1: 

2192 print() 

2193 

2194 

2195def main(*cargs): 

2196 """Call demo with command line arguments. 

2197 

2198 Parameters 

2199 ---------- 

2200 cargs: list of strings 

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

2202 """ 

2203 # command line arguments: 

2204 parser = argparse.ArgumentParser(add_help=True, 

2205 description='Convert audio file formats.', 

2206 epilog=f'version {__version__} by Benda-Lab (2020-{__year__})') 

2207 parser.add_argument('--version', action='version', version=__version__) 

2208 parser.add_argument('-f', dest='dataformat', action='store_true', 

2209 help='list file format only') 

2210 parser.add_argument('-m', dest='metadata', action='store_true', 

2211 help='list metadata only') 

2212 parser.add_argument('-c', dest='cues', action='store_true', 

2213 help='list cues/markers only') 

2214 parser.add_argument('-t', dest='chunks', action='store_true', 

2215 help='list tags of all riff/wave chunks contained in the file') 

2216 parser.add_argument('files', type=str, nargs='+', 

2217 help='audio file') 

2218 if len(cargs) == 0: 

2219 cargs = None 

2220 args = parser.parse_args(cargs) 

2221 

2222 # expand wildcard patterns: 

2223 files = [] 

2224 if os.name == 'nt': 

2225 for fn in args.files: 

2226 files.extend(glob.glob(fn)) 

2227 else: 

2228 files = args.files 

2229 

2230 demo(files, args.dataformat, args.metadata, args.cues, args.chunks) 

2231 

2232 

2233if __name__ == "__main__": 

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