recs 0.3.1__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- recs/.DS_Store +0 -0
- recs/audio/.DS_Store +0 -0
- recs/audio/block.py +6 -4
- recs/audio/channel_writer.py +98 -54
- recs/audio/file_opener.py +22 -20
- recs/audio/header_size.py +7 -7
- recs/base/.DS_Store +0 -0
- recs/base/_query_device.py +3 -3
- recs/base/cfg_raw.py +5 -4
- recs/base/pyproject.py +1 -1
- recs/base/type_conversions.py +0 -9
- recs/base/types.py +2 -34
- recs/cfg/.DS_Store +0 -0
- recs/cfg/__init__.py +4 -11
- recs/cfg/aliases.py +13 -17
- recs/cfg/app.py +7 -5
- recs/cfg/cfg.py +24 -12
- recs/cfg/cli.py +37 -18
- recs/cfg/device.py +30 -42
- recs/cfg/file_source.py +61 -0
- recs/cfg/hash_cmp.py +2 -2
- recs/cfg/metadata.py +2 -5
- recs/cfg/path_pattern.py +13 -5
- recs/cfg/run_cli.py +8 -17
- recs/cfg/source.py +42 -0
- recs/cfg/time_settings.py +4 -1
- recs/cfg/track.py +12 -9
- recs/misc/.DS_Store +0 -0
- recs/misc/__init__.py +0 -1
- recs/misc/contexts.py +1 -1
- recs/misc/counter.py +12 -4
- recs/misc/file_list.py +1 -1
- recs/misc/log.py +5 -5
- recs/ui/.DS_Store +0 -0
- recs/ui/full_state.py +11 -6
- recs/ui/live.py +9 -9
- recs/ui/recorder.py +39 -48
- recs/ui/source_recorder.py +76 -0
- recs/ui/{device_tracks.py → source_tracks.py} +19 -17
- recs/ui/table.py +2 -2
- {recs-0.3.1.dist-info → recs-0.11.0.dist-info}/METADATA +14 -14
- recs-0.11.0.dist-info/RECORD +53 -0
- {recs-0.3.1.dist-info → recs-0.11.0.dist-info}/WHEEL +1 -1
- recs-0.11.0.dist-info/entry_points.txt +3 -0
- recs/ui/device_process.py +0 -37
- recs/ui/device_recorder.py +0 -83
- recs-0.3.1.dist-info/LICENSE +0 -21
- recs-0.3.1.dist-info/RECORD +0 -47
- recs-0.3.1.dist-info/entry_points.txt +0 -3
recs/cfg/app.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
1
3
|
import click
|
|
2
4
|
import dtyper
|
|
3
5
|
|
|
@@ -30,7 +32,7 @@ app = dtyper.Typer(
|
|
|
30
32
|
class TimeParam(click.ParamType):
|
|
31
33
|
name = 'TIME'
|
|
32
34
|
|
|
33
|
-
def convert(self, value, p, ctx) -> float:
|
|
35
|
+
def convert(self, value: t.Any, p: t.Any, ctx: t.Any) -> float:
|
|
34
36
|
if isinstance(value, (int, float)):
|
|
35
37
|
return value
|
|
36
38
|
try:
|
|
@@ -41,9 +43,9 @@ class TimeParam(click.ParamType):
|
|
|
41
43
|
|
|
42
44
|
class DictParam(click.ParamType):
|
|
43
45
|
name = 'NONE'
|
|
44
|
-
prefix_dict: dict
|
|
46
|
+
prefix_dict: dict[str, t.Any]
|
|
45
47
|
|
|
46
|
-
def convert(self, value, p, ctx):
|
|
48
|
+
def convert(self, value: t.Any, p: t.Any, ctx: t.Any) -> t.Any:
|
|
47
49
|
if not value:
|
|
48
50
|
return None
|
|
49
51
|
if not isinstance(value, str):
|
|
@@ -72,7 +74,7 @@ class SubtypeParam(DictParam):
|
|
|
72
74
|
class MetadataParam(click.ParamType):
|
|
73
75
|
name = 'METADATA'
|
|
74
76
|
|
|
75
|
-
def convert(self, value, p, ctx):
|
|
77
|
+
def convert(self, value: t.Any, p: t.Any, ctx: t.Any) -> t.Any:
|
|
76
78
|
try:
|
|
77
79
|
metadata.to_dict([value])
|
|
78
80
|
return value
|
|
@@ -83,5 +85,5 @@ class MetadataParam(click.ParamType):
|
|
|
83
85
|
class AliasParam(click.ParamType):
|
|
84
86
|
name = 'ALIAS'
|
|
85
87
|
|
|
86
|
-
def convert(self, value, p, ctx):
|
|
88
|
+
def convert(self, value: t.Any, p: t.Any, ctx: t.Any) -> t.Any:
|
|
87
89
|
return value
|
recs/cfg/cfg.py
CHANGED
|
@@ -4,11 +4,13 @@ import logging
|
|
|
4
4
|
import typing as t
|
|
5
5
|
import warnings
|
|
6
6
|
from functools import wraps
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
|
-
import soundfile
|
|
9
|
+
import soundfile
|
|
9
10
|
|
|
10
11
|
from recs.base import RecsError
|
|
11
12
|
from recs.base.cfg_raw import CfgRaw
|
|
13
|
+
from recs.base.prefix_dict import PrefixDict
|
|
12
14
|
from recs.base.type_conversions import SDTYPE_TO_SUBTYPE, SUBTYPE_TO_SDTYPE
|
|
13
15
|
from recs.base.types import SDTYPE, Format, SdType, Subtype
|
|
14
16
|
from recs.misc import log
|
|
@@ -19,12 +21,12 @@ from .aliases import Aliases
|
|
|
19
21
|
|
|
20
22
|
class Cfg:
|
|
21
23
|
devices: device.InputDevices
|
|
22
|
-
|
|
24
|
+
formats: t.Sequence[Format]
|
|
23
25
|
subtype: Subtype | None
|
|
24
26
|
sdtype: SdType
|
|
25
27
|
|
|
26
28
|
@wraps(CfgRaw.__init__)
|
|
27
|
-
def __init__(self, *a, **ka) -> None:
|
|
29
|
+
def __init__(self, *a: t.Any, **ka: t.Any) -> None:
|
|
28
30
|
self.cfg = cfg = CfgRaw(*a, **ka)
|
|
29
31
|
|
|
30
32
|
# This constructor has this *global side-effect*, see log.py
|
|
@@ -32,9 +34,16 @@ class Cfg:
|
|
|
32
34
|
if cfg.verbose:
|
|
33
35
|
logging.basicConfig(level=logging.DEBUG)
|
|
34
36
|
|
|
35
|
-
self.
|
|
37
|
+
self.output_directory = path_pattern.PathPattern(cfg.output_directory)
|
|
36
38
|
|
|
37
|
-
self.
|
|
39
|
+
self.files = [Path(f) for f in cfg.files or ()]
|
|
40
|
+
if not_exist := [f for f in self.files if not f.exists()]:
|
|
41
|
+
s = 's' * (len(not_exist) != 1)
|
|
42
|
+
fname = ', '.join(str(f) for f in not_exist)
|
|
43
|
+
raise RecsError(f'Non-existent file{s}: {fname}')
|
|
44
|
+
|
|
45
|
+
assert not isinstance(cfg.formats, str)
|
|
46
|
+
self.formats = [Format(f) for f in cfg.formats] or [Format._default]
|
|
38
47
|
|
|
39
48
|
if cfg.subtype:
|
|
40
49
|
self.subtype = t.cast(Subtype, cfg.subtype)
|
|
@@ -43,15 +52,15 @@ class Cfg:
|
|
|
43
52
|
else:
|
|
44
53
|
subtype = SDTYPE_TO_SUBTYPE[t.cast(SdType, cfg.sdtype)]
|
|
45
54
|
|
|
46
|
-
if
|
|
55
|
+
if soundfile.check_format(self.formats[0], subtype):
|
|
47
56
|
self.subtype = subtype
|
|
48
57
|
else:
|
|
49
58
|
self.subtype = None
|
|
50
|
-
msg = f'
|
|
51
|
-
warnings.warn(f"Can't get subtype for {msg}")
|
|
59
|
+
msg = f'formats={self.formats[0]:s}, sdtype={cfg.sdtype:s}'
|
|
60
|
+
warnings.warn(f"Can't get subtype for {msg}", stacklevel=2)
|
|
52
61
|
|
|
53
|
-
if self.subtype and not
|
|
54
|
-
raise RecsError(f'{self.
|
|
62
|
+
if self.subtype and not soundfile.check_format(self.formats[0], self.subtype):
|
|
63
|
+
raise RecsError(f'{self.formats[0]} and {self.subtype} are incompatible')
|
|
55
64
|
|
|
56
65
|
if cfg.sdtype:
|
|
57
66
|
self.sdtype = t.cast(SdType, cfg.sdtype)
|
|
@@ -60,7 +69,10 @@ class Cfg:
|
|
|
60
69
|
else:
|
|
61
70
|
self.sdtype = SDTYPE
|
|
62
71
|
|
|
63
|
-
if
|
|
72
|
+
if self.files:
|
|
73
|
+
self.devices = PrefixDict()
|
|
74
|
+
|
|
75
|
+
elif cfg.devices.name:
|
|
64
76
|
if not cfg.devices.exists():
|
|
65
77
|
raise RecsError(f'{cfg.devices} does not exist')
|
|
66
78
|
devices = json.loads(cfg.devices.read_text())
|
|
@@ -77,7 +89,7 @@ class Cfg:
|
|
|
77
89
|
def __getattr__(self, k: str) -> t.Any:
|
|
78
90
|
return getattr(self.cfg, k)
|
|
79
91
|
|
|
80
|
-
def _times(self) -> time_settings.TimeSettings:
|
|
92
|
+
def _times(self) -> time_settings.TimeSettings[float]:
|
|
81
93
|
fields = (f.name for f in dc.fields(time_settings.TimeSettings))
|
|
82
94
|
d = {k: getattr(self, k) for k in fields}
|
|
83
95
|
return time_settings.TimeSettings(**d)
|
recs/cfg/cli.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import string
|
|
2
|
+
import typing as t
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
import dtyper
|
|
5
|
-
from typer import
|
|
6
|
+
from typer import rich_utils
|
|
6
7
|
|
|
7
8
|
from recs.base import types
|
|
8
9
|
from recs.base.cfg_raw import CfgRaw
|
|
@@ -18,7 +19,7 @@ RECS = CfgRaw()
|
|
|
18
19
|
# Reading configs and environment variables would go here
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def Option(default, *a, **ka) ->
|
|
22
|
+
def Option(default: t.Any, *a: t.Any, **ka: t.Any) -> t.Any:
|
|
22
23
|
_SINGLES.update(i[1] for i in a if len(i) == 2)
|
|
23
24
|
return dtyper.Option(default, *a, **ka)
|
|
24
25
|
|
|
@@ -33,11 +34,17 @@ RECORD_PANEL = 'Record Settings'
|
|
|
33
34
|
|
|
34
35
|
@app.app.command(help=app.HELP)
|
|
35
36
|
def recs(
|
|
37
|
+
files: list[str] = dtyper.Argument(
|
|
38
|
+
None, help='One or more files to split for silence'
|
|
39
|
+
),
|
|
36
40
|
#
|
|
37
41
|
# Directory settings
|
|
38
42
|
#
|
|
39
|
-
|
|
40
|
-
RECS.
|
|
43
|
+
output_directory: str = Option(
|
|
44
|
+
RECS.output_directory,
|
|
45
|
+
'-o',
|
|
46
|
+
'--output-directory',
|
|
47
|
+
help='Path or output_directory pattern for recorded file locations',
|
|
41
48
|
),
|
|
42
49
|
#
|
|
43
50
|
# General
|
|
@@ -59,7 +66,7 @@ def recs(
|
|
|
59
66
|
RECS.verbose,
|
|
60
67
|
'-v',
|
|
61
68
|
'--verbose',
|
|
62
|
-
help='Print more stuff
|
|
69
|
+
help='Print more stuff',
|
|
63
70
|
rich_help_panel=GENERAL_PANEL,
|
|
64
71
|
),
|
|
65
72
|
#
|
|
@@ -110,12 +117,12 @@ def recs(
|
|
|
110
117
|
#
|
|
111
118
|
# File
|
|
112
119
|
#
|
|
113
|
-
|
|
114
|
-
RECS.
|
|
120
|
+
formats: t.List[types.Format] = Option(
|
|
121
|
+
tuple(RECS.formats),
|
|
115
122
|
'-f',
|
|
116
|
-
'--
|
|
123
|
+
'--formats',
|
|
117
124
|
click_type=app.FormatParam(),
|
|
118
|
-
help='Audio file
|
|
125
|
+
help='Audio file formats',
|
|
119
126
|
rich_help_panel=FILE_PANEL,
|
|
120
127
|
),
|
|
121
128
|
metadata: list[str] = Option(
|
|
@@ -126,8 +133,7 @@ def recs(
|
|
|
126
133
|
help='Metadata fields to add to output files',
|
|
127
134
|
rich_help_panel=FILE_PANEL,
|
|
128
135
|
),
|
|
129
|
-
sdtype: types.SdType
|
|
130
|
-
| None = Option(
|
|
136
|
+
sdtype: types.SdType | None = Option(
|
|
131
137
|
RECS.sdtype,
|
|
132
138
|
'-d',
|
|
133
139
|
'--sdtype',
|
|
@@ -135,8 +141,7 @@ def recs(
|
|
|
135
141
|
help='Integer or float number type for recording',
|
|
136
142
|
rich_help_panel=FILE_PANEL,
|
|
137
143
|
),
|
|
138
|
-
subtype: types.Subtype
|
|
139
|
-
| None = Option(
|
|
144
|
+
subtype: types.Subtype | None = Option(
|
|
140
145
|
RECS.subtype,
|
|
141
146
|
'-u',
|
|
142
147
|
'--subtype',
|
|
@@ -177,10 +182,17 @@ def recs(
|
|
|
177
182
|
#
|
|
178
183
|
# Record
|
|
179
184
|
#
|
|
185
|
+
band_mode: bool = Option(
|
|
186
|
+
RECS.band_mode,
|
|
187
|
+
'--band-mode',
|
|
188
|
+
'-B',
|
|
189
|
+
help='Band mode: any track starting starts them all',
|
|
190
|
+
rich_help_panel=RECORD_PANEL,
|
|
191
|
+
),
|
|
180
192
|
infinite_length: bool = Option(
|
|
181
193
|
RECS.infinite_length,
|
|
182
194
|
'--infinite-length',
|
|
183
|
-
help='Ignore file size
|
|
195
|
+
help='Ignore file size limit: 4G on .wav',
|
|
184
196
|
rich_help_panel=RECORD_PANEL,
|
|
185
197
|
),
|
|
186
198
|
longest_file_time: str = Option(
|
|
@@ -197,15 +209,22 @@ def recs(
|
|
|
197
209
|
),
|
|
198
210
|
noise_floor: float = Option(
|
|
199
211
|
RECS.noise_floor,
|
|
200
|
-
'-
|
|
212
|
+
'-z',
|
|
201
213
|
'--noise-floor',
|
|
202
214
|
help='The noise floor in decibels',
|
|
203
215
|
rich_help_panel=RECORD_PANEL,
|
|
204
216
|
),
|
|
217
|
+
record_everything: bool = Option(
|
|
218
|
+
RECS.record_everything,
|
|
219
|
+
'-R',
|
|
220
|
+
'--record-everything',
|
|
221
|
+
help='Start immediately, record everything until end',
|
|
222
|
+
rich_help_panel=RECORD_PANEL,
|
|
223
|
+
),
|
|
205
224
|
shortest_file_time: str = Option(
|
|
206
225
|
RECS.shortest_file_time,
|
|
207
226
|
click_type=app.TimeParam(),
|
|
208
|
-
help='
|
|
227
|
+
help='Files shorter than this duration get deleted',
|
|
209
228
|
rich_help_panel=RECORD_PANEL,
|
|
210
229
|
),
|
|
211
230
|
quiet_after_end: str = Option(
|
|
@@ -250,5 +269,5 @@ def recs(
|
|
|
250
269
|
_USED_SINGLES = ''.join(sorted(_SINGLES))
|
|
251
270
|
_UNUSED_SINGLES = ''.join(sorted(set(string.ascii_lowercase) - set(_SINGLES)))
|
|
252
271
|
|
|
253
|
-
assert _USED_SINGLES == '
|
|
254
|
-
assert _UNUSED_SINGLES == '
|
|
272
|
+
assert _USED_SINGLES == 'BRabcdefimnorstuvz', _USED_SINGLES
|
|
273
|
+
assert _UNUSED_SINGLES == 'ghjklpqwxy', _UNUSED_SINGLES
|
recs/cfg/device.py
CHANGED
|
@@ -5,72 +5,60 @@ import traceback
|
|
|
5
5
|
import typing as t
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
|
+
from overrides import override
|
|
9
|
+
from threa import Runnable, Wrapper
|
|
8
10
|
|
|
9
11
|
from recs.base import times
|
|
10
12
|
from recs.base.prefix_dict import PrefixDict
|
|
11
|
-
from recs.base.types import
|
|
12
|
-
from recs.cfg import hash_cmp
|
|
13
|
+
from recs.base.types import SdType
|
|
13
14
|
|
|
15
|
+
from .source import Source, Update
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
array: np.ndarray
|
|
17
|
-
timestamp: float
|
|
17
|
+
DeviceDict = dict[str, float | int | str]
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class InputStream(t.Protocol):
|
|
24
|
-
def close(self, ignore_errors=True) -> None:
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
def start(self) -> None:
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
def stop(self, ignore_errors=True) -> None:
|
|
31
|
-
pass
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class InputDevice(hash_cmp.HashCmp):
|
|
20
|
+
class InputDevice(Source):
|
|
35
21
|
def __init__(self, info: DeviceDict) -> None:
|
|
36
22
|
self.info = info
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def __str__(self) -> str:
|
|
43
|
-
return self.name
|
|
23
|
+
super().__init__(
|
|
24
|
+
channels=t.cast(int, self.info['max_input_channels']),
|
|
25
|
+
name=t.cast(str, self.info['name']),
|
|
26
|
+
samplerate=int(self.info['default_samplerate']),
|
|
27
|
+
)
|
|
44
28
|
|
|
29
|
+
@override
|
|
45
30
|
def input_stream(
|
|
46
|
-
self,
|
|
47
|
-
) ->
|
|
48
|
-
import sounddevice
|
|
49
|
-
|
|
50
|
-
stream:
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
self, sdtype: SdType, update_callback: t.Callable[[Update], None]
|
|
32
|
+
) -> Runnable:
|
|
33
|
+
import sounddevice
|
|
34
|
+
|
|
35
|
+
stream: sounddevice.InputStream
|
|
36
|
+
result: Wrapper
|
|
37
|
+
|
|
38
|
+
def callback(
|
|
39
|
+
indata: np.ndarray, # type: ignore[type-arg]
|
|
40
|
+
frames: int,
|
|
41
|
+
time: t.Any,
|
|
42
|
+
status: int,
|
|
43
|
+
) -> None:
|
|
53
44
|
if status: # pragma: no cover
|
|
54
45
|
print('Status', self, status, file=sys.stderr)
|
|
55
46
|
|
|
56
47
|
try:
|
|
57
|
-
|
|
48
|
+
update_callback(Update(indata.copy(), times.timestamp()))
|
|
58
49
|
|
|
59
50
|
except Exception: # pragma: no cover
|
|
60
51
|
traceback.print_exc()
|
|
61
|
-
|
|
62
|
-
on_error()
|
|
63
|
-
except Exception:
|
|
64
|
-
traceback.print_exc()
|
|
52
|
+
result.stop()
|
|
65
53
|
|
|
66
|
-
stream =
|
|
54
|
+
stream = sounddevice.InputStream(
|
|
67
55
|
callback=callback,
|
|
68
56
|
channels=self.channels,
|
|
69
57
|
device=self.name,
|
|
70
58
|
dtype=sdtype,
|
|
71
59
|
samplerate=self.samplerate,
|
|
72
60
|
)
|
|
73
|
-
return stream
|
|
61
|
+
return (result := Wrapper(stream))
|
|
74
62
|
|
|
75
63
|
|
|
76
64
|
InputDevices = PrefixDict[InputDevice]
|
|
@@ -88,7 +76,7 @@ def query_devices() -> t.Sequence[DeviceDict]:
|
|
|
88
76
|
r = sp.run(CMD, text=True, check=True, stdout=sp.PIPE)
|
|
89
77
|
except sp.CalledProcessError:
|
|
90
78
|
return []
|
|
91
|
-
return json.loads(r.stdout)
|
|
79
|
+
return t.cast(list[DeviceDict], json.loads(r.stdout))
|
|
92
80
|
|
|
93
81
|
|
|
94
82
|
def input_names() -> t.Sequence[str]:
|
recs/cfg/file_source.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import typing as t
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import soundfile
|
|
6
|
+
from overrides import override
|
|
7
|
+
from threa import HasThread, Runnable
|
|
8
|
+
|
|
9
|
+
from recs.base.types import Format, SdType, Subtype
|
|
10
|
+
|
|
11
|
+
from .source import Source, Update, to_matrix
|
|
12
|
+
|
|
13
|
+
BLOCKSIZE = 0x1000
|
|
14
|
+
BLOCKCOUNT = 0x1000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileSource(Source):
|
|
18
|
+
def __init__(self, path: Path) -> None:
|
|
19
|
+
self.path = path
|
|
20
|
+
assert self.path.exists()
|
|
21
|
+
|
|
22
|
+
with self._stream() as fp:
|
|
23
|
+
self.format = Format(fp.format.lower())
|
|
24
|
+
self.subtype = Subtype(fp.subtype.lower())
|
|
25
|
+
super().__init__(
|
|
26
|
+
channels=fp.channels,
|
|
27
|
+
format=self.format,
|
|
28
|
+
name=str(path),
|
|
29
|
+
samplerate=int(fp.samplerate),
|
|
30
|
+
subtype=self.subtype,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _stream(self) -> soundfile.SoundFile:
|
|
34
|
+
return soundfile.SoundFile(file=self.path, mode='r')
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
def input_stream(
|
|
38
|
+
self, sdtype: SdType, update_callback: t.Callable[[Update], None]
|
|
39
|
+
) -> Runnable:
|
|
40
|
+
result: Runnable
|
|
41
|
+
|
|
42
|
+
def input_stream() -> None:
|
|
43
|
+
try:
|
|
44
|
+
with self._stream() as fp:
|
|
45
|
+
timestamp = 0
|
|
46
|
+
for block in fp.blocks(BLOCKSIZE * BLOCKCOUNT):
|
|
47
|
+
block = to_matrix(block)
|
|
48
|
+
for i in range(BLOCKCOUNT):
|
|
49
|
+
array = block[i * BLOCKSIZE : (i + 1) * BLOCKSIZE]
|
|
50
|
+
if not array.size:
|
|
51
|
+
break
|
|
52
|
+
update_callback(Update(array, timestamp / self.samplerate))
|
|
53
|
+
timestamp += BLOCKSIZE
|
|
54
|
+
|
|
55
|
+
except Exception:
|
|
56
|
+
traceback.print_exc()
|
|
57
|
+
|
|
58
|
+
finally:
|
|
59
|
+
result.stop()
|
|
60
|
+
|
|
61
|
+
return (result := HasThread(input_stream))
|
recs/cfg/hash_cmp.py
CHANGED
|
@@ -10,10 +10,10 @@ class HashCmp(ABC):
|
|
|
10
10
|
def __eq__(self, o: t.Any) -> bool:
|
|
11
11
|
return isinstance(o, type(self)) and self._key == o._key
|
|
12
12
|
|
|
13
|
-
def __lt__(self, o) -> bool:
|
|
13
|
+
def __lt__(self, o: t.Any) -> bool:
|
|
14
14
|
if not isinstance(o, type(self)):
|
|
15
15
|
return NotImplemented
|
|
16
|
-
return self._key < o._key
|
|
16
|
+
return bool(self._key < o._key)
|
|
17
17
|
|
|
18
18
|
def __hash__(self) -> int:
|
|
19
19
|
return hash(self._key)
|
recs/cfg/metadata.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
|
|
3
|
-
import soundfile
|
|
3
|
+
import soundfile
|
|
4
4
|
|
|
5
5
|
from recs.base import RecsError, prefix_dict
|
|
6
6
|
from recs.base.types import Format
|
|
@@ -43,17 +43,14 @@ def to_dict(metadata: t.Sequence[str]) -> dict[str, str]:
|
|
|
43
43
|
return result
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def get_metadata(fp:
|
|
46
|
+
def get_metadata(fp: soundfile.SoundFile) -> dict[str, str]:
|
|
47
47
|
return {k: v for k in ALL if (v := getattr(fp, k))}
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
ALLOWS_METADATA = {
|
|
51
|
-
Format.aiff,
|
|
52
|
-
Format.caf,
|
|
53
51
|
Format.flac,
|
|
54
52
|
Format.mp3,
|
|
55
53
|
Format.ogg,
|
|
56
54
|
Format.rf64,
|
|
57
55
|
Format.wav,
|
|
58
|
-
Format.wavex,
|
|
59
56
|
}
|
recs/cfg/path_pattern.py
CHANGED
|
@@ -2,6 +2,7 @@ import re
|
|
|
2
2
|
import string
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import IntEnum, auto
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from recs.base import RecsError
|
|
7
8
|
from recs.cfg import aliases, track
|
|
@@ -26,6 +27,7 @@ class Req(IntEnum):
|
|
|
26
27
|
|
|
27
28
|
class PathPattern:
|
|
28
29
|
def __init__(self, path: str) -> None:
|
|
30
|
+
self.raw_path = path
|
|
29
31
|
str_parts = parse_fields(path)
|
|
30
32
|
time_parts = findall_strftime(path)
|
|
31
33
|
parts = set(time_parts + str_parts)
|
|
@@ -66,23 +68,29 @@ class PathPattern:
|
|
|
66
68
|
def times(self, ts: datetime) -> dict[str, str]:
|
|
67
69
|
return {k: ts.strftime(FIELD_TO_PSTRING[k]) for k in self.strf_parts}
|
|
68
70
|
|
|
69
|
-
def
|
|
71
|
+
def make_path(
|
|
70
72
|
self,
|
|
71
73
|
track: track.Track,
|
|
72
74
|
aliases: aliases.Aliases,
|
|
73
75
|
timestamp: float,
|
|
74
76
|
index: int,
|
|
75
|
-
) ->
|
|
77
|
+
) -> Path:
|
|
78
|
+
if path := getattr(track.source, 'path', None):
|
|
79
|
+
# It's a file!
|
|
80
|
+
if not (parent_directory := Path(self.raw_path)).exists():
|
|
81
|
+
raise FileNotFoundError(f'{parent_directory=}')
|
|
82
|
+
return parent_directory / f'{path.stem}-{index}'
|
|
83
|
+
|
|
76
84
|
ts = datetime.fromtimestamp(timestamp)
|
|
77
85
|
s = ts.strftime(self.path)
|
|
78
|
-
|
|
79
|
-
return s.format(
|
|
86
|
+
p = s.format(
|
|
80
87
|
channel=track.name,
|
|
81
|
-
device=aliases.display_name(track.
|
|
88
|
+
device=aliases.display_name(track.source),
|
|
82
89
|
index=str(index),
|
|
83
90
|
track=aliases.display_name(track, short=False),
|
|
84
91
|
**self.times(ts),
|
|
85
92
|
)
|
|
93
|
+
return Path(p)
|
|
86
94
|
|
|
87
95
|
|
|
88
96
|
DATE = {Req.year, Req.month, Req.day}
|
recs/cfg/run_cli.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
-
import soundfile
|
|
3
|
+
import soundfile
|
|
4
4
|
|
|
5
5
|
from recs.base.types import Format, SdType
|
|
6
6
|
from recs.cfg import device
|
|
@@ -11,26 +11,17 @@ from . import Cfg
|
|
|
11
11
|
|
|
12
12
|
def run_cli(cfg: Cfg) -> None:
|
|
13
13
|
if cfg.info:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
rec = Recorder(cfg)
|
|
20
|
-
try:
|
|
21
|
-
rec.run_recorder()
|
|
22
|
-
finally:
|
|
23
|
-
if cfg.calibrate:
|
|
24
|
-
states = rec.state.state.items()
|
|
25
|
-
d = {j: {k: v.db_range for k, v in u.items()} for j, u in states}
|
|
26
|
-
d2 = {'(all)': rec.state.total.db_range}
|
|
27
|
-
print(json.dumps(d | d2, indent=2))
|
|
14
|
+
_info()
|
|
15
|
+
elif cfg.list_types:
|
|
16
|
+
_list_types()
|
|
17
|
+
else:
|
|
18
|
+
Recorder(cfg).run()
|
|
28
19
|
|
|
29
20
|
|
|
30
21
|
def _list_types() -> None:
|
|
31
|
-
avail =
|
|
22
|
+
avail = soundfile.available_formats()
|
|
32
23
|
fmts = [f.upper() for f in Format]
|
|
33
|
-
formats = {f: [avail[f],
|
|
24
|
+
formats = {f: [avail[f], soundfile.available_subtypes(f)] for f in fmts}
|
|
34
25
|
sdtypes = [str(s) for s in SdType]
|
|
35
26
|
d = {'formats': formats, 'sdtypes': sdtypes}
|
|
36
27
|
|
recs/cfg/source.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from threa import Runnable
|
|
6
|
+
|
|
7
|
+
from recs.base.types import Format, SdType, Subtype
|
|
8
|
+
from recs.cfg import hash_cmp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_matrix(array: np.ndarray) -> np.ndarray: # type: ignore[type-arg]
|
|
12
|
+
return array.reshape(*array.shape, 1) if len(array.shape) == 1 else array
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Update(t.NamedTuple):
|
|
16
|
+
array: np.ndarray # type: ignore[type-arg]
|
|
17
|
+
timestamp: float
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Source(hash_cmp.HashCmp, abc.ABC):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
channels: int,
|
|
24
|
+
name: str,
|
|
25
|
+
samplerate: int,
|
|
26
|
+
format: Format | None = None,
|
|
27
|
+
subtype: Subtype | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._key = self.name = name
|
|
30
|
+
self.channels = channels
|
|
31
|
+
self.format = format
|
|
32
|
+
self.samplerate = samplerate
|
|
33
|
+
self.subtype = subtype
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
return self.name
|
|
37
|
+
|
|
38
|
+
@abc.abstractmethod
|
|
39
|
+
def input_stream(
|
|
40
|
+
self, sdtype: SdType, update_callback: t.Callable[[Update], None]
|
|
41
|
+
) -> Runnable:
|
|
42
|
+
pass
|
recs/cfg/time_settings.py
CHANGED
|
@@ -44,6 +44,9 @@ class TimeSettings(t.Generic[T]):
|
|
|
44
44
|
#: The noise floor in decibels
|
|
45
45
|
noise_floor: float = 70
|
|
46
46
|
|
|
47
|
+
#: Ignore noise_floor, record everything immediately until the end
|
|
48
|
+
record_everything: bool = False
|
|
49
|
+
|
|
47
50
|
#: Amount of total time to run. 0 or less means "run forever"
|
|
48
51
|
total_run_time: T = t.cast(T, 0)
|
|
49
52
|
|
|
@@ -51,7 +54,7 @@ class TimeSettings(t.Generic[T]):
|
|
|
51
54
|
def noise_floor_amplitude(self) -> float:
|
|
52
55
|
return db_to_amplitude(self.noise_floor)
|
|
53
56
|
|
|
54
|
-
def __post_init__(self):
|
|
57
|
+
def __post_init__(self) -> None:
|
|
55
58
|
if negative_fields := [k for k, v in dc.asdict(self).items() if v < 0]:
|
|
56
59
|
raise ValueError(f'TimeSettings cannot be negative: {negative_fields=}')
|
|
57
60
|
|