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.
- 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.0.dist-info → recs-0.10.0.dist-info}/METADATA +14 -14
- recs-0.10.0.dist-info/RECORD +53 -0
- {recs-0.3.0.dist-info → recs-0.10.0.dist-info}/WHEEL +1 -1
- recs-0.10.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.0.dist-info/LICENSE +0 -21
- recs-0.3.0.dist-info/RECORD +0 -47
- 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 .
|
|
4
|
+
from .source import Source
|
|
5
5
|
|
|
6
6
|
__all__ = ('Track',)
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Track(hash_cmp.HashCmp):
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
17
|
+
self.channels = _channels(channels, source.name, source.channels)
|
|
16
18
|
else:
|
|
17
19
|
self.channels = channels
|
|
18
20
|
|
|
19
|
-
self._key =
|
|
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.
|
|
33
|
-
return self.
|
|
34
|
+
return f'{self.source.name} + {self.name}'
|
|
35
|
+
return self.source.name
|
|
34
36
|
|
|
35
37
|
def __repr__(self) -> str:
|
|
36
|
-
return f
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
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
|
|
5
|
+
from recs.cfg import Source, Track
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class FullState:
|
|
9
|
-
def __init__(self, tracks:
|
|
10
|
-
def device_state(t) -> dict[str, state.ChannelState]:
|
|
11
|
-
return {i.name: state.ChannelState() for i in
|
|
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
|
|
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:
|
|
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__(
|
|
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,
|
|
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
|
-
|
|
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(
|
|
23
|
-
self.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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.
|
|
31
|
-
incs = [i for i in inc if d.name == i.
|
|
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[
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: recs
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: 🎙
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
26
|
+
Requires-Dist: humanfriendly
|
|
26
27
|
Requires-Dist: tomli
|
|
27
|
-
Requires-
|
|
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,,
|