Module audioio.fixtimestamps
Fix time stamps.
Change time stamps in the metadata (of wave files) and file names without rewriting the entire file. This is useful in case the real-time clock of a recorder failed.
Command line script
Let's assume you have a continous recording spread over the following four files each covering 3 minutes of the recording:
logger-20190101T000015.wav
logger-20190101T000315.wav
logger-20190101T000615.wav
logger-20190101T000915.wav
However, the recording was actually started at 2025-06-09T10:42:17. Obviously, the real-time clock failed, since all times in the file name and the time stamps in the metadata start in the year 2019.
To fix this, run
> fixtimestamps -s '2025-06-09T10:42:17' logger-2019*.wav
Then the files are renamed:
logger-20190101T000015.wav -> logger-20250609T104217.wav
logger-20190101T000315.wav -> logger-20250609T104517.wav
logger-20190101T000615.wav -> logger-20250609T104817.wav
logger-20190101T000915.wav -> logger-20250609T105117.wav
and the time stamps in the meta data are set accordingly.
Adding the -n
flag runs the script in dry mode, i.e. it just reports what it would do without modifying the audio files:
> fixtimestamps -n -s 20250701T173420 *.wav
Alternatively, the script can be run from within the audioio source tree as:
python -m src.audioio.fixtimestamps -s 20250701T173420 *.wav
Running
fixtimestamps --help
prints
usage: fixtimestamps [-h] [--version] -s STARTTIME [-n] files [files ...]
Fix time stamps.
positional arguments:
files audio files
options:
-h, --help show this help message and exit
--version show program's version number and exit
-s STARTTIME new start time of the first file
-n do not modify the files, just report what would be done.
version 2.5.0 by Benda-Lab (2020-2025)
Functions
parse_datetime()
: parse string for a date and a time.replace_datetime()
: replace in a string date and time.write_riff_datetime()
: modify time stamps in the metadata of a RIFF/WAVE file.
Functions
def parse_datetime(string)
-
Expand source code
def parse_datetime(string): """Parse string for a date and a time. Parameters ---------- string: str String to be parsed. Returns ------- dtime: datetime or None The date and time parsed from the string. None if neither a date nor a time was found. """ date = None time = None time_pos = 0 m = re.search('([123][0-9][0-9][0-9]-[01][0-9]-[0123][0-9])', string) if m is not None: date = dt.date.fromisoformat(m[0]) time_pos = m.end() else: m = re.search('([123][0-9][0-9][0-9][01][0-9][0123][0-9])', string) if m is not None: dts = m[0] dts = f'{dts[0:4]}-{dts[4:6]}-{dts[6:8]}' date = dt.date.fromisoformat(dts) time_pos = m.end() m = re.search('([012][0-9]:[0-5][0-9]:[0-5][0-9])', string[time_pos:]) if m is not None: time = dt.time.fromisoformat(m[0]) else: m = re.search('([012][0-9][0-5][0-9][0-5][0-9])', string[time_pos:]) if m is not None: dts = m[0] dts = f'{dts[0:2]}:{dts[2:4]}:{dts[4:6]}' time = dt.time.fromisoformat(dts) if date is None and time is None: return None if date is None: date = dt.date(1, 1, 1) if time is None: time = dt.time(0, 0, 0) dtime = dt.datetime.combine(date, time) return dtime
Parse string for a date and a time.
Parameters
string
:str
- String to be parsed.
Returns
dtime
:datetime
orNone
- The date and time parsed from the string. None if neither a date nor a time was found.
def replace_datetime(string, date_time)
-
Expand source code
def replace_datetime(string, date_time): """ Replace in a string date and time. Parameters ---------- string: str String in which date and time are replaced. date_time: datetime Date and time to write into the string. Returns ------- new_string: str The `string` with date and time replaced by `date_time`. """ if date_time is None: return string new_string = string time_pos = 0 dts = date_time.date().isoformat() pattern = re.compile('([123][0-9][0-9][0-9]-[01][0-9]-[0123][0-9])') m = pattern.search(new_string) if m is not None: time_pos = m.end() new_string = pattern.sub(dts, new_string) else: pattern = re.compile('([123][0-9][0-9][0-9][01][0-9][0123][0-9])') m = pattern.search(new_string) if m is not None: time_pos = m.end() new_string = pattern.sub(dts.replace('-', ''), new_string) dts = date_time.time().isoformat() pattern = re.compile('([012][0-9]:[0-5][0-9]:[0-5][0-9])') m = pattern.search(new_string[time_pos:]) if m is not None: new_string = new_string[:time_pos] + \ pattern.sub(dts, new_string[time_pos:]) else: pattern = re.compile('([012][0-9][0-5][0-9][0-5][0-9])') m = pattern.search(new_string[time_pos:]) if m is not None: new_string = new_string[:time_pos] + \ pattern.sub(dts.replace(':', ''), new_string[time_pos:]) return new_string
Replace in a string date and time.
Parameters
string
:str
- String in which date and time are replaced.
date_time
:datetime
- Date and time to write into the string.
Returns
new_string
:str
- The
string
with date and time replaced bydate_time
.
def write_riff_datetime(path, start_time, file_time=None, no_mod=False)
-
Expand source code
def write_riff_datetime(path, start_time, file_time=None, no_mod=False): """ Modify time stamps in the metadata of a RIFF/WAVE file. Parameters ---------- path: str Path to a wave file. start_time: datetime Date and time to which all time stamps should be set. file_time: None or date_time If provided check whether the time stamp in the metadata matches. Returns ------- duration: timedelta Total duration of the audio data in the file. orig_time: date_time or None The time stamp found in the metadata. no_mod: bool Do not modify the files, just report what would be done. """ def check_starttime(file_orig, time_time, path): if file_time is not None and orig_time is not None and \ abs(orig_time - file_time) > dt.timedelta(seconds=1): raise ValueError(f'"{path}" start time is {orig_time} but should be {file_time} for a continuous recording.') duration = dt.timedelta(seconds=0) orig_time = None store_empty = False with open(path, 'r+b') as sf: try: fsize = read_riff_header(sf) except ValueError: raise ValueError(f'"{path}" is not a valid RIFF/WAVE file, time stamps cannot be modified.') tags = read_chunk_tags(sf) if 'FMT ' not in tags: raise ValueError(f'missing FMT chunk in "{path}".') sf.seek(tags['FMT '][0] - 4, os.SEEK_SET) channels, rate, bits = read_format_chunk(sf) bts = 1 + (bits - 1) // 8 if 'DATA' not in tags: raise ValueError(f'missing DATA chunk in "{path}".') dsize = tags['DATA'][1] duration = dt.timedelta(seconds=(dsize//bts//channels)/rate) for chunk in tags: sf.seek(tags[chunk][0] - 4, os.SEEK_SET) md = {} if chunk == 'LIST-INFO': md['INFO'] = read_info_chunks(sf, store_empty) orig_time = get_datetime(md) check_starttime(orig_time, file_time, path) if not no_mod and set_starttime(md, start_time): sf.seek(tags[chunk][0] - 8, os.SEEK_SET) write_info_chunk(sf, md, tags[chunk][1]) elif chunk == 'BEXT': md['BEXT'] = read_bext_chunk(sf, store_empty) orig_time = get_datetime(md) check_starttime(orig_time, file_time, path) if not no_mod and set_starttime(md, start_time): sf.seek(tags[chunk][0] - 8, os.SEEK_SET) write_bext_chunk(sf, md) elif chunk == 'IXML': md['IXML'] = read_ixml_chunk(sf, store_empty) orig_time = get_datetime(md) check_starttime(orig_time, file_time, path) if not no_mod and set_starttime(md, start_time): sf.seek(tags[chunk][0] - 8, os.SEEK_SET) write_ixml_chunk(sf, md) elif chunk == 'GUAN': md['GUANO'] = read_guano_chunk(sf) orig_time = get_datetime(md) check_starttime(orig_time, file_time, path) if not no_mod and set_starttime(md, start_time): sf.seek(tags[chunk][0] - 8, os.SEEK_SET) write_guano_chunk(sf, md['GUANO']) return duration, orig_time
Modify time stamps in the metadata of a RIFF/WAVE file.
Parameters
path
:str
- Path to a wave file.
start_time
:datetime
- Date and time to which all time stamps should be set.
file_time
:None
ordate_time
- If provided check whether the time stamp in the metadata matches.
Returns
duration
:timedelta
- Total duration of the audio data in the file.
orig_time
:date_time
orNone
- The time stamp found in the metadata.
no_mod
:bool
- Do not modify the files, just report what would be done.
def demo(start_time, file_pathes, no_mod=False)
-
Expand source code
def demo(start_time, file_pathes, no_mod=False): """Modify time stamps of audio files. Parameters ---------- start_time: str Time stamp of the first file. file_pathes: list of str Pathes of audio files. no_mod: bool Do not modify the files, just report what would be done. """ file_time = None start_time = dt.datetime.fromisoformat(start_time) for fp in file_pathes: duration, orig_time = write_riff_datetime(fp, start_time, file_time, no_mod) name_time = parse_datetime(Path(fp).stem) if orig_time is None: orig_time = name_time if file_time is None: file_time = orig_time if orig_time is None: raise ValueError(f'"{fp}" does not contain any time in its metadata or name.') if name_time is not None: p = Path(fp) np = p.with_stem(replace_datetime(p.stem, start_time)) if not no_mod: os.rename(fp, np) print(f'{fp} -> {np}') else: print(f'{fp}: {orig_time} -> {start_time}') start_time += duration file_time += duration
Modify time stamps of audio files.
Parameters
start_time
:str
- Time stamp of the first file.
file_pathes
:list
ofstr
- Pathes of audio files.
no_mod
:bool
- Do not modify the files, just report what would be done.
def main(*cargs)
-
Expand source code
def main(*cargs): """Call demo with command line arguments. Parameters ---------- cargs: list of strings Command line arguments as provided by sys.argv[1:] """ # command line arguments: parser = argparse.ArgumentParser(add_help=True, description='Fix time stamps.', epilog=f'version {__version__} by Benda-Lab (2020-{__year__})') parser.add_argument('--version', action='version', version=__version__) parser.add_argument('-s', dest='starttime', default=None, type=str, required=True, help='new start time of the first file') parser.add_argument('-n', dest='nomod', action='store_true', help='do not modify the files, just report what would be done.') parser.add_argument('files', type=str, nargs='+', help='audio files') if len(cargs) == 0: cargs = None args = parser.parse_args(cargs) demo(args.starttime, args.files, args.nomod)
Call demo with command line arguments.
Parameters
cargs
:list
ofstrings
- Command line arguments as provided by sys.argv[1:]