recs 0.3.0__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.0.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.0.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.0.dist-info/LICENSE +0 -21
  48. recs-0.3.0.dist-info/RECORD +0 -47
  49. recs-0.3.0.dist-info/entry_points.txt +0 -3
recs/cfg/track.py CHANGED
@@ -1,22 +1,24 @@
1
1
  from recs.base import RecsError
2
2
 
3
3
  from . import hash_cmp
4
- from .device import InputDevice
4
+ from .source import Source
5
5
 
6
6
  __all__ = ('Track',)
7
7
 
8
8
 
9
9
  class Track(hash_cmp.HashCmp):
10
- def __init__(self, d: InputDevice, channel: str | tuple[int, ...] = ()) -> None:
11
- self.device = d
10
+ name: str
11
+
12
+ def __init__(self, source: Source, channel: str | tuple[int, ...] = ()) -> None:
13
+ self.source = source
12
14
 
13
15
  channels = channel or ()
14
16
  if isinstance(channels, str):
15
- self.channels = _channels(channels, d.name, d.channels)
17
+ self.channels = _channels(channels, source.name, source.channels)
16
18
  else:
17
19
  self.channels = channels
18
20
 
19
- self._key = d.name, self.channels
21
+ self._key = source.name, self.channels
20
22
 
21
23
  if self.channels:
22
24
  a, b = self.channels[0], self.channels[-1]
@@ -29,11 +31,11 @@ class Track(hash_cmp.HashCmp):
29
31
 
30
32
  def __str__(self) -> str:
31
33
  if self.channels:
32
- return f'{self.device.name} + {self.name}'
33
- return self.device.name
34
+ return f'{self.source.name} + {self.name}'
35
+ return self.source.name
34
36
 
35
37
  def __repr__(self) -> str:
36
- return f'Track(\'{self}\')'
38
+ return f"Track('{self}')"
37
39
 
38
40
 
39
41
  def _channels(channel: str, device_name: str, max_channels: int) -> tuple[int, ...]:
