recs 0.3.1__py3-none-any.whl → 0.10.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.
Files changed (49) hide show
  1. recs/.DS_Store +0 -0
  2. recs/audio/.DS_Store +0 -0
  3. recs/audio/block.py +6 -4
  4. recs/audio/channel_writer.py +98 -54
  5. recs/audio/file_opener.py +22 -20
  6. recs/audio/header_size.py +7 -7
  7. recs/base/.DS_Store +0 -0
  8. recs/base/_query_device.py +3 -3
  9. recs/base/cfg_raw.py +5 -4
  10. recs/base/pyproject.py +1 -1
  11. recs/base/type_conversions.py +0 -9
  12. recs/base/types.py +2 -34
  13. recs/cfg/.DS_Store +0 -0
  14. recs/cfg/__init__.py +4 -11
  15. recs/cfg/aliases.py +13 -17
  16. recs/cfg/app.py +7 -5
  17. recs/cfg/cfg.py +24 -12
  18. recs/cfg/cli.py +37 -18
  19. recs/cfg/device.py +30 -42
  20. recs/cfg/file_source.py +61 -0
  21. recs/cfg/hash_cmp.py +2 -2
  22. recs/cfg/metadata.py +2 -5
  23. recs/cfg/path_pattern.py +13 -5
  24. recs/cfg/run_cli.py +8 -17
  25. recs/cfg/source.py +42 -0
  26. recs/cfg/time_settings.py +4 -1
  27. recs/cfg/track.py +12 -9
  28. recs/misc/.DS_Store +0 -0
  29. recs/misc/__init__.py +0 -1
  30. recs/misc/contexts.py +1 -1
  31. recs/misc/counter.py +12 -4
  32. recs/misc/file_list.py +1 -1
  33. recs/misc/log.py +5 -5
  34. recs/ui/.DS_Store +0 -0
  35. recs/ui/full_state.py +11 -6
  36. recs/ui/live.py +9 -9
  37. recs/ui/recorder.py +39 -48
  38. recs/ui/source_recorder.py +76 -0
  39. recs/ui/{device_tracks.py → source_tracks.py} +19 -17
  40. recs/ui/table.py +2 -2
  41. {recs-0.3.1.dist-info → recs-0.10.0.dist-info}/METADATA +14 -14
  42. recs-0.10.0.dist-info/RECORD +53 -0
  43. {recs-0.3.1.dist-info → recs-0.10.0.dist-info}/WHEEL +1 -1
  44. recs-0.10.0.dist-info/entry_points.txt +3 -0
  45. recs/ui/device_process.py +0 -37
  46. recs/ui/device_recorder.py +0 -83
  47. recs-0.3.1.dist-info/LICENSE +0 -21
  48. recs-0.3.1.dist-info/RECORD +0 -47
  49. 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 as sf
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
- format: Format
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.path = path_pattern.PathPattern(cfg.path)
37
+ self.output_directory = path_pattern.PathPattern(cfg.output_directory)
36
38
 
37
- self.format = t.cast(Format, cfg.format)
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 sf.check_format(self.format, subtype):
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'format={self.format:s}, sdtype={cfg.sdtype:s}'
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 sf.check_format(self.format, self.subtype):
54
- raise RecsError(f'{self.format} and {self.subtype} are incompatible')
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 cfg.devices.name:
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 Argument, rich_utils
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) -> dtyper.Option:
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
- path: str = Argument(
40
- RECS.path, help='Path or path pattern for recorded file locations'
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 - currently does nothing',
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
- format: types.Format = Option(
114
- RECS.format,
120
+ formats: t.List[types.Format] = Option(
121
+ tuple(RECS.formats),
115
122
  '-f',
116
- '--format',
123
+ '--formats',
117
124
  click_type=app.FormatParam(),
118
- help='Audio file format',
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 limits (4G on .wav, 2G on .aiff)',
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
- '-o',
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='Shortest amount of time per file',
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 == 'abcdefimnorstuv', _USED_SINGLES
254
- assert _UNUSED_SINGLES == 'ghjklpqwxyz', _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 DeviceDict, SdType, Stop
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
- class Update(t.NamedTuple):
16
- array: np.ndarray
17
- timestamp: float
17
+ DeviceDict = dict[str, float | int | str]
18
18
 
19
19
 
20
- DeviceCallback = t.Callable[[Update], None]
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
- self.channels = t.cast(int, self.info['max_input_channels'])
38
- self.samplerate = int(self.info['default_samplerate'])
39
- self.name = t.cast(str, self.info['name'])
40
- self._key = self.name
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, device_callback: DeviceCallback, sdtype: SdType, on_error: Stop
47
- ) -> InputStream:
48
- import sounddevice as sd
49
-
50
- stream: sd.InputStream
51
-
52
- def callback(indata: np.ndarray, frames: int, time: t.Any, status: int) -> None:
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
- device_callback(Update(indata.copy(), times.timestamp()))
48
+ update_callback(Update(indata.copy(), times.timestamp()))
58
49
 
59
50
  except Exception: # pragma: no cover
60
51
  traceback.print_exc()
61
- try:
62
- on_error()
63
- except Exception:
64
- traceback.print_exc()
52
+ result.stop()
65
53
 
66
- stream = sd.InputStream(
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]:
@@ -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 as sf
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: sf.SoundFile) -> dict[str, str]:
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 evaluate(
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
- ) -> str:
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.device),
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 as sf
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
- return _info()
15
-
16
- if cfg.list_types:
17
- return _list_types()
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 = sf.available_formats()
22
+ avail = soundfile.available_formats()
32
23
  fmts = [f.upper() for f in Format]
33
- formats = {f: [avail[f], sf.available_subtypes(f)] for f in fmts}
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