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/.DS_Store ADDED
Binary file
recs/audio/.DS_Store ADDED
Binary file
recs/audio/block.py CHANGED
@@ -1,3 +1,5 @@
1
+ # mypy: disable-error-code="no-any-return, type-arg"
2
+
1
3
  import dataclasses as dc
2
4
  import numbers
3
5
  import typing as t
@@ -5,6 +7,8 @@ from functools import cached_property
5
7
 
6
8
  import numpy as np
7
9
 
10
+ from recs.cfg.source import to_matrix
11
+
8
12
  _EMPTY_SEEN = False
9
13
 
10
14
 
@@ -15,9 +19,7 @@ class Block:
15
19
  def __post_init__(self) -> None:
16
20
  if not self.block.size:
17
21
  raise ValueError('Empty block')
18
-
19
- if len(self.block.shape) == 1:
20
- self.__dict__['block'] = self.block.reshape(*self.block.shape, 1)
22
+ self.__dict__['block'] = to_matrix(self.block)
21
23
 
22
24
  def __len__(self) -> int:
23
25
  return self.block.shape[0]
@@ -26,7 +28,7 @@ class Block:
26
28
  return Block(self.block[index])
27
29
 
28
30
  @cached_property
29
- def is_float(self):
31
+ def is_float(self) -> bool:
30
32
  return not issubclass(self.block.dtype.type, numbers.Integral)
31
33
 
32
34
  @cached_property
@@ -1,16 +1,19 @@
1
1
  import contextlib
2
+ import sys
2
3
  import typing as t
3
4
  from datetime import datetime
4
5
  from pathlib import Path
5
6
  from threading import Lock
6
7
 
8
+ from numpy.typing import NDArray
7
9
  from overrides import override
8
10
  from soundfile import SoundFile
9
11
  from threa import Runnable
10
12
 
11
13
  from recs.base.state import ChannelState
14
+ from recs.base.type_conversions import SUBTYPE_TO_SDTYPE
12
15
  from recs.base.types import SDTYPE, Active, Format, SdType
13
- from recs.cfg import Cfg, Track, device, time_settings
16
+ from recs.cfg import Cfg, Track, time_settings
14
17
  from recs.misc import counter, file_list
15
18
 
16
19
  from .block import Block, Blocks
@@ -19,11 +22,8 @@ from .header_size import header_size
19
22
 
20
23
  URL = 'https://github.com/rec/recs'
21
24
 
22
- BUFFER = 128
23
- FORMAT_TO_SIZE_LIMIT = {
24
- Format.aiff: 0x8000_0000,
25
- Format.wav: 0x1_0000_0000,
26
- }
25
+ BUFFER = 0x80
26
+ MAX_WAV_SIZE = 0x1_0000_0000 - BUFFER
27
27
 