@@ -54,7 +56,8 @@ def _channels(channel: str, device_name: str, max_channels: int) -> tuple[int, .
54
56
  raise ValueError('Channels must be in order')
55
57
 
56
58
  if channels[-1] > max_channels:
57
- raise ValueError(f'Device has only {max_channels} channels')
59
+ s = '' if max_channels == 1 else 's'
60
+ raise ValueError(f'Device has only {max_channels} channel{s}')
58
61
 
59
62
  return channels
60
63
 
recs/misc/.DS_Store ADDED
Binary file
recs/misc/__init__.py CHANGED
@@ -1 +0,0 @@
1
-
recs/misc/contexts.py CHANGED
@@ -3,7 +3,7 @@ from contextlib import ExitStack, contextmanager
3
3
 
4
4
 
5
5
  @contextmanager
6
- def contexts(*contexts: t.ContextManager) -> t.Generator:
6
+ def contexts(*contexts: t.ContextManager) -> t.Generator: # type: ignore[type-arg]
7
7
  with ExitStack() as stack:
8
8
  for c in contexts:
9
9
  stack.enter_context(c)
recs/misc/counter.py CHANGED
@@ -9,7 +9,9 @@ import numpy as np
9
9
  from recs.audio.block import Block
10
10
 
11
11
  # TODO: isn't there some type comprising these first four?
12
- Num: t.TypeAlias = int | float | numbers.Integral | numbers.Real | np.ndarray
12
+ Num: t.TypeAlias = (
13
+ int | float | numbers.Integral | numbers.Real | np.ndarray # type: ignore[type-arg]
14
+ )
13
15
 
14
16
 
15
17
  @dc.dataclass
@@ -17,11 +19,13 @@ class Counter:
17
19
  value: int = 0
18
20
  lock: Lock = dc.field(default_factory=Lock)
19
21
 
20
- def __call__(self, i: int = 1) -> int:
22
+ def accumulate(self, i: int = 1) -> int:
21
23
  with self.lock:
22
24
  self.value += i
23
25
  return self.value
24
26
 
27
+ __call__ = accumulate # deprecated
28
+
25
29
 
26
30
  class Accumulator:
27
31
  count: int = 0
@@ -29,7 +33,7 @@ class Accumulator:
29
33
  sum: Num
30
34
  square_sum: Num
31
35
 
32
- def __call__(self, x: Num) -> None:
36
+ def accumulate(self, x: Num) -> None:
33
37
  self.value = x
34
38
  try:
35
39
  self.sum += x
@@ -40,6 +44,8 @@ class Accumulator:
40
44
  self.square_sum += x * x
41
45
  self.count += 1
42
46
 
47
+ __call__ = accumulate # deprecated
48
+
43
49
  def mean(self) -> Num:
44
50
  return self.count and self.sum / self.count
45
51
 
@@ -56,13 +62,15 @@ class MovingBlock:
56
62
  def __init__(self, moving_average_time: int):
57
63
  self.moving_average_time = moving_average_time
58
64
 
59
- def __call__(self, b: Block) -> None:
65
+ def accumulate(self, b: Block) -> None:
60
66
  if self._dq is None:
61
67
  maxlen = int(0.5 + self.moving_average_time / len(b))
62
68
  self._dq = deque((), maxlen)
63
69
 
64
70
  self._dq.append(b.amplitude)
65
71
 
72
+ __call__ = accumulate # deprecated
73
+
66
74
  def mean(self) -> np.ndarray:
67
75
  if not self._dq:
68
76
  return np.array([0])
recs/misc/file_list.py CHANGED
@@ -24,7 +24,7 @@ class FileList(list[Path]):
24
24
  return self._total_size + _getsize(self[-1])
25
25
 
26
26
 
27
- def _getsize(p) -> int:
27
+ def _getsize(p: Path) -> int:
28
28
  try:
29
29
  return os.path.getsize(p)
30
30
  except FileNotFoundError:
recs/misc/log.py CHANGED
@@ -2,7 +2,7 @@ import os
2
2
  import typing as t
3
3
  from functools import cache, wraps
4
4
 
5
- C = t.TypeVar('C', bound=t.Callable)
5
+ C = t.TypeVar('C', bound=t.Callable[..., t.Any])
6
6
 
7
7
  DISABLE = False
8
8
 
@@ -12,15 +12,15 @@ VERBOSE = not False
12
12
 
13
13
 
14
14
  @cache
15
- def _logger():
15
+ def _logger() -> t.TextIO:
16
16
  return open(f'/tmp/log-{os.getpid()}.txt', 'w')
17
17
 
18
18
 
19
- def log(*a, **ka) -> None:
19
+ def log(*a: t.Any, **ka: t.Any) -> None:
20
20
  print(*a, **ka, file=_logger())
21
21
 
22
22
 
23
- def verbose(*a, **ka) -> None:
23
+ def verbose(*a: t.Any, **ka: t.Any) -> None:
24
24
  if VERBOSE:
25
25
  log(*a, **ka)
26
26
 
@@ -30,7 +30,7 @@ def logged(function: C) -> C:
30
30
  return function
31
31
 
32
32
  @wraps(function)
33
- def wrapped(*args, **kwargs):
33
+ def wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any:
34
34
  verbose(function, 'before')
35
35
  try:
36
36
  return function(*args, **kwargs)
recs/ui/.DS_Store ADDED
Binary file
recs/ui/full_state.py CHANGED
@@ -2,15 +2,15 @@ import typing as t
2
2
 
3
3
  from recs.base import state, times
4
4
  from recs.base.types import Active
5
- from recs.cfg import InputDevice, Track
5
+ from recs.cfg import Source, Track
6
6
 
7
7
 
8
8
  class FullState:
9
- def __init__(self, tracks: dict[InputDevice, t.Sequence[Track]]) -> None:
10
- def device_state(t) -> dict[str, state.ChannelState]:
11
- return {i.name: state.ChannelState() for i in t}
9
+ def __init__(self, tracks: t.Sequence[tuple[Source, t.Sequence[Track]]]) -> None:
10
+ def device_state(tr: t.Sequence[Track]) -> dict[str, state.ChannelState]:
11
+ return {i.name: state.ChannelState() for i in tr}
12
12
 
13
- self.state = {k.name: device_state(v) for k, v in tracks.items()}
13
+ self.state = {k.name: device_state(v) for k, v in tracks}
14
14
  self.total = state.ChannelState()
15
15
  self.start_time = times.timestamp()
16
16
 
@@ -18,7 +18,7 @@ class FullState:
18
18
  def elapsed_time(self) -> float:
19
19
  return times.timestamp() - self.start_time
20
20
 
21
- def update(self, state: dict[str, dict[str, state.ChannelState]]) -> None:
21
+ def update(self, state: t.Mapping[str, t.Mapping[str, state.ChannelState]]) -> None:
22
22
  for device_name, device_state in state.items():
23
23
  for channel_name, channel_state in device_state.items():
24
24
  self.state[device_name][channel_name] += channel_state
@@ -48,3 +48,8 @@ class FullState:
48
48
  'file_count': s.file_count,
49
49
  'volume': len(s.volume) and sum(s.volume) / len(s.volume),
50
50
  }
