[ci] add pip install to readthedocs.yaml
[aubio.git] / python / lib / aubio / slicing.py
1 """utility routines to slice sound files at given timestamps"""
2
3 import os
4 from aubio import source, sink
5
6 _max_timestamp = 1e120
7
8
9 def slice_source_at_stamps(source_file, timestamps, timestamps_end=None,
10                            output_dir=None, samplerate=0, hopsize=256,
11                            create_first=False):
12     """Slice a sound file at given timestamps.
13
14     This function reads `source_file` and creates slices, new smaller
15     files each starting at `t` in `timestamps`, a list of integer
16     corresponding to time locations in `source_file`, in samples.
17
18     If `timestamps_end` is unspecified, the slices will end at
19     `timestamps_end[n] = timestamps[n+1]-1`, or the end of file.
20     Otherwise, `timestamps_end` should be a list with the same length
21     as `timestamps` containing the locations of the end of each slice.
22
23     If `output_dir` is unspecified, the new slices will be written in
24     the current directory. If `output_dir` is a string, new slices
25     will be written in `output_dir`, after creating the directory if
26     required.
27
28     The default `samplerate` is 0, meaning the original sampling rate
29     of `source_file` will be used. When using a sampling rate
30     different to the one of the original files, `timestamps` and
31     `timestamps_end` should be expressed in the re-sampled signal.
32
33     The `hopsize` parameter simply tells :class:`source` to use this
34     hopsize and does not change the output slices.
35
36     If `create_first` is True and `timestamps` does not start with `0`, the
37     first slice from `0` to `timestamps[0] - 1` will be automatically added.
38
39     Parameters
40     ----------
41     source_file : str
42         path of the resource to slice
43     timestamps : :obj:`list` of :obj:`int`
44         time stamps at which to slice, in samples
45     timestamps_end : :obj:`list` of :obj:`int` (optional)
46         time stamps at which to end the slices
47     output_dir : str (optional)
48         output directory to write the slices to
49     samplerate : int (optional)
50         samplerate to read the file at
51     hopsize : int (optional)
52         number of samples read from source per iteration
53     create_first : bool (optional)
54         always create the slice at the start of the file
55
56     Examples
57     --------
58     Create two slices: the first slice starts at the beginning of the
59     input file `loop.wav` and lasts exactly one second, starting at
60     sample `0` and ending at sample `44099`; the second slice starts
61     at sample `44100` and lasts until the end of the input file:
62
63     >>> aubio.slice_source_at_stamps('loop.wav', [0, 44100])
64
65     Create one slice, from 1 second to 2 seconds:
66
67     >>> aubio.slice_source_at_stamps('loop.wav', [44100], [44100 * 2 - 1])
68
69     Notes
70     -----
71     Slices may be overlapping. If `timestamps_end` is `1` element
72     shorter than `timestamps`, the last slice will end at the end of
73     the file.
74     """
75
76     if not timestamps:
77         raise ValueError("no timestamps given")
78
79     if timestamps[0] != 0 and create_first:
80         timestamps = [0] + timestamps
81         if timestamps_end is not None:
82             timestamps_end = [timestamps[1] - 1] + timestamps_end
83
84     if timestamps_end is not None:
85         if len(timestamps_end) == len(timestamps) - 1:
86             timestamps_end = timestamps_end + [_max_timestamp]
87         elif len(timestamps_end) != len(timestamps):
88             raise ValueError("len(timestamps_end) != len(timestamps)")
89     else:
90         timestamps_end = [t - 1 for t in timestamps[1:]] + [_max_timestamp]
91
92     regions = list(zip(timestamps, timestamps_end))
93
94     source_base_name, _ = os.path.splitext(os.path.basename(source_file))
95     if output_dir is not None:
96         if not os.path.isdir(output_dir):
97             os.makedirs(output_dir)
98         source_base_name = os.path.join(output_dir, source_base_name)
99
100     def _new_sink_name(source_base_name, timestamp, samplerate):
101         # create name based on a timestamp in samples, converted in seconds
102         timestamp_seconds = timestamp / float(samplerate)
103         return source_base_name + "_%011.6f" % timestamp_seconds + '.wav'
104
105     # open source file
106     _source = source(source_file, samplerate, hopsize)
107     samplerate = _source.samplerate
108
109     total_frames = 0
110     slices = []
111
112     while True:
113         # get hopsize new samples from source
114         vec, read = _source.do_multi()
115         # if the total number of frames read will exceed the next region start
116         while regions and total_frames + read >= regions[0][0]:
117             # get next region
118             start_stamp, end_stamp = regions.pop(0)
119             # create a name for the sink
120             new_sink_path = _new_sink_name(source_base_name, start_stamp,
121                                            samplerate)
122             # create its sink
123             _sink = sink(new_sink_path, samplerate, _source.channels)
124             # create a dictionary containing all this
125             new_slice = {'start_stamp': start_stamp, 'end_stamp': end_stamp,
126                          'sink': _sink}
127             # append the dictionary to the current list of slices
128             slices.append(new_slice)
129
130         for current_slice in slices:
131             start_stamp = current_slice['start_stamp']
132             end_stamp = current_slice['end_stamp']
133             _sink = current_slice['sink']
134             # sample index to start writing from new source vector
135             start = max(start_stamp - total_frames, 0)
136             # number of samples yet to written be until end of region
137             remaining = end_stamp - total_frames + 1
138             # not enough frames remaining, time to split
139             if remaining < read:
140                 if remaining > start:
141                     # write remaining samples from current region
142                     _sink.do_multi(vec[:, start:remaining], remaining - start)
143                     # close this file
144                     _sink.close()
145             elif read > start:
146                 # write all the samples
147                 _sink.do_multi(vec[:, start:read], read - start)
148         total_frames += read
149         # remove old slices
150         slices = list(filter(lambda s: s['end_stamp'] > total_frames,
151                              slices))
152         if read < hopsize:
153             break