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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 19:14 +0000
1"""Working with metadata.
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.
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```
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.
37## Functions
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.
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.
56Do not forget that you can easily manipulate the metadata by means of
57the standard functions of dictionaries.
59If you need to make a copy of the metadata use `deepcopy`:
60```
61from copy import deepcopy
62md_orig = deepcopy(md)
63```
65### Output
67Write and load nested dictionaries as texts:
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.
73### Flatten
75Conversion between nested and flat dictionaries:
77- `flatten_metadata()`: flatten hierachical metadata to a single dictionary.
78- `unflatten_metadata()`: unflatten a previously flattened metadata dictionary.
80### Parse numbers with units
82- `parse_number()`: parse string with number and unit.
83- `change_unit()`: scale numerical value to a new unit.
85### Find and get values
87Find keys and get their values parsed and converted to various types:
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.
97### Organize metadata
99Add and remove metadata:
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.
109### Special metadata fields
111Retrieve and set specific metadata:
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.
121Lists of standard keys:
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.
129## Command line script
131The module can be run as a script from the command line to display the
132metadata and markers contained in an audio file:
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
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```
158Alternatively, the script can be run from within the audioio source tree as:
159```
160python -m src.audioio.audiometadata audiofile.wav
161```
163Running
164```sh
165audiometadata --help
166```
167prints
168```text
169usage: audiometadata [-h] [--version] [-f] [-m] [-c] [-t] files [files ...]
171Convert audio file formats.
173positional arguments:
174 files audio file
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
184version 2.0.0 by Benda-Lab (2020-2024)
185```
187"""
189import os
190import sys
191import glob
192import argparse
193import numpy as np
194import datetime as dt
196from pathlib import Path
197from .version import __version__, __year__
200def write_metadata_text(fh, meta, prefix='', indent=4, replace=None):
201 """Write meta data into a text/yaml file or stream.
203 With the default parameters, the output is a valid yaml file.
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.
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 """
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')
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()
265def print_metadata(meta, prefix='', indent=4, replace=None):
266 """Write meta data to standard output.
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.
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)
298def load_metadata(fh, prefix=''):
299 """Load meta data from a text/yaml file or stream.
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.
309 Returns
310 -------
311 meta: nested dict
312 Key-value pairs of metadata loaded from the file.
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
390def flatten_metadata(md, keep_sections=False, sep='.'):
391 """Flatten hierarchical metadata to a single dictionary.
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.
402 Returns
403 -------
404 d: dict
405 Non-nested dict containing all key-value pairs of `md`.
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
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
443 return flatten(md, '')
446def unflatten_metadata(md, sep='.'):
447 """Unflatten a previously flattened metadata dictionary.
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.
457 Returns
458 -------
459 d: nested dict
460 Hierarchical dictionary with sub-dictionaries and key-value pairs.
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
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
510def parse_number(s):
511 """Parse string with number and unit.
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.
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.
531 Examples
532 --------
534 ```
535 >>> from audioio import parse_number
537 # integer:
538 >>> parse_number('42')
539 (42, '', 0)
541 # integer with unit:
542 >>> parse_number('42ms')
543 (42, 'ms', 0)
545 # float with unit:
546 >>> parse_number('42.ms')
547 (42.0, 'ms', 0)
549 # float with unit:
550 >>> parse_number('42.3ms')
551 (42.3, 'ms', 1)
553 # float with space and unit:
554 >>> parse_number('423.17 Hz')
555 (423.17, 'Hz', 2)
556 ```
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
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. """
602def change_unit(val, old_unit, new_unit):
603 """Scale numerical value to a new unit.
605 Adapted from https://github.com/relacs/relacs/blob/1facade622a80e9f51dbf8e6f8171ac74c27f100/options/src/parameter.cc#L1647-L1703
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.
616 Returns
617 -------
618 new_val: float
619 The input value `val` scaled to `new_unit`.
621 Examples
622 --------
624 ```
625 >>> from audioio import change_unit
626 >>> change_unit(5, 'mm', 'cm')
627 0.5
629 >>> change_unit(5, '', 'cm')
630 5.0
632 >>> change_unit(5, 'mm', '')
633 5.0
635 >>> change_unit(5, 'cm', 'mm')
636 50.0
638 >>> change_unit(4, 'kg', 'g')
639 4000.0
641 >>> change_unit(12, '%', '')
642 0.12
644 >>> change_unit(1.24, '', '%')
645 124.0
647 >>> change_unit(2.5, 'min', 's')
648 150.0
650 >>> change_unit(3600, 's', 'h')
651 1.0
653 ```
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
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}
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];
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];
685 return val*f1/f2
688def find_key(metadata, key, sep='.'):
689 """Find dictionary in metadata hierarchy containing the specified key.
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`.
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`.
713 Examples
714 --------
716 Independent of whether found or not found, you can assign to the
717 returned dictionary with the returned key.
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
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 ...
741 >>> m, k = find_key(md, 'hhh')
742 >>> m[k] = 12
743 >>> print_metadata(md)
744 ...
745 gggg:
746 hhh: 12
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 ```
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 ```
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)
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
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.
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`.
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.
834 Examples
835 --------
837 ```
838 >>> from audioio import get_number_unit
839 >>> md = dict(aaaa='42', bbbb='42.3ms')
841 # integer:
842 >>> get_number_unit(md, 'aaaa')
843 (42, '')
845 # float with unit:
846 >>> get_number_unit(md, 'bbbb')
847 (42.3, 'ms')
849 # two keys:
850 >>> get_number_unit(md, ['cccc', 'bbbb'])
851 (42.3, 'ms')
853 # not found:
854 >>> get_number_unit(md, 'cccc')
855 (None, '')
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 ```
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
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.
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`.
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.
914 Examples
915 --------
917 ```
918 >>> from audioio import get_number
919 >>> md = dict(aaaa='42', bbbb='42.3ms')
921 # milliseconds to seconds:
922 >>> get_number(md, 's', 'bbbb')
923 0.0423
925 # milliseconds to microseconds:
926 >>> get_number(md, 'us', 'bbbb')
927 42300.0
929 # value without unit is not scaled:
930 >>> get_number(md, 'Hz', 'aaaa')
931 42
933 # two keys:
934 >>> get_number(md, 's', ['cccc', 'bbbb'])
935 0.0423
937 # not found:
938 >>> get_number(md, 's', 'cccc')
939 None
941 # not found with default value:
942 >>> get_number(md, 's', 'cccc', default=1.0)
943 1.0
944 ```
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)
954def get_int(metadata, keys, sep='.', default=None, remove=False):
955 """Find a key in metadata and return its integer value.
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`.
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.
982 Examples
983 --------
985 ```
986 >>> from audioio import get_int
987 >>> md = dict(aaaa='42', bbbb='42.3ms')
989 # integer:
990 >>> get_int(md, 'aaaa')
991 42
993 # two keys:
994 >>> get_int(md, ['cccc', 'aaaa'])
995 42
997 # float:
998 >>> get_int(md, 'bbbb')
999 None
1001 # not found:
1002 >>> get_int(md, 'cccc')
1003 None
1005 # not found with default value:
1006 >>> get_int(md, 'cccc', default=0)
1007 0
1008 ```
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
1026def get_bool(metadata, keys, sep='.', default=None, remove=False):
1027 """Find a key in metadata and return its boolean value.
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`.
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.
1056 Examples
1057 --------
1059 ```
1060 >>> from audioio import get_bool
1061 >>> md = dict(aaaa='TruE', bbbb='No', cccc=0, dddd=1, eeee=True, ffff='ui')
1063 # case insensitive:
1064 >>> get_bool(md, 'aaaa')
1065 True
1067 >>> get_bool(md, 'bbbb')
1068 False
1070 >>> get_bool(md, 'cccc')
1071 False
1073 >>> get_bool(md, 'dddd')
1074 True
1076 >>> get_bool(md, 'eeee')
1077 True
1079 # not found:
1080 >>> get_bool(md, 'ffff')
1081 None
1083 # two keys (string is preferred over number):
1084 >>> get_bool(md, ['cccc', 'aaaa'])
1085 True
1087 # two keys (take first match):
1088 >>> get_bool(md, ['cccc', 'ffff'])
1089 False
1091 # not found with default value:
1092 >>> get_bool(md, 'ffff', default=False)
1093 False
1094 ```
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
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"""
1135def get_datetime(metadata, keys=default_starttime_keys,
1136 sep='.', default=None, remove=False):
1137 """Find keys in metadata and return a datetime.
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`.
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.
1168 Examples
1169 --------
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')
1177 # separate date and time:
1178 >>> get_datetime(md, ('date', 'time'))
1179 datetime.datetime(2024, 3, 2, 10, 42, 24)
1181 # single datetime:
1182 >>> get_datetime(md, ('datetime',))
1183 datetime.datetime(2023, 4, 15, 22, 10)
1185 # two alternative key tuples:
1186 >>> get_datetime(md, [('aaaa',), ('date', 'time')])
1187 datetime.datetime(2024, 3, 2, 10, 42, 24)
1189 # not found:
1190 >>> get_datetime(md, ('cccc',))
1191 None
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 ```
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
1243def get_str(metadata, keys, sep='.', default=None, remove=False):
1244 """Find a key in metadata and return its string value.
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`.
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.
1269 Examples
1270 --------
1272 ```
1273 >>> from audioio import get_str
1274 >>> md = dict(aaaa=42, bbbb='hello')
1276 # string:
1277 >>> get_str(md, 'bbbb')
1278 'hello'
1280 # int as str:
1281 >>> get_str(md, 'aaaa')
1282 '42'
1284 # two keys:
1285 >>> get_str(md, ['cccc', 'bbbb'])
1286 'hello'
1288 # not found:
1289 >>> get_str(md, 'cccc')
1290 None
1292 # not found with default value:
1293 >>> get_str(md, 'cccc', default='-')
1294 '-'
1295 ```
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
1312def add_sections(metadata, sections, value=False, sep='.'):
1313 """Add sections to metadata dictionary.
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`.
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`.
1335 Examples
1336 --------
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 ```
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 ```
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 ```
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
1389def strlist_to_dict(mds):
1390 """Convert list of key-value-pair strings to dictionary.
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.
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
1423def set_metadata(metadata, mds, sep='.'):
1424 """Set values of existing metadata.
1426 Only if a key is found in the metadata, its value is updated.
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`.
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
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 ```
1457 See also
1458 --------
1459 add_metadata()
1460 strlist_to_dict()
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]
1472def add_metadata(metadata, mds, sep='.'):
1473 """Add or modify key-value pairs.
1475 If a key does not exist, it is added to the metadata.
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`.
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
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 ```
1512 See also
1513 --------
1514 set_metadata()
1515 strlist_to_dict()
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]
1527def move_metadata(src_md, dest_md, keys, new_key=None, sep='.'):
1528 """Remove a key from metadata and add it to a dictionary.
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`.
1546 Returns
1547 -------
1548 moved: bool
1549 `True` if key was found and moved to dictionary.
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 ```
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
1577def remove_metadata(metadata, key_list, sep='.'):
1578 """Remove key-value pairs or sections from metadata.
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`.
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 ```
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]
1613def cleanup_metadata(metadata):
1614 """Remove empty sections from metadata.
1616 Parameters
1617 ----------
1618 metadata: nested dict
1619 Metadata.
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 ```
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])
1642default_gain_keys = ['gain']
1643"""Default keys of gain settings in metadata. Used by `get_gain()` function.
1644"""
1646def get_gain(metadata, gain_key=default_gain_keys, sep='.',
1647 default=None, default_unit='', remove=False):
1648 """Get gain and unit from metadata.
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`.
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
1685def update_gain(metadata, fac, gain_key=default_gain_keys, sep='.'):
1686 """Update gain setting in metadata.
1688 Searches for the first appearance of a gain key in the metadata
1689 hierarchy. If found, divide the gain value by `fac`.
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`.
1707 Returns
1708 -------
1709 done: bool
1710 True if gain has been found and set.
1713 Examples
1714 --------
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 ```
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
1748def set_starttime(metadata, datetime_value,
1749 time_keys=default_starttime_keys):
1750 """Set all start-of-recording times in metadata.
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.
1769 Returns
1770 -------
1771 success: bool
1772 True if at least one time has been set.
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 ```
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
1829default_timeref_keys = ['TimeReference']
1830"""Default keys of integer time references in metadata.
1831Used by `update_starttime()` function.
1832"""
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.
1839 Add `deltat` to `time_keys`and `ref_keys` fields in the metadata.
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.
1867 Returns
1868 -------
1869 success: bool
1870 True if at least one time has been updated.
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 ```
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
1948def bext_history_str(encoding, rate, channels, text=None):
1949 """ Assemble a string for the BEXT CodingHistory field.
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.
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)
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"""
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.
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.
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`.
2027 Returns
2028 -------
2029 success: bool
2030 True if the history string has beend added to the metadata.
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 ```
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 ```
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
2082def add_unwrap(metadata, thresh, clip=0, unit=''):
2083 """Add unwrap infos to metadata.
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.
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.
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`.
2107 Examples
2108 --------
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 ```
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}'
2134def demo(file_pathes, list_format, list_metadata, list_cues, list_chunks):
2135 """Print metadata and markers of audio files.
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()
2195def main(*cargs):
2196 """Call demo with command line arguments.
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)
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
2230 demo(files, args.dataformat, args.metadata, args.cues, args.chunks)
2233if __name__ == "__main__":
2234 main(*sys.argv[1:])