51
+
52
+ def db_ranges(self) -> dict[str, float]:
53
+ items = self.state.items()
54
+ d = {f'{k} - {k2}': v2.db_range for k, v in items for k2, v2 in v.items()}
55
+ return d | {'(all)': self.total.db_range}
recs/ui/live.py CHANGED
@@ -13,15 +13,15 @@ from recs.cfg import Cfg
13
13
 
14
14
  from .table import TableFormatter, to_str
15
15
 
16
- RowsFunction = t.Callable[[], t.Iterator[dict[str, t.Any]]]
17
-
18
16
  CONSOLE = Console(color_system='truecolor')
19
17
 
20
18
 
21
19
  class Live(Runnable):
22
20
  _last_update_time: float = 0
23
21
 
24
- def __init__(self, rows: RowsFunction, cfg: Cfg) -> None:
22
+ def __init__(
23
+ self, rows: t.Callable[[], t.Iterator[t.Mapping[str, t.Any]]], cfg: Cfg
24
+ ) -> None:
25
25
  self.rows = rows
26
26
  self.cfg = cfg
27
27
  super().__init__()
@@ -53,7 +53,7 @@ class Live(Runnable):
53
53
  super().stop()
54
54
 
55
55
 
56
- def _rgb(r=0, g=0, b=0) -> str:
56
+ def _rgb(r: int = 0, g: int = 0, b: int = 0) -> str:
57
57
  r, g, b = (int(i) % 256 for i in (r, g, b))
58
58
  return f'[rgb({r},{g},{b})]'
59
59
 
@@ -67,9 +67,9 @@ def _on(active: Active) -> str:
67
67
  return ''
68
68
 
69
69
 
70
- def _volume(x) -> str:
70
+ def _volume(x: t.Any) -> str:
71
71
  try:
72
- s = sum(x) / len(x)
72
+ s: float = sum(x) / len(x)
73
73
  except Exception:
74
74
  s = x
75
75
 
@@ -78,15 +78,15 @@ def _volume(x) -> str:
78
78
 
79
79
  if s < 1 / 3:
80
80
  r = 0
81
- g = 3 * s
81
+ g = round(3 * s)
82
82
  else:
83
- r = (3 * s - 1) / 2
83
+ r = round((3 * s - 1) / 2)
84
84
  g = 1 - r
85
85
 
86
86
  return _rgb(r * 256, g * 256) + to_str(x)
87
87
 
88
88
 
89
- def _time_to_str(x) -> str:
89
+ def _time_to_str(x: int) -> str:
90
90
  if not x:
91
91
  return ''
92
92
  s = times.to_str(x)
recs/ui/recorder.py CHANGED
@@ -1,74 +1,65 @@
1
+ import json
2
+ import multiprocessing as mp
1
3
  import typing as t
2
4
  from multiprocessing import connection
3
5
 
4
- from threa import HasThread, Runnables, IsThread
6
+ from threa import HasThread, Runnables, Wrapper
5
7
 
8
+ from recs.base import RecsError
6
9
  from recs.cfg import Cfg, device
7
10
 
