Skip to content

Commit 6dcd0a7

Browse files
committed
Add first ideas
1 parent 5f4a293 commit 6dcd0a7

File tree

3 files changed

+1317
-69
lines changed

3 files changed

+1317
-69
lines changed

audinterface/core/process.py

Lines changed: 172 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -248,51 +248,83 @@ def _process_file(
248248
) -> typing.Tuple[
249249
typing.List[typing.Any],
250250
typing.List[str],
251-
typing.List[pd.Timedelta],
252-
typing.List[pd.Timedelta],
251+
typing.Optional[typing.List[pd.Timedelta]],
252+
typing.Optional[typing.List[pd.Timedelta]],
253253
]:
254+
r"""Process a file.
255+
256+
Args:
257+
file: file path
258+
root: optional root path of file
259+
start: start time to read media file
260+
end: end time to read media file
261+
process_func_args: arguments to pass to process function
262+
263+
Returns:
264+
result of processing function, files, starts, ends
265+
266+
"""
254267
if start is not None:
255268
start = utils.to_timedelta(start, self.sampling_rate)
256269
if end is not None:
257270
end = utils.to_timedelta(end, self.sampling_rate)
258271

259-
signal, sampling_rate = utils.read_audio(
260-
file,
261-
start=start,
262-
end=end,
263-
root=root,
264-
)
272+
ext = audeer.file_extension(file).lower()
265273