28
28
  ITEMSIZE = {
29
29
  SdType.float32: 4,
@@ -35,9 +35,9 @@ BLOCK_FUZZ = 2
35
35
 
36
36
 
37
37
  class ChannelWriter(Runnable):
38
- bytes_in_this_file: int = 0
38
+ bytes_in_file: int = 0
39
39
 
40
- frames_in_this_file: int = 0
40
+ frames_in_file: int = 0
41
41
  frames_written: int = 0 # Used elsewhere
42
42
 
43
43
  largest_file_size: int = 0
@@ -45,19 +45,19 @@ class ChannelWriter(Runnable):
45
45
 
46
46
  timestamp: float = 0
47
47
 
48
- _sf: SoundFile | None = None
48
+ _sfs: t.Sequence[SoundFile] = ()
49
49
 
50
50
  @property
51
51
  def active(self) -> Active:
52
- return Active.active if self._sf else Active.inactive
52
+ return Active.active if self._sfs else Active.inactive
53
53
 
54
54
  def __init__(
55
55
  self, cfg: Cfg, times: time_settings.TimeSettings[int], track: Track
56
56
  ) -> None:
57
57
  super().__init__()
58
58
 
59
+ self.cfg = cfg
59
60
  self.do_not_record = cfg.dry_run or cfg.calibrate
60
- self.format = cfg.format
61
61
  self.metadata = cfg.metadata
62
62
  self.times = times
63
63
  self.track = track
@@ -65,20 +65,58 @@ class ChannelWriter(Runnable):
65
65
  self._blocks = Blocks()
66
66
  self._lock = Lock()
67
67
 
68
+ if track.source.format is None or cfg.cfg.formats:
69
+ self.formats = cfg.formats
70
+ else:
71
+ self.formats = [track.source.format]
72
+
73
+ if track.source.subtype is None or cfg.cfg.subtype:
74
+ subtype = cfg.subtype
75
+ else:
76
+ subtype = track.source.subtype
77
+
78
+ if track.source.subtype is None or cfg.cfg.sdtype:
79
+ sdtype = cfg.sdtype or SDTYPE
80
+ else:
81
+ sdtype = SUBTYPE_TO_SDTYPE[track.source.subtype]
82
+
68
83
  self.files_written = file_list.FileList()
69
- self.frame_size = ITEMSIZE[cfg.sdtype or SDTYPE] * len(track.channels)
84
+ self.frame_size = ITEMSIZE[sdtype] * len(track.channels)
70
85
  self.longest_file_frames = times.longest_file_time
71
- self.opener = FileOpener(cfg, track)
86
+
87
+ self.openers = [
88
+ FileOpener(
89
+ channels=len(track.channels),
90
+ format=f,
91
+ samplerate=track.source.samplerate,
92
+ subtype=subtype,
93
+ )
94
+ for f in self.formats
95
+ ]
72
96
  self._volume = counter.MovingBlock(times.moving_average_time)
73
97
 
74
- if not cfg.infinite_length:
75
- largest = FORMAT_TO_SIZE_LIMIT.get(cfg.format, 0)
76
- self.largest_file_size = max(largest - BUFFER, 0)
98
+ def size(f: str) -> int:
99
+ return (
100
+ MAX_WAV_SIZE if f == Format.wav and not self.cfg.infinite_length else 0
101
+ )
102
+
103
+ self.largest_file_size = max(0, *(size(f) for f in cfg.formats))
104
+
105
+ def to_block(self, array: NDArray) -> Block:
106
+ return Block(array[:, self.track.slice])
77
107
 
78
- def update(self, update: device.Update) -> ChannelState:
79
- block = Block(update.array[:, self.track.slice])
108
+ def receive_update(
109
+ self, block: Block, timestamp: float, should_record: bool = False
110
+ ) -> ChannelState:
80
111
  with self._lock:
81
- return self._receive_block(block, update.timestamp)
112
+ should_record = should_record or self.should_record(block)
113
+ return self._receive_block(block, timestamp, should_record)
114
+
115
+ def should_record(self, block: Block) -> bool:
116
+ return (
117
+ self.times.record_everything
118
+ or block.volume >= self.times.noise_floor_amplitude
119
+ )
82
120
 
83
121
  @override
84
122
  def stop(self) -> None:
@@ -88,32 +126,37 @@ class ChannelWriter(Runnable):
88
126
  self.stopped = True
89
127
 
90
128
  def _close(self) -> None:
91
- sf, self._sf = self._sf, None
92
- if sf and sf.frames and sf.frames >= self.times.shortest_file_time:
93
- sf.close()
94
- elif sf:
95
- with contextlib.suppress(Exception):
129
+ sfs, self._sfs = self._sfs, ()
130
+ for sf in sfs:
131
+ if sf.frames and sf.frames >= self.times.shortest_file_time:
132
+ print(sf.name, file=sys.stderr)
96
133
  sf.close()
97
- with contextlib.suppress(Exception):
98
- Path(sf.name).unlink()
99
-
100
- def _open(self, offset: int) -> SoundFile:
101
- timestamp = self.timestamp - offset / self.track.device.samplerate
102
- ts = datetime.fromtimestamp(timestamp)
103
-
134
+ else:
135
+ with contextlib.suppress(Exception):
136
+ sf.close()
137
+ with contextlib.suppress(Exception):
138
+ Path(sf.name).unlink()
139
+
140
+ def _open(self, offset: int) -> t.Sequence[SoundFile]:
141
+ timestamp = self.timestamp - offset / self.track.source.samplerate
142
+ date = datetime.fromtimestamp(timestamp).isoformat()
104
143
  index = 1 + len(self.files_written)
105
-
106
- metadata = {'date': ts.isoformat(), 'software': URL, 'tracknumber': str(index)}
144
+ metadata = {'date': date, 'software': URL, 'tracknumber': str(index)}
107
145
  metadata |= self.metadata
108
146
 
109
- self.bytes_in_this_file = header_size(metadata, self.format)
110
- self.frames_in_this_file = 0
147
+ self.bytes_in_file = max(header_size(metadata, f) for f in self.cfg.formats)
148
+ self.frames_in_file = 0
111
149
 
112
- sf = self.opener.create(metadata, timestamp, index)
113
- self.files_written.append(Path(sf.name))
114
- return sf
150
+ path = self.cfg.output_directory.make_path(
151
+ self.track, self.cfg.aliases, timestamp, index
152
+ )
153
+ sfs = [o.create(metadata, path) for o in self.openers]
154
+ self.files_written.extend(Path(sf.name) for sf in sfs)
155
+ return sfs
115
156
 
116
- def _receive_block(self, block: Block, timestamp: float) -> ChannelState:
157
+ def _receive_block(
158
+ self, block: Block, timestamp: float, should_record: bool
159
+ ) -> ChannelState:
117
160
  saved_state = self._state(
118
161
  max_amp=max(block.max) / block.scale,
119
162
  min_amp=min(block.min) / block.scale,
@@ -123,16 +166,16 @@ class ChannelWriter(Runnable):
123
166
  self.timestamp = timestamp
124
167
  self._volume(block)
125
168
 
126
- if not self.do_not_record and (self._sf or not self.stopped):
127
- expected_dt = len(block) / self.track.device.samplerate
169
+ if not self.do_not_record and (self._sfs or not self.stopped):
170
+ expected_dt = len(block) / self.track.source.samplerate
128
171
 
129
172
  if dt > expected_dt * BLOCK_FUZZ: # We were asleep, or otherwise lost time
130
173
  self._write_and_close()
131
174
 
132
175
  self._blocks.append(block)
133
176
 
134
- if block.volume >= self.times.noise_floor_amplitude:
135
- if not self._sf: # Record some quiet before the first block
177
+ if should_record:
178
+ if not self._sfs: # Record some quiet before the first block
136
179
  length = self.times.quiet_before_start + len(self._blocks[-1])
137
180
  self._blocks.clip(length, from_start=True)
138
181
 
@@ -144,12 +187,12 @@ class ChannelWriter(Runnable):
144
187
 
145
188
  return self._state() - saved_state
146
189
 
147
- def _state(self, **kwargs) -> ChannelState:
190
+ def _state(self, **kwargs: t.Any) -> ChannelState:
148
191
  return ChannelState(
149
192
  file_count=len(self.files_written),
150
193
  file_size=self.files_written.total_size,
151
- is_active=bool(self._sf),
152
- recorded_time=self.frames_written / self.track.device.samplerate,
194
+ is_active=bool(self._sfs),
195
+ recorded_time=self.frames_written / self.track.source.samplerate,
153
196
  timestamp=self.timestamp,
154
197
  volume=tuple(self._volume.mean()),
155
198
  **kwargs,
@@ -159,7 +202,7 @@ class ChannelWriter(Runnable):
159
202
  # Record some quiet after the last block
160
203
  removed = self._blocks.clip(self.times.quiet_after_end, from_start=False)
161
204
 
162
- if self._sf and removed:
205
+ if self._sfs and removed:
163
206
  self._write_blocks(reversed(removed))
164
207
 
165
208
  self._close()
@@ -176,19 +219,20 @@ class ChannelWriter(Runnable):
176
219
  remains: list[int] = []
177
220
 
178
221
  if self.longest_file_frames:
179
- remains.append(self.longest_file_frames - self.frames_in_this_file)
222
+ remains.append(self.longest_file_frames - self.frames_in_file)
180
223
 
181
- if self._sf and self.largest_file_size:
182
- file_bytes = self.largest_file_size - self.bytes_in_this_file
224
+ if self._sfs and self.largest_file_size:
225
+ file_bytes = self.largest_file_size - self.bytes_in_file
183
226
  remains.append(file_bytes // self.frame_size)
184
227
 
185
228
  if remains and min(remains) <= len(b):
186
229
  self._close()
187
230
 
188
- self._sf = self._sf or self._open(offset)
189
- self._sf.write(b.block)
231
+ self._sfs = self._sfs or self._open(offset)
232
+ for sf in self._sfs:
233
+ sf.write(b.block)
190
234
  offset += len(b)
191
235
 
192
- self.frames_in_this_file += len(b)
236
+ self.frames_in_file += len(b)
193
237
  self.frames_written += len(b)
194
- self.bytes_in_this_file += len(b) * self.frame_size
238
+ self.bytes_in_file += len(b) * self.frame_size
recs/audio/file_opener.py CHANGED
@@ -1,48 +1,50 @@
1
+ import dataclasses as dc
1
2
  import itertools
3
+ import typing as t
2
4
  from pathlib import Path
3
5
 
4
- import soundfile as sf
6
+ import soundfile
5
7
 
6
- from recs.cfg import Cfg, Track
8
+ from recs.base.types import Format, Subtype
7
9
  from recs.cfg.metadata import ALLOWS_METADATA
8
10
 
9
11
 
12
+ @dc.dataclass
10
13
  class FileOpener:
11
- def __init__(self, cfg: Cfg, track: Track) -> None:
12
- self.cfg = cfg
13
- self.track = track
14
+ format: Format
15
+ channels: int = 1
16
+ samplerate: int = 48_000
17
+ subtype: Subtype | None = None
14
18
 
15
19
  def open(
16
- self, path: Path | str, metadata: dict[str, str], overwrite: bool = False
17
- ) -> sf.SoundFile:
18
- path = Path(path).with_suffix('.' + self.cfg.format)
20
+ self, path: Path | str, metadata: t.Mapping[str, str], overwrite: bool = False
21
+ ) -> soundfile.SoundFile:
22
+ path = Path(path).with_suffix('.' + self.format)
19
23
  if not overwrite and path.exists():
20
24
  raise FileExistsError(str(path))
21
25
 
22
- fp = sf.SoundFile(
23
- channels=len(self.track.channels),
26
+ fp = soundfile.SoundFile(
27
+ channels=self.channels,
24
28
  file=path,
25
- format=self.cfg.format,
29
+ format=self.format,
26
30
  mode='w',
27
- samplerate=self.track.device.samplerate,
28
- subtype=self.cfg.subtype,
31
+ samplerate=self.samplerate,
32
+ subtype=self.subtype,
29
33
  )
30
34
 
31
- if self.cfg.format in ALLOWS_METADATA:
35
+ if self.format in ALLOWS_METADATA:
32
36
  for k, v in metadata.items():
33
37
  setattr(fp, k, v)
34
38
 
35
39
  return fp
36
40
 
37
- def create(
38
- self, metadata: dict[str, str], timestamp: float, index: int
39
- ) -> sf.SoundFile:
40
- p = Path(self.cfg.path.evaluate(self.track, self.cfg.aliases, timestamp, index))
41
- p.parent.mkdir(exist_ok=True, parents=True)
41
+ def create(self, metadata: t.Mapping[str, str], path: Path) -> soundfile.SoundFile:
42
+ path.parent.mkdir(exist_ok=True, parents=True)
42
43
 
43
44
  for i in itertools.count():
44
- f = p.parent / (p.name + bool(i) * f'_{i}')
45
+ f = path.parent / (path.name + bool(i) * f'_{i}')
45
46
  try:
46
47
  return self.open(f, metadata)
47
48
  except FileExistsError:
48
49
  pass
50
+ raise FileNotFoundError
recs/audio/header_size.py CHANGED
@@ -1,15 +1,15 @@
1
+ import typing as t
2
+
1
3
  from recs.base.types import Format
2
4
 
3
5
 
4
- def header_size(metadata: dict[str, str], format: Format) -> int:
5
- tag = 9
6
- if format == Format.aiff:
7
- base, first, software = 54, 0, 22
8
- elif format == Format.wav:
9
- base, first, software = 44, 12, 19
10
- else:
6
+ def header_size(metadata: t.Mapping[str, str], format: Format) -> int:
7
+ if format != Format.wav:
11
8
  return 0
12
9
 
10
+ tag = 9
11
+ base, first, software = 44, 12, 19
12
+
13
13
  values = (software * (k == 'software') + tag + len(v) for k, v in metadata.items())
14
14
  values = (v + v % 2 for v in values)
15
15
 
recs/base/.DS_Store ADDED
Binary file
@@ -8,14 +8,14 @@ import json
8
8
  import typing as t
9
9
 
10
10
 
11
- def query_devices() -> list[dict[str, t.Any]]:
11
+ def _query_devices() -> t.Any:
12
12
  try:
13
13
  import sounddevice
14
14
 
15
15
  return sounddevice.query_devices()
16
- except Exception:
16
+ except BaseException:
17
17
  return []
18
18
 
19
19
 
20
20
  if __name__ == '__main__':
21
- print(json.dumps(query_devices(), indent=4))
21
+ print(json.dumps(_query_devices(), indent=4))
recs/base/cfg_raw.py CHANGED
@@ -2,8 +2,6 @@ import dataclasses as dc
2
2
  import typing as t
3
3
  from pathlib import Path
4
4
 
5
- from .types import Format
6
-
7
5
 
8
6
  @dc.dataclass
9
7
  class CfgRaw:
@@ -11,7 +9,8 @@ class CfgRaw:
11
9
  #
12
10
  # Directory settings
13
11
  #
14
- path: str = ''
12
+ files: list[str] = dc.field(default_factory=list)
13
+ output_directory: str = ''
15
14
  #
16
15
  # General purpose settings
17
16
  #
@@ -33,7 +32,7 @@ class CfgRaw:
33
32
  #
34
33
  # Audio file format and subtype
35
34
  #
36
- format: str = Format.flac
35
+ formats: t.Sequence[str] = ()
37
36
  metadata: t.Sequence[str] = ()
38
37
  sdtype: str = ''
39
38
  subtype: str = ''
@@ -47,10 +46,12 @@ class CfgRaw:
47
46
  #
48
47
  # Settings relating to times
49
48
  #
49
+ band_mode: bool = True
50
50
  infinite_length: bool = False
51
51
  longest_file_time: float = 0.0
52
52
  moving_average_time: float = 1.0
53
53
  noise_floor: float = 70.0
54
+ record_everything: bool = False
54
55
  shortest_file_time: float = 1.0
55
56
  quiet_after_end: float = 2.0
56
57
  quiet_before_start: float = 1.0
recs/base/pyproject.py CHANGED
@@ -7,7 +7,7 @@ assert PYPROJECT.exists()
7
7
 
8
8
 
9
9
  def message() -> str:
10
- p = tomli.loads(PYPROJECT.read_text())['tool']['poetry']
10
+ p = tomli.loads(PYPROJECT.read_text())['project']
11
11
  desc, name = p['description'], p['name']
12
12
 
13
13
  icon, *d, icon2 = desc.split()
@@ -11,16 +11,7 @@ SUBTYPE_TO_SDTYPE = {
11
11
  Subtype.alac_24: SdType.int32,
12
12
  Subtype.alac_32: SdType.int32,
13
13
  Subtype.double: SdType.float32,
14
- Subtype.dpcm_16: SdType.int16,
15
- Subtype.dpcm_8: SdType.int16,
16
- Subtype.dwvw_12: SdType.int16,
17
- Subtype.dwvw_16: SdType.int16,
18
- Subtype.dwvw_24: SdType.int32,
19
14
  Subtype.float: SdType.float32,
20
- Subtype.ms_adpcm: SdType.int32,
21
- Subtype.nms_adpcm_16: SdType.int16,
22
- Subtype.nms_adpcm_24: SdType.int32,
23
- Subtype.nms_adpcm_32: SdType.int32,
24
15
  Subtype.pcm_16: SdType.int16,
25
16
  Subtype.pcm_24: SdType.int32,
26
17
  Subtype.pcm_32: SdType.int32,
recs/base/types.py CHANGED
@@ -3,7 +3,6 @@ from enum import auto
3
3
 
4
4
  from strenum import StrEnum
5
5
 
6
- DeviceDict = dict[str, float | int | str]
7
6
  Stop = t.Callable[[], None]
8
7
 
9
8
 
@@ -23,27 +22,14 @@ SDTYPE = SdType.float32
23
22
 
24
23
 
25
24
  class Format(StrEnum):
26
- aiff = auto()
27
- au = auto()
28
- avr = auto()
29
- caf = auto()
30
25
  flac = auto()
31
- ircam = auto()
32
- mat4 = auto()
33
- mat5 = auto()
34
26
  mp3 = auto()
35
- mpc2k = auto()
36
- nist = auto()
37
27
  ogg = auto()
38
- paf = auto()
39
- pvf = auto()
40
28
  raw = auto()
41
29
  rf64 = auto()
42
- sd2 = auto()
43
- voc = auto()
44
- w64 = auto()
45
30
  wav = auto()
46
- wavex = auto()
31
+
32
+ _default = wav
47
33
 
48
34
 
49
35
  class Subtype(StrEnum):
@@ -51,33 +37,15 @@ class Subtype(StrEnum):
51
37
  alac_20 = auto()
52
38
  alac_24 = auto()
53
39
  alac_32 = auto()
54
- alaw = auto()
55
40
  double = auto()
56
- dpcm_16 = auto()
57
- dpcm_8 = auto()
58
- dwvw_12 = auto()
59
- dwvw_16 = auto()
60
- dwvw_24 = auto()
61
- dwvw_n = auto()
62
41
  float = auto()
63
- g721_32 = auto()
64
- g723_24 = auto()
65
- g723_40 = auto()
66
- gsm610 = auto()
67
- ima_adpcm = auto()
68
42
  mpeg_layer_i = auto()
69
43
  mpeg_layer_ii = auto()
70
44
  mpeg_layer_iii = auto()
71
- ms_adpcm = auto()
72
- nms_adpcm_16 = auto()
73
- nms_adpcm_24 = auto()
74
- nms_adpcm_32 = auto()
75
45
  opus = auto()
76
46
  pcm_16 = auto()
77
47
  pcm_24 = auto()
78
48
  pcm_32 = auto()
79
49
  pcm_s8 = auto()
80
50
  pcm_u8 = auto()
81
- ulaw = auto()
82
51
  vorbis = auto()
83
- vox_adpcm = auto()
recs/cfg/.DS_Store ADDED
Binary file
recs/cfg/__init__.py CHANGED
@@ -1,15 +1,8 @@
1
1
  from .aliases import Aliases
2
2
  from .cfg import Cfg
3
- from .device import InputDevice, InputDevices, InputStream
4
- from .path_pattern import PathPattern
3
+ from .device import InputDevice
4
+ from .file_source import FileSource
5
+ from .source import Source
5
6
  from .track import Track
6
7
 
7
- __all__ = (
8
- 'Aliases',
9
- 'Cfg',
10
- 'InputDevice',
11
- 'InputDevices',
12
- 'InputStream',
13
- 'PathPattern',
14
- 'Track',
15
- )
8
+ __all__ = 'Aliases', 'Cfg', 'FileSource', 'InputDevice', 'Source', 'Track'
recs/cfg/aliases.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import typing as t
2
2
 
3
+ from recs.base import RecsError, prefix_dict
3
4
  from recs.base.prefix_dict import PrefixDict
4
- from recs.base import prefix_dict, RecsError
5
5
 
6
-
7
- from .device import InputDevice, InputDevices
6
+ from .device import InputDevices
7
+ from .source import Source
8
8
  from .track import Track
9
9
 
10
10
  CHANNEL_SPLITTER = '+'
@@ -15,11 +15,9 @@ class Aliases:
15
15
 
16
16
  def __init__(self, aliases: t.Sequence[str], devices: InputDevices) -> None:
17
17
  self.tracks = PrefixDict()
18
-
19
- assert devices
20
18
  self.devices = devices
21
19
 
22
- if not aliases:
20
+ if not (aliases and devices):
23
21
  self.inv = {}
24
22
  return
25
23
 
@@ -27,11 +25,11 @@ class Aliases:
27
25
  alias, sep, value = (n.strip() for n in name.partition('='))
28
26
  return alias, (value or alias)
29
27
 
30
- names, values = zip(*(split(n) for n in aliases))
28
+ names, values = zip(*(split(n) for n in aliases), strict=False)
31
29
  if len(set(names)) < len(names):
32
30
  raise RecsError(f'Duplicate aliases: {aliases}')
33
31
 
34
- self.tracks.update(sorted(zip(names, self.to_tracks(values))))
32
+ self.tracks.update(sorted(zip(names, self.to_tracks(values), strict=False)))
35
33
 
36
34
  inv: dict[Track, list[str]] = {}
37
35
  for k, v in self.tracks.items():
@@ -56,7 +54,7 @@ class Aliases:
56
54
  if not errors:
57
55
  return result
58
56
 
59
- def err(k, v) -> str:
57
+ def err(k: str, v: list[str]) -> str:
60
58
  s = 's' * (len(v) != 1)
61
59
  return f'{k.capitalize()} device name{s}: {", ".join(v)}'
62
60
 
@@ -66,8 +64,8 @@ class Aliases:
66
64
 
67
65
  raise RecsError('\n'.join([*errs, devices]))
68
66
 
69
- def display_name(self, x: InputDevice | Track, short: bool = True) -> str:
70
- if isinstance(x, InputDevice):
67
+ def display_name(self, x: Source | Track, short: bool = True) -> str:
68
+ if isinstance(x, Source):
71
69
  return self.inv.get(Track(x), x.name)
72
70
 
73
71
  default = x.name if short else str(x)
@@ -83,10 +81,8 @@ class Aliases:
83
81
  try:
84
82
  track = self.tracks[name]
85
83
  except KeyError:
86
- device = self.devices[name]
87
- else:
88
- if track.channels:
89
- raise KeyError(track_name, 'impossible')
90
- device = track.device
84
+ return Track(self.devices[name], channels)
91
85
 
92
- return Track(device, channels)
86
+ if track.channels:
87
+ raise KeyError(track_name, 'impossible')
88
+ return Track(track.source, channels)