8
11
  from . import live
9
- from .device_process import DeviceProcess
10
- from .device_recorder import POLL_TIMEOUT
11
- from .device_tracks import device_tracks
12
12
  from .full_state import FullState
13
+ from .source_recorder import POLL_TIMEOUT, SourceRecorder
14
+ from .source_tracks import source_tracks
13
15
 
14
16
 
15
17
  class Recorder(Runnables):
16
18
  def __init__(self, cfg: Cfg) -> None:
17
19
  super().__init__()
18
20
 
19
- tracks = device_tracks(cfg)
21
+ if not (all_tracks := list(source_tracks(cfg))):
22
+ raise RecsError('No channels selected')
23
+
20
24
  self.cfg = cfg
21
25
  self.live = live.Live(self.rows, cfg)
22
- self.state = FullState(tracks)
23
- self.device_names = DeviceNames(cfg.sleep_time_device)
26
+ self.state = FullState(all_tracks)
27
+ self.names = device.input_names()
28
+ self.connections: list[connection.Connection] = []
29
+ self.processes: list[mp.Process] = []
30
+
31
+ for _, tracks in all_tracks:
32
+ conn, child = mp.Pipe()
33
+ self.connections.append(conn)
34
+ kwargs = {'cfg': cfg.cfg, 'connection': child, 'tracks': tracks}
35
+ process = mp.Process(target=SourceRecorder, kwargs=kwargs)
36
+ self.processes.append(process)
24
37
 
25
- processes = tuple(DeviceProcess(cfg, t) for t in tracks.values())
26
- self.connections = {p.connection: p for p in processes}
27
38
  ui_time = 1 / self.cfg.ui_refresh_rate
28
39
  live_thread = HasThread(
29
40
  self.live.update, looping=True, name='LiveUpdate', pre_delay=ui_time
30
41
  )
31
42
 
32
- self.runnables = self.device_names, *processes, live_thread, self.live
33
-
34
- def run_recorder(self) -> None:
35
- with self:
36
- while self.running:
37
- self.receive()
38
-
39
- def finish(self) -> None:
40
- for r in reversed(self.runnables):
41
- r.finish()
42
- self.stop()
43
+ self.runnables = *(Wrapper(p) for p in self.processes), live_thread, self.live
43
44
 
44
45
  def rows(self) -> t.Iterator[dict[str, t.Any]]:
45
- yield from self.state.rows(self.device_names.names)
46
-
47
- def receive(self) -> None:
48
- for conn in connection.wait(list(self.connections), timeout=POLL_TIMEOUT):
49
- c = t.cast(connection.Connection, conn)
50
- for device_name, msg in c.recv().items():
51
- if msg.get('_exit'):
52
- # Not called
53
- device_process = self.connections[c]
54
- device_process.set_sent()
55
- print('Recorder _exit', device_process.device_name)
56
- self.running = False
57
- else:
58
- self.state.update({device_name: msg})
46
+ yield from self.state.rows(self.names)
59
47
 
60
- if (rt := self.cfg.total_run_time) and rt <= self.state.elapsed_time:
61
- self.stop()
48
+ def run(self) -> None:
49
+ try:
50
+ self._run()
51
+ finally:
52
+ if self.cfg.calibrate or self.cfg.verbose:
53
+ print(json.dumps(self.state.db_ranges(), indent=2))
62
54
 