266-
y, files, starts, ends = self._process_signal(
267-
signal,
268-
sampling_rate,
269-
idx=idx,
270-
root=root,
271-
file=file,
272-
process_func_args=process_func_args,
273-
)
274-
275-
def precision_offset(duration, sampling_rate):
276-
# Ensure we get the same precision
277-
# by storing what is lost due to rounding
278-
# when reading the file
279-
duration_at_sample = utils.to_timedelta(
280-
audmath.samples(duration.total_seconds(), sampling_rate) / sampling_rate
274+
# Text files
275+
if ext in ["json", "txt"]:
276+
data = utils.read_text(file, root=root)
277+
y = self._call_data(
278+
data,
279+
idx=idx,
280+
root=root,
281+
file=file,
282+
process_func_args=process_func_args,
281283
)
282-
return duration - duration_at_sample
284+
files = [file]
285+
starts = None
286+
ends = None
283287

284-
if self.win_dur is not None:
285-
if start is not None:
286-
starts = starts + start
287-
ends = ends + start
288+
# Audio/video files
288289
else:
289-
if start is not None and not pd.isna(start):
290-
starts[0] += start
291-
ends[0] += start - precision_offset(start, sampling_rate)
292-
if self.keep_nat and (end is None or pd.isna(end)):
293-
ends[0] = pd.NaT
294-
if end is not None and not pd.isna(end):
295-
ends[-1] += precision_offset(end, sampling_rate)
290+
signal, sampling_rate = utils.read_audio(
291+
file,
292+
start=start,
293+
end=end,
294+
root=root,
295+
)
296+
297+
y, files, starts, ends = self._process_signal(
298+
signal,
299+
sampling_rate,
300+
idx=idx,
301+
root=root,
302+
file=file,
303+
process_func_args=process_func_args,
304+
)
305+
306+
def precision_offset(duration, sampling_rate):
307+
# Ensure we get the same precision
308+
# by storing what is lost due to rounding
309+
# when reading the file
310+
duration_at_sample = utils.to_timedelta(
311+
audmath.samples(duration.total_seconds(), sampling_rate)
312+
/ sampling_rate
313+
)
314+
return duration - duration_at_sample
315+
316+
if self.win_dur is not None:
317+
if start is not None:
318+
starts = starts + start
319+
ends = ends + start
320+
else:
321+
if start is not None and not pd.isna(start):
322+
starts[0] += start
323+
ends[0] += start - precision_offset(start, sampling_rate)
324+
if self.keep_nat and (end is None or pd.isna(end)):
325+
ends[0] = pd.NaT
326+
if end is not None and not pd.isna(end):
327+
ends[-1] += precision_offset(end, sampling_rate)
296328

297329
return y, files, starts, ends
298330

@@ -348,7 +380,6 @@ def process_file(
348380
end=end,
349381
process_func_args=process_func_args,
350382
)
351-
352383
index = audformat.segmented_index(files, starts, ends)
353384

354385
if len(y) == 0:
@@ -714,7 +745,7 @@ def _process_signal(
714745
def process_signal(
715746
self,
716747
signal: np.ndarray,
717-
sampling_rate: int,
748+
sampling_rate: int = None,
718749
*,
719750
file: str = None,
720751
start: Timestamp = None,
@@ -768,24 +799,31 @@ def process_signal(
768799
process_func_args=process_func_args,
769800
)
770801
else:
771-
if start is not None:
772-
start = utils.to_timedelta(start, sampling_rate)
773-
if end is not None:
774-
end = utils.to_timedelta(end, sampling_rate)
775-
776-
y, files, starts, ends = self._process_signal(
777-
signal,
778-
sampling_rate,
779-
file=file,
780-
start=start,
781-
end=end,
782-
process_func_args=process_func_args,
783-
)
802+
# Text files
803+
if sampling_rate is None:
804+
pass
805+
# Implement
784806

785-
if file is not None:
786-
index = audformat.segmented_index(files, starts, ends)
807+
# Audio/video files
787808
else:
788-
index = utils.signal_index(starts, ends)
809+
if start is not None:
810+
start = utils.to_timedelta(start, sampling_rate)
811+
if end is not None:
812+
end = utils.to_timedelta(end, sampling_rate)
813+
814+
y, files, starts, ends = self._process_signal(
815+
signal,
816+
sampling_rate,
817+
file=file,
818+
start=start,
819+
end=end,
820+
process_func_args=process_func_args,
821+
)
822+
823+
if file is not None:
824+
index = audformat.segmented_index(files, starts, ends)
825+
else:
826+
index = utils.signal_index(starts, ends)
789827

790828
if len(y) == 0:
791829
return pd.Series([], index, dtype=object)
@@ -920,7 +958,28 @@ def _call(
920958
file: str = None,
921959
process_func_args: typing.Dict[str, typing.Any] = None,
922960
) -> typing.Any:
923-
r"""Call processing function, possibly pass special args."""
961+
r"""Call processing function on audio/video files.
962+
963+
Assumes a ``numpy`` array as signal,
964+
with channels and samples as dimensions.
965+
The signal is resampled and/or remixed,
966+
if required.
967+
968+
Special arguments are extracted,
969+
and passed to the processing function.
970+
971+
Args:
972+
signal: signal values
973+
sampling_rate: sampling rate in Hz
974+
idx: index
975+
root: root path
976+
file: file path
977+
process_func_args: processing function arguments
978+
979+
Returns:
980+
result of processing function
981+
982+
"""
924983
signal, sampling_rate = utils.preprocess_signal(
925984
signal,
926985
sampling_rate,
@@ -931,14 +990,7 @@ def _call(
931990
)
932991

933992
process_func_args = process_func_args or self.process_func_args
934-
special_args = {}
935-
for key, value in [
936-
("idx", idx),
937-
("root", root),
938-
("file", file),
939-
]:
940-
if key in self._process_func_signature and key not in process_func_args:
941-
special_args[key] = value
993+
special_args = self._special_args(idx, root, file, process_func_args)
942994

943995
def _helper(x):
944996
if self.process_func_is_mono:
@@ -973,18 +1025,66 @@ def _helper(x):
9731025

9741026
return y
9751027

1028+
def _call_data(
1029+
self,
1030+
data: typing.Any,
1031+
*,
1032+
idx: int = 0,
1033+
root: str = None,
1034+
file: str = None,
1035+
process_func_args: typing.Dict[str, typing.Any] = None,
1036+
) -> typing.Any:
1037+
r"""Call processing function on general data."""
1038+
process_func_args = process_func_args or self.process_func_args
1039+
special_args = self._special_args(idx, root, file, process_func_args)
1040+
y = self.process_func(data, **special_args, **process_func_args)
1041+
return y
1042+
1043+
def _special_args(
1044+
self,
1045+
idx: int,
1046+
root: typing.Optional[str],
1047+
file: typing.Optional[str],
1048+
process_func_args: typing.Dict[str, typing.Any] = None,
1049+
) -> typing.Dict[str, typing.Union[int, str]]:
1050+
r"""Identify special arguments in processing function.
1051+
1052+
If one of the arguments of the processing function is named
1053+
``"idx"``, ``"root"``, or ``"file"``,
1054+
and not provided in ``process_func_args``,
1055+
it is identified as a special argument.
1056+
1057+
Args:
1058+
idx: index
1059+
root: root path
1060+
file: file path
1061+
process_func_args: processing function arguments
1062+
1063+
Returns:
1064+
special arguments dictionary
1065+
1066+
"""
1067+
special_args = {}
1068+
for key, value in [("idx", idx), ("root", root), ("file", file)]:
1069+
if key in self._process_func_signature and key not in process_func_args:
1070+
special_args[key] = value
1071+
return special_args
1072+
9761073
def __call__(
9771074
self,
9781075
signal: np.ndarray,
979-
sampling_rate: int,
1076+
sampling_rate: int = None,
9801077
) -> typing.Any:
9811078
r"""Apply processing to signal.
9821079
983-
This function processes the signal **without** transforming the output
984-
into a :class:`pd.Series`. Instead, it will return the raw processed
985-
signal. However, if channel selection, mixdown and/or resampling
986-
is enabled, the signal will be first remixed and resampled if the
987-
input sampling rate does not fit the expected sampling rate.
1080+
This function processes the signal
1081+
**without** transforming the output into a :class:`pd.Series`.
1082+
Instead, it will return the raw processed signal.
1083+
However,
1084+
if channel selection, mixdown and/or resampling is enabled,
1085+
and ``sampling_rate`` is not ``None``,
1086+
the signal will be first remixed and resampled
1087+
if the input sampling rate does not fit the expected sampling rate.
9881088
9891089
Args:
9901090
signal: signal values
@@ -998,4 +1098,7 @@ def __call__(
9981098
RuntimeError: if channel selection is invalid
9991099
10001100
"""
1001-
return self._call(signal, sampling_rate)
1101+
if sampling_rate is not None:
1102+
return self._call(signal, sampling_rate)
1103+
else:
1104+
return self._call_data(signal)

audinterface/core/utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import collections
2+
import json
23
import os
34
import typing
45

@@ -148,6 +149,37 @@ def read_audio(
148149
return signal, sampling_rate
149150

150151

152+
def read_text(
153+
file: str,
154+
*,
155+
root: str = None,
156+
) -> typing.Union[dict, str]:
157+
"""Reads text file.
158+
159+
Args:
160+
file: path to audio file
161+
root: root folder
162+
163+
Returns:
164+
dictionary with values,
165+
if ``file`` is a json file,
166+
else content of file as string
167+
168+
"""
169+
if root is not None and not os.path.isabs(file):
170+
file = os.path.join(root, file)
171+
172+
ext = audeer.file_extension(file).lower()
173+
if ext == "json":
174+
with open(file) as json_file:
175+
data = json.load(f)
176+
elif ext == "txt":
177+
with open(file) as txt_file:
178+
data = txt_file.read()
179+
180+
return data
181+
182+
151183
def segment_to_indices(
152184
signal: np.ndarray,
153185
sampling_rate: int,

0 commit comments

Comments
 (0)