63
-
64
- class DeviceNames(IsThread):
65
- daemon = True
66
- looping = True
67
-
68
- def __init__(self, pre_delay: float) -> None:
69
- self.pre_delay = pre_delay
70
- super().__init__()
71
- self.callback()
72
-
73
- def callback(self) -> None:
74
- self.names = device.input_names()
55
+ def _run(self) -> None:
56
+ with self:
57
+ while self.running and all(p.is_alive() for p in self.processes):
58
+ for c in connection.wait(self.connections, timeout=POLL_TIMEOUT):
59
+ conn = t.cast(connection.Connection, c)
60
+ try:
61
+ msg = conn.recv()
62
+ except EOFError:
63
+ pass
64
+ else:
65
+ self.state.update(msg)
@@ -0,0 +1,76 @@
1
+ import contextlib
2
+ import os
3
+ import typing as t
4
+ from multiprocessing.connection import Connection
5
+ from queue import Empty, Queue
6
+
7
+ import numpy as np
8
+ from threa import Runnables
9
+
10
+ from recs.audio.channel_writer import ChannelWriter
11
+ from recs.base import cfg_raw
12
+ from recs.base.types import Format
13
+ from recs.cfg import Cfg, Track
14
+ from recs.cfg.source import Update
15
+
16
+ NEW_CODE_FLAG = 'RECS_NEW_CODE' in os.environ
17
+ FINISH = 'finish'
18
+ OFFLINE_TIME = 1
19
+ POLL_TIMEOUT = 0.05
20
+
21
+
22
+ class SourceRecorder(Runnables):
23
+ sample_count: int = 0
24
+
25
+ def __init__(
26
+ self,
27
+ cfg: cfg_raw.CfgRaw,
28
+ connection: Connection,
29
+ tracks: t.Sequence[Track],
30
+ ) -> None:
31
+ self.cfg = Cfg(**cfg.asdict())
32
+ self.connection = connection
33
+
34
+ self.source = tracks[0].source
35
+ assert all(t.source == self.source for t in tracks)
36
+
37
+ self.name = self.cfg.aliases.display_name(self.source)
38
+ self.queue: Queue[Update] = Queue()
39
+ self.times = self.cfg.times.scale(self.source.samplerate)
40
+ self.channel_writers = tuple(
41
+ ChannelWriter(cfg=self.cfg, times=self.times, track=t) for t in tracks
42
+ )
43
+
44
+ self.input_stream = self.source.input_stream(
45
+ sdtype=self.cfg.sdtype,
46
+ update_callback=self.queue.put,
47
+ )
48
+ super().__init__(self.input_stream, *self.channel_writers)
49
+
50
+ with contextlib.suppress(KeyboardInterrupt), self:
51
+ while self.running:
52
+ with contextlib.suppress(Empty):
53
+ self._receive_update(self.queue.get(timeout=POLL_TIMEOUT))
54
+
55
+ with contextlib.suppress(Empty):
56
+ while True:
57
+ self._receive_update(self.queue.get(block=False))
58
+
59
+ def _receive_update(self, u: Update) -> None:
60
+ if self.cfg.formats == Format.mp3 and u.array.dtype == np.float32:
61
+ # mp3 and float32 crashes every time on my machine
62
+ u = Update(u.array.astype(np.float64), u.timestamp)
63
+
64
+ cb = {c: c.to_block(u.array) for c in self.channel_writers}
65
+ should_record = self.cfg.band_mode and any(
66
+ c.should_record(b) for c, b in cb.items()
67
+ )
68
+ msgs = {
69
+ c.track.name: c.receive_update(b, u.timestamp, should_record)
70
+ for c, b in cb.items()
71
+ }
72
+ self.connection.send({self.source.name: msgs})
73
+
74
+ self.sample_count += len(u.array)
75
+ if (t := self.times.total_run_time) and self.sample_count >= t:
76
+ self.running = False
@@ -1,34 +1,36 @@
1
1
  import typing as t
2
2
 
3
3
  from recs.base import RecsError
4
- from recs.cfg import Cfg, InputDevice, Track
4
+ from recs.cfg import Cfg, FileSource, InputDevice, Source, Track
5
5
 
6
- DeviceTracks = dict[InputDevice, t.Sequence[Track]]
7
6
 
7
+ def source_tracks(cfg: Cfg) -> t.Iterator[tuple[Source, t.Sequence[Track]]]:
8
+ if not (cfg.devices or cfg.files):
9
+ raise RecsError('No inputs were found')
8
10
 
9
- def device_tracks(cfg: Cfg) -> DeviceTracks:
10
- if not cfg.devices:
11
- raise RecsError('No audio input devices were found')
11
+ if cfg.files:
12
+ for file in cfg.files:
13
+ source = FileSource(file)
14
+ channels = '1' if source.channels == 1 else f'1-{source.channels}'
15
+ track = Track(source, channels)
16
+ yield source, [track]
12
17
 
13
- exc = cfg.aliases.to_tracks(cfg.exclude)
14
- inc = cfg.aliases.to_tracks(cfg.include)
18
+ else:
19
+ exc = cfg.aliases.to_tracks(cfg.exclude)
20
+ inc = cfg.aliases.to_tracks(cfg.include)
21
+ for d in cfg.devices.values():
22
+ if tracks := list(source_track(d, exc, inc)):
23
+ yield d, tracks
15
24
 
16
- it = ((d, list(device_track(d, exc, inc))) for d in cfg.devices.values())
17
- ts: DeviceTracks = {d: v for d, v in it if v}
18
- if ts:
19
- return ts
20
25
 
21
- raise RecsError('No channels selected')
22
-
23
-
24
- def device_track(
26
+ def source_track(
25
27
  d: InputDevice, exc: t.Sequence[Track] = (), inc: t.Sequence[Track] = ()
26
28
  ) -> t.Iterator[Track]:
27
29
  if Track(d) in exc:
28
30
  return
29
31
 
30
- excs = [i for i in exc if d.name == i.device.name]
31
- incs = [i for i in inc if d.name == i.device.name]
32
+ excs = [i for i in exc if d.name == i.source.name]
33
+ incs = [i for i in inc if d.name == i.source.name]
32
34
  if inc and not incs:
33
35
  return
34
36
 
recs/ui/table.py CHANGED
@@ -16,13 +16,13 @@ class TableFormatter:
16
16
  def __init__(self, **kwargs: t.Any):
17
17
  self.kwargs = kwargs
18
18
 
19
- def _to_str(self, row, column) -> str:
19
+ def _to_str(self, row: t.Mapping[str, t.Any], column: str) -> str:
20
20
  _to_str = self.kwargs.get(column) or to_str
21
21
  if (x := row.get(column)) is not None:
22
22
  return _to_str(x)
23
23
  return ''
24
24
 
25
- def __call__(self, rows: t.Iterator[dict[str, t.Any]]) -> Table:
25
+ def __call__(self, rows: t.Iterator[t.Mapping[str, t.Any]]) -> Table:
26
26
  t = Table(*self.kwargs)
27
27
  cols = set(self.kwargs)
28
28
  for r in rows:
@@ -1,30 +1,31 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: recs
3
- Version: 0.3.0
4
- Summary: 🎙 recs: the Universal Recorder 🎙
5
- License: MIT
3
+ Version: 0.10.0
4
+ Summary: 🎙 The Universal Recorder 🎙
6
5
  Author: Tom Ritchford
7
- Author-email: tom@swirly.com
8
- Requires-Python: >=3.10
9
- Classifier: License :: OSI Approved :: MIT License
6
+ Author-email: Tom Ritchford <tom@swirly.com>
7
+ License-Expression: MIT
10
8
  Classifier: Programming Language :: Python :: 3
11
9
  Classifier: Programming Language :: Python :: 3.10
12
10
  Classifier: Programming Language :: Python :: 3.11
13
11
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: coverage
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
15
14
  Requires-Dist: dtyper
16
- Requires-Dist: humanfriendly
17
- Requires-Dist: impall
18
15
  Requires-Dist: numpy
19
- Requires-Dist: overrides
20
16
  Requires-Dist: pyaudio
21
17
  Requires-Dist: rich
22
18
  Requires-Dist: sounddevice
23
19
  Requires-Dist: soundfile
20
+ Requires-Dist: threa>=1.9.0
21
+ Requires-Dist: typer
22
+ Requires-Dist: impall
23
+ Requires-Dist: overrides
24
+ Requires-Dist: coverage
24
25
  Requires-Dist: strenum
25
- Requires-Dist: threa (>=1.9.0)
26
+ Requires-Dist: humanfriendly
26
27
  Requires-Dist: tomli
27
- Requires-Dist: typer
28
+ Requires-Python: >=3.10
28
29
  Description-Content-Type: text/markdown
29
30
 
30
31
  # 🎬 recs: the Universal Recorder 🎬
@@ -138,4 +139,3 @@ detect if a device goes offline and report it.
138
139
  The holy grail is reconnecting to a device that comes back online: this is an
139
140
  [unsolved problem](https://github.com/spatialaudio/python-sounddevice/issues/382)
140
141
  in Python, I believe, but I am on my way to solving it.
141
-
@@ -0,0 +1,53 @@
1
+ recs/.DS_Store,sha256=ugcyo0Wglatna4kMk2Lru97Zf5b1FcsqANtLzQdLnGk,6148
2
+ recs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ recs/__main__.py,sha256=FjhOKF__Z0Bm-L7_DAGcG9WfhxH0A20GLI9w8oLySbY,526
4
+ recs/audio/.DS_Store,sha256=oXSVMh_xJr9RwgcfzWnAFq7w8Bm8f7Rd2_Shw4DUe1E,6148
5
+ recs/audio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ recs/audio/block.py,sha256=1EraKTLC33N0_fLjYKBnPpHAUV4L1GM08nfuGymmdxA,2644
7
+ recs/audio/channel_writer.py,sha256=6_f3PLb5yIoFDxm3aUp_yQI9VsLtnhrhxeNHSWipy5w,7653
8
+ recs/audio/file_opener.py,sha256=90iHmUPMfToD_OBSvpDJECub-zir6vXoxndKurkKK44,1378
9
+ recs/audio/header_size.py,sha256=MC2PhXEjKK9KcXKc9uHLau7oFT7Qvyem5A5XSeA9a6c,412
10
+ recs/base/.DS_Store,sha256=2dmLwn6taDy0mAi9pHfxpVnnpP33CuMp842xjRqMBOw,6148
11
+ recs/base/__init__.py,sha256=B6x8s10ZCO9a4VrMNCzAxNdyKEhQ3YgV7d3FUgDL9PE,120
12
+ recs/base/_query_device.py,sha256=VjsXZcdodxMAZ9iomQcEyiRKV3bjwUnXUSWd1QJ3C6Y,426
13
+ recs/base/cfg_raw.py,sha256=QdsTu8pwu1wdxLqVDh-DfFZcozMtMT5fdr6OwrfTqsw,1429
14
+ recs/base/prefix_dict.py,sha256=hCLvFyfgd8ZaMbN4U9RH3CwxuXng1mIycAoc3n_2JiM,640
15
+ recs/base/pyproject.py,sha256=Qt-zH-Iy3o258UXHH3dNpw32brltE3Zs8KjVH6T20Do,379
16
+ recs/base/state.py,sha256=ksEsiqEeUwV5KIlmg-5pcIKB_urE9DiWgQktZ9fRDws,1851
17
+ recs/base/times.py,sha256=zyj9ufCXUBHtCLBlWsaxuN-e-QL2FRAQpVbbX2JN0dY,1013
18
+ recs/base/type_conversions.py,sha256=YNxqbWZKzhG5d2Tp8HGjkrxNh9TlYb0IdgwB6_BU_mw,791
19
+ recs/base/types.py,sha256=FBMXSlnjuakUONZXTTQCYXs0h-uVyajm59gDda0IJLs,814
20
+ recs/cfg/.DS_Store,sha256=ls8VJ1hKne0S490ZI7YgG6Ppnxg4sXqpIUoXGl4ojDM,6148
21
+ recs/cfg/__init__.py,sha256=DrXVn9QximaFakqtoKZ6o6rieRPccA2u_RBjjrE2u6s,246
22
+ recs/cfg/aliases.py,sha256=lHIMbxGkbUCY8dIeP_a60pgrsUvFCniFrYz8gLVVLz4,2794
23
+ recs/cfg/app.py,sha256=i2TVj9EYYQfjgYb4Ba8WEy5NAk7VPejYcjUXP_8LtfA,2246
24
+ recs/cfg/cfg.py,sha256=yUKAT-XRc87oGzYjDFl49_8orfIwUncExYZsZPpIr0Q,3252
25
+ recs/cfg/cli.py,sha256=cd3xD-yl06jolQKHRbGDoyvQhenuqEFqMHheaGOeLb4,7383
26
+ recs/cfg/device.py,sha256=1SbtLWMGq7z3S52nqbujL2EymCuAX5l8ijU0EbpbP8o,2320
27
+ recs/cfg/file_source.py,sha256=zRfQ8rSytywciFOtcxmDAE4fDDZtibNb3sIqwrFssJQ,1836
28
+ recs/cfg/hash_cmp.py,sha256=2s2woFSx2iVotLYuzsOxz-oKUHcv0K_dWSWdv2AeZKw,456
29
+ recs/cfg/metadata.py,sha256=m3FwKtidrm1RTmPKocwT97J2Yo6nbX-yDwzDHH2BYeE,1469
30
+ recs/cfg/path_pattern.py,sha256=rwYXtXs7O-4J4v7HVhipi8xt8gdZZax_cEGZMjCU-N4,4413
31
+ recs/cfg/run_cli.py,sha256=3yS74CrIrimXP4DnsAQGuOBnFTxjt8rBv2DLTJEo24s,774
32
+ recs/cfg/source.py,sha256=iAvPksApMYgGRwEXxH-YIhr8OCknOIbqQ9srht5HQOA,1024
33
+ recs/cfg/time_settings.py,sha256=6h0To_ypLmI2nQpU9zt7GLDcCRhcr-aE5lx0YbUFOT4,1871
34
+ recs/cfg/track.py,sha256=clxqYKQpAvxfaUKwYPhImkiPEFUeBorggza7yLWjZtM,1908
35
+ recs/misc/.DS_Store,sha256=v8tUVbIwzvuTCSuAPSWAv_xNN21KlXbytYjtL6lhnUU,6148
36
+ recs/misc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ recs/misc/contexts.py,sha256=A9rA4ehlyAjb1n2CL7mmQVh1YLhx24yCoCu6wlAyFac,277
38
+ recs/misc/counter.py,sha256=1BfRZ7S0XHElNO1pJtlT902niJXjGnNWlo4nNLnvtU8,1931
39
+ recs/misc/file_list.py,sha256=R7GYzCUOdlt0QOmpne--PSoQ0E6HPZCHw0YyKdcsul8,711
40
+ recs/misc/legal_filename.py,sha256=7FnaiI3b0S0kkfMq6AiMPWE0MbWesKtP-INJAZ0pwso,413
41
+ recs/misc/log.py,sha256=PlYOXvp1ZB5QpLDdE79C2Ern2kl9DUYQ3p7qxPS1eUA,964
42
+ recs/ui/.DS_Store,sha256=JO-gnxNW68i20yBD2s2Yjog-J37tclO5PYjsvpfeO4k,6148
43
+ recs/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ recs/ui/full_state.py,sha256=TrA_TO59cSBRuMW56zs580Vkow1nbOc8sSTjYrcLtPg,2315
45
+ recs/ui/live.py,sha256=AU-YuuNIV1Hl9wyAZhXXR_Xm8XgZmTRt9KV42vt9IKE,2665
46
+ recs/ui/recorder.py,sha256=bkpZ8JhG8Gr-d2QrB77Pe6-Q_mWukBl-YXvRL6MOF7M,2181
47
+ recs/ui/source_recorder.py,sha256=5axGT4zjdtsbjUla40GP32Ap9iAz0nFxk-MXWqPQybw,2490
48
+ recs/ui/source_tracks.py,sha256=twOKAQLE6_9ERLzNRfudoROFhX5DrunY2EvE02eW3LI,2054
49
+ recs/ui/table.py,sha256=tz7WiJVvTGhoVjUcf1kgmzeaKsCFeeZfYDMZVZ56Yik,877
50
+ recs-0.10.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
51
+ recs-0.10.0.dist-info/entry_points.txt,sha256=aAL8CbBHHwb74UkJneFUiDSjLs9WayruRYao1tQXgR8,44
52
+ recs-0.10.0.dist-info/METADATA,sha256=YBjuM_3xKsQcHsxNoz36qzKlHfa3BNP1Lzw4SfY8ZVU,4816
53
+ recs-0.10.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: uv 0.9.28
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ recs = recs.base.cli:run
3
+