recs 0.2.0__py3-none-any.whl → 0.3.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/__main__.py +27 -4
- recs/audio/block.py +2 -5
- recs/audio/channel_writer.py +94 -78
- recs/audio/file_opener.py +14 -15
- recs/base/_query_device.py +21 -0
- recs/base/cfg_raw.py +16 -14
- recs/base/prefix_dict.py +10 -11
- recs/base/state.py +73 -0
- recs/base/times.py +3 -62
- recs/base/type_conversions.py +5 -5
- recs/base/types.py +2 -7
- recs/cfg/__init__.py +11 -2
- recs/cfg/aliases.py +32 -21
- recs/cfg/app.py +87 -0
- recs/cfg/cfg.py +36 -60
- recs/cfg/cli.py +161 -100
- recs/cfg/device.py +37 -43
- recs/{base → cfg}/metadata.py +23 -2
- recs/cfg/path_pattern.py +156 -0
- recs/cfg/run_cli.py +43 -0
- recs/cfg/time_settings.py +61 -0
- recs/cfg/track.py +4 -4
- recs/misc/contexts.py +10 -0
- recs/misc/counter.py +9 -8
- recs/misc/legal_filename.py +1 -1
- recs/misc/log.py +43 -0
- recs/ui/__init__.py +0 -1
- recs/ui/device_process.py +37 -0
- recs/ui/device_recorder.py +66 -82
- recs/ui/device_tracks.py +9 -1
- recs/ui/full_state.py +50 -0
- recs/ui/live.py +28 -16
- recs/ui/recorder.py +52 -71
- recs/ui/table.py +1 -1
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/METADATA +59 -17
- recs-0.3.0.dist-info/RECORD +47 -0
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/WHEEL +1 -1
- recs/cfg/run.py +0 -32
- recs/misc/recording_path.py +0 -47
- recs/ui/channel_recorder.py +0 -51
- recs-0.2.0.dist-info/RECORD +0 -40
- /recs/{misc → cfg}/hash_cmp.py +0 -0
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/LICENSE +0 -0
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/entry_points.txt +0 -0
recs/__main__.py
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from recs.base import RecsError
|
|
6
|
+
from recs.cfg import app, cli
|
|
7
|
+
|
|
8
|
+
assert cli
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run() -> int:
|
|
12
|
+
try:
|
|
13
|
+
app.app(prog_name='recs', standalone_mode=False)
|
|
14
|
+
return 0
|
|
3
15
|
|
|
4
|
-
|
|
16
|
+
except RecsError as e:
|
|
17
|
+
print('ERROR:', *e.args, file=sys.stderr)
|
|
5
18
|
|
|
6
|
-
|
|
19
|
+
except click.ClickException as e:
|
|
20
|
+
print(f'{e.__class__.__name__}: {e.message}', file=sys.stderr)
|
|
21
|
+
|
|
22
|
+
except click.Abort:
|
|
23
|
+
print('Interrupted', file=sys.stderr)
|
|
24
|
+
|
|
25
|
+
return -1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == '__main__':
|
|
29
|
+
sys.exit(run())
|
recs/audio/block.py
CHANGED
|
@@ -37,7 +37,7 @@ class Block:
|
|
|
37
37
|
|
|
38
38
|
@cached_property
|
|
39
39
|
def volume(self) -> float:
|
|
40
|
-
return sum(self.amplitude) / len(self.amplitude)
|
|
40
|
+
return sum(self.amplitude) / len(self.amplitude)
|
|
41
41
|
|
|
42
42
|
@cached_property
|
|
43
43
|
def channel_count(self) -> int:
|
|
@@ -45,7 +45,7 @@ class Block:
|
|
|
45
45
|
|
|
46
46
|
@cached_property
|
|
47
47
|
def amplitude(self) -> np.ndarray:
|
|
48
|
-
return (self.max - self.min) / 2
|
|
48
|
+
return (self.max - self.min) / (2 * self.scale)
|
|
49
49
|
|
|
50
50
|
@cached_property
|
|
51
51
|
def max(self) -> np.ndarray:
|
|
@@ -99,6 +99,3 @@ class Blocks:
|
|
|
99
99
|
|
|
100
100
|
def __getitem__(self, i: int) -> Block:
|
|
101
101
|
return self.blocks[i]
|
|
102
|
-
|
|
103
|
-
def __len__(self) -> int:
|
|
104
|
-
return len(self.blocks)
|
recs/audio/channel_writer.py
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import typing as t
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from threading import Lock
|
|
5
6
|
|
|
7
|
+
from overrides import override
|
|
6
8
|
from soundfile import SoundFile
|
|
7
9
|
from threa import Runnable
|
|
8
10
|
|
|
9
|
-
from recs.base import
|
|
11
|
+
from recs.base.state import ChannelState
|
|
10
12
|
from recs.base.types import SDTYPE, Active, Format, SdType
|
|
11
|
-
from recs.cfg import Track
|
|
12
|
-
from recs.misc import file_list
|
|
13
|
+
from recs.cfg import Cfg, Track, device, time_settings
|
|
14
|
+
from recs.misc import counter, file_list
|
|
13
15
|
|
|
14
|
-
from ..cfg.cfg import Cfg
|
|
15
16
|
from .block import Block, Blocks
|
|
16
17
|
from .file_opener import FileOpener
|
|
17
18
|
from .header_size import header_size
|
|
@@ -27,7 +28,6 @@ FORMAT_TO_SIZE_LIMIT = {
|
|
|
27
28
|
ITEMSIZE = {
|
|
28
29
|
SdType.float32: 4,
|
|
29
30
|
SdType.int16: 2,
|
|
30
|
-
SdType.int24: 3,
|
|
31
31
|
SdType.int32: 4,
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -35,20 +35,15 @@ BLOCK_FUZZ = 2
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class ChannelWriter(Runnable):
|
|
38
|
-
blocks_seen: int = 0
|
|
39
|
-
blocks_written: int = 0
|
|
40
38
|
bytes_in_this_file: int = 0
|
|
41
39
|
|
|
42
40
|
frames_in_this_file: int = 0
|
|
43
|
-
|
|
44
|
-
frame_size: int = 0
|
|
45
|
-
frames_written: int = 0
|
|
41
|
+
frames_written: int = 0 # Used elsewhere
|
|
46
42
|
|
|
47
43
|
largest_file_size: int = 0
|
|
48
44
|
longest_file_frames: int = 0
|
|
49
45
|
|
|
50
46
|
timestamp: float = 0
|
|
51
|
-
tracknumber: int = 0
|
|
52
47
|
|
|
53
48
|
_sf: SoundFile | None = None
|
|
54
49
|
|
|
@@ -56,10 +51,12 @@ class ChannelWriter(Runnable):
|
|
|
56
51
|
def active(self) -> Active:
|
|
57
52
|
return Active.active if self._sf else Active.inactive
|
|
58
53
|
|
|
59
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self, cfg: Cfg, times: time_settings.TimeSettings[int], track: Track
|
|
56
|
+
) -> None:
|
|
60
57
|
super().__init__()
|
|
61
58
|
|
|
62
|
-
self.
|
|
59
|
+
self.do_not_record = cfg.dry_run or cfg.calibrate
|
|
63
60
|
self.format = cfg.format
|
|
64
61
|
self.metadata = cfg.metadata
|
|
65
62
|
self.times = times
|
|
@@ -72,51 +69,94 @@ class ChannelWriter(Runnable):
|
|
|
72
69
|
self.frame_size = ITEMSIZE[cfg.sdtype or SDTYPE] * len(track.channels)
|
|
73
70
|
self.longest_file_frames = times.longest_file_time
|
|
74
71
|
self.opener = FileOpener(cfg, track)
|
|
72
|
+
self._volume = counter.MovingBlock(times.moving_average_time)
|
|
75
73
|
|
|
76
74
|
if not cfg.infinite_length:
|
|
77
75
|
largest = FORMAT_TO_SIZE_LIMIT.get(cfg.format, 0)
|
|
78
76
|
self.largest_file_size = max(largest - BUFFER, 0)
|
|
79
77
|
|
|
78
|
+
def update(self, update: device.Update) -> ChannelState:
|
|
79
|
+
block = Block(update.array[:, self.track.slice])
|
|
80
|
+
with self._lock:
|
|
81
|
+
return self._receive_block(block, update.timestamp)
|
|
82
|
+
|
|
83
|
+
@override
|
|
80
84
|
def stop(self) -> None:
|
|
81
85
|
with self._lock:
|
|
82
|
-
self.running
|
|
86
|
+
self.running = False
|
|
83
87
|
self._write_and_close()
|
|
84
|
-
self.stopped
|
|
88
|
+
self.stopped = True
|
|
85
89
|
|
|
86
|
-
def
|
|
87
|
-
self.
|
|
88
|
-
|
|
90
|
+
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):
|
|
96
|
+
sf.close()
|
|
97
|
+
with contextlib.suppress(Exception):
|
|
98
|
+
Path(sf.name).unlink()
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
def _open(self, offset: int) -> SoundFile:
|
|
101
|
+
timestamp = self.timestamp - offset / self.track.device.samplerate
|
|
102
|
+
ts = datetime.fromtimestamp(timestamp)
|
|
93
103
|
|
|
94
|
-
|
|
95
|
-
self._write(block, dt)
|
|
104
|
+
index = 1 + len(self.files_written)
|
|
96
105
|
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
metadata = {'date': ts.isoformat(), 'software': URL, 'tracknumber': str(index)}
|
|
107
|
+
metadata |= self.metadata
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
self._write_and_close()
|
|
109
|
+
self.bytes_in_this_file = header_size(metadata, self.format)
|
|
110
|
+
self.frames_in_this_file = 0
|
|
103
111
|
|
|
104
|
-
self.
|
|
112
|
+
sf = self.opener.create(metadata, timestamp, index)
|
|
113
|
+
self.files_written.append(Path(sf.name))
|
|
114
|
+
return sf
|
|
105
115
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
def _receive_block(self, block: Block, timestamp: float) -> ChannelState:
|
|
117
|
+
saved_state = self._state(
|
|
118
|
+
max_amp=max(block.max) / block.scale,
|
|
119
|
+
min_amp=min(block.min) / block.scale,
|
|
120
|
+
)
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
dt = self.timestamp - timestamp
|
|
123
|
+
self.timestamp = timestamp
|
|
124
|
+
self._volume(block)
|
|
114
125
|
|
|
115
|
-
if not self.
|
|
116
|
-
self.
|
|
126
|
+
if not self.do_not_record and (self._sf or not self.stopped):
|
|
127
|
+
expected_dt = len(block) / self.track.device.samplerate
|
|
128
|
+
|
|
129
|
+
if dt > expected_dt * BLOCK_FUZZ: # We were asleep, or otherwise lost time
|
|
130
|
+
self._write_and_close()
|
|
131
|
+
|
|
132
|
+
self._blocks.append(block)
|
|
133
|
+
|
|
134
|
+
if block.volume >= self.times.noise_floor_amplitude:
|
|
135
|
+
if not self._sf: # Record some quiet before the first block
|
|
136
|
+
length = self.times.quiet_before_start + len(self._blocks[-1])
|
|
137
|
+
self._blocks.clip(length, from_start=True)
|
|
138
|
+
|
|
139
|
+
self._write_blocks(self._blocks)
|
|
140
|
+
self._blocks.clear()
|
|
141
|
+
|
|
142
|
+
if self.stopped or self._blocks.duration > self.times.stop_after_quiet:
|
|
143
|
+
self._write_and_close()
|
|
144
|
+
|
|
145
|
+
return self._state() - saved_state
|
|
146
|
+
|
|
147
|
+
def _state(self, **kwargs) -> ChannelState:
|
|
148
|
+
return ChannelState(
|
|
149
|
+
file_count=len(self.files_written),
|
|
150
|
+
file_size=self.files_written.total_size,
|
|
151
|
+
is_active=bool(self._sf),
|
|
152
|
+
recorded_time=self.frames_written / self.track.device.samplerate,
|
|
153
|
+
timestamp=self.timestamp,
|
|
154
|
+
volume=tuple(self._volume.mean()),
|
|
155
|
+
**kwargs,
|
|
156
|
+
)
|
|
117
157
|
|
|
118
158
|
def _write_and_close(self) -> None:
|
|
119
|
-
# Record
|
|
159
|
+
# Record some quiet after the last block
|
|
120
160
|
removed = self._blocks.clip(self.times.quiet_after_end, from_start=False)
|
|
121
161
|
|
|
122
162
|
if self._sf and removed:
|
|
@@ -124,15 +164,13 @@ class ChannelWriter(Runnable):
|
|
|
124
164
|
|
|
125
165
|
self._close()
|
|
126
166
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _write_blocks(self, blocks: t.Iterable[Block]) -> None:
|
|
167
|
+
def _write_blocks(self, blox: t.Iterable[Block]) -> None:
|
|
168
|
+
blocks = list(blox)
|
|
169
|
+
|
|
170
|
+
# The last block in the list ends at self.timestamp so
|
|
171
|
+
# we keep track of the sample offset before that
|
|
172
|
+
offset = -sum(len(b) for b in blocks)
|
|
173
|
+
|
|
136
174
|
for b in blocks:
|
|
137
175
|
# Check if this block will overrun the file size or length
|
|
138
176
|
remains: list[int] = []
|
|
@@ -140,39 +178,17 @@ class ChannelWriter(Runnable):
|
|
|
140
178
|
if self.longest_file_frames:
|
|
141
179
|
remains.append(self.longest_file_frames - self.frames_in_this_file)
|
|
142
180
|
|
|
143
|
-
if self.
|
|
181
|
+
if self._sf and self.largest_file_size:
|
|
144
182
|
file_bytes = self.largest_file_size - self.bytes_in_this_file
|
|
145
183
|
remains.append(file_bytes // self.frame_size)
|
|
146
184
|
|
|
147
|
-
if remains and
|
|
148
|
-
self._write_one(b[:r])
|
|
185
|
+
if remains and min(remains) <= len(b):
|
|
149
186
|
self._close()
|
|
150
|
-
b = b[r:]
|
|
151
|
-
|
|
152
|
-
if b:
|
|
153
|
-
self._write_one(b)
|
|
154
|
-
|
|
155
|
-
def _write_one(self, b: Block) -> None:
|
|
156
|
-
if not self._sf:
|
|
157
|
-
self.tracknumber += 1
|
|
158
|
-
t = str(self.tracknumber)
|
|
159
|
-
# TODO: the timestamp will be a bit late for this block
|
|
160
|
-
# because self.timestamp is the time of the last block
|
|
161
|
-
# in the list!
|
|
162
|
-
ts = datetime.fromtimestamp(self.timestamp)
|
|
163
|
-
metadata = dict(date=ts.isoformat(), software=URL, tracknumber=t)
|
|
164
|
-
metadata |= self.metadata
|
|
165
|
-
|
|
166
|
-
self._sf = self.opener.create(metadata, timestamp=self.timestamp)
|
|
167
|
-
self.bytes_in_this_file = header_size(metadata, self.format)
|
|
168
|
-
|
|
169
|
-
self.files_written.append(Path(self._sf.name))
|
|
170
|
-
self.frames_in_this_file = 0
|
|
171
|
-
|
|
172
|
-
self._sf.write(b.block)
|
|
173
187
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
self._sf = self._sf or self._open(offset)
|
|
189
|
+
self._sf.write(b.block)
|
|
190
|
+
offset += len(b)
|
|
177
191
|
|
|
178
|
-
|
|
192
|
+
self.frames_in_this_file += len(b)
|
|
193
|
+
self.frames_written += len(b)
|
|
194
|
+
self.bytes_in_this_file += len(b) * self.frame_size
|
recs/audio/file_opener.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import itertools
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import soundfile as sf
|
|
4
5
|
|
|
5
6
|
from recs.cfg import Cfg, Track
|
|
6
|
-
from recs.
|
|
7
|
+
from recs.cfg.metadata import ALLOWS_METADATA
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class FileOpener:
|
|
@@ -27,23 +28,21 @@ class FileOpener:
|
|
|
27
28
|
subtype=self.cfg.subtype,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
if self.cfg.format in ALLOWS_METADATA:
|
|
32
|
+
for k, v in metadata.items():
|
|
33
|
+
setattr(fp, k, v)
|
|
32
34
|
|
|
33
35
|
return fp
|
|
34
36
|
|
|
35
|
-
def create(
|
|
36
|
-
index
|
|
37
|
-
|
|
38
|
-
path,
|
|
39
|
-
|
|
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
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
p.parent.mkdir(exist_ok=True, parents=True)
|
|
43
|
+
for i in itertools.count():
|
|
44
|
+
f = p.parent / (p.name + bool(i) * f'_{i}')
|
|
45
45
|
try:
|
|
46
|
-
return self.open(
|
|
46
|
+
return self.open(f, metadata)
|
|
47
47
|
except FileExistsError:
|
|
48
|
-
|
|
49
|
-
suffix = f'_{index}'
|
|
48
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Print the current devices as JSON without loading any other part of recs.
|
|
3
|
+
|
|
4
|
+
Called repeatedly as a subprocess to detect devices going off- and online.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import typing as t
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def query_devices() -> list[dict[str, t.Any]]:
|
|
12
|
+
try:
|
|
13
|
+
import sounddevice
|
|
14
|
+
|
|
15
|
+
return sounddevice.query_devices()
|
|
16
|
+
except Exception:
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == '__main__':
|
|
21
|
+
print(json.dumps(query_devices(), indent=4))
|
recs/base/cfg_raw.py
CHANGED
|
@@ -11,15 +11,15 @@ class CfgRaw:
|
|
|
11
11
|
#
|
|
12
12
|
# Directory settings
|
|
13
13
|
#
|
|
14
|
-
path:
|
|
15
|
-
subdirectory: t.Sequence[str] = ()
|
|
14
|
+
path: str = ''
|
|
16
15
|
#
|
|
17
16
|
# General purpose settings
|
|
18
17
|
#
|
|
18
|
+
calibrate: bool = False
|
|
19
19
|
dry_run: bool = False
|
|
20
|
+
verbose: bool = False
|
|
20
21
|
info: bool = False
|
|
21
22
|
list_types: bool = False
|
|
22
|
-
verbose: bool = False
|
|
23
23
|
#
|
|
24
24
|
# Aliases for input devices or channels
|
|
25
25
|
#
|
|
@@ -40,19 +40,21 @@ class CfgRaw:
|
|
|
40
40
|
#
|
|
41
41
|
# Console and UI settings
|
|
42
42
|
#
|
|
43
|
+
clear: bool = False
|
|
43
44
|
silent: bool = False
|
|
44
|
-
|
|
45
|
-
ui_refresh_rate: float = 23
|
|
46
|
-
sleep_time: float = 0.013
|
|
45
|
+
sleep_time_device: float = 0.1
|
|
46
|
+
ui_refresh_rate: float = 23.0
|
|
47
47
|
#
|
|
48
48
|
# Settings relating to times
|
|
49
49
|
#
|
|
50
50
|
infinite_length: bool = False
|
|
51
|
-
longest_file_time:
|
|
52
|
-
moving_average_time: float = 1
|
|
53
|
-
noise_floor: float = 70
|
|
54
|
-
shortest_file_time:
|
|
55
|
-
quiet_after_end: float = 2
|
|
56
|
-
quiet_before_start: float = 1
|
|
57
|
-
stop_after_quiet: float = 20
|
|
58
|
-
total_run_time: float = 0
|
|
51
|
+
longest_file_time: float = 0.0
|
|
52
|
+
moving_average_time: float = 1.0
|
|
53
|
+
noise_floor: float = 70.0
|
|
54
|
+
shortest_file_time: float = 1.0
|
|
55
|
+
quiet_after_end: float = 2.0
|
|
56
|
+
quiet_before_start: float = 1.0
|
|
57
|
+
stop_after_quiet: float = 20.0
|
|
58
|
+
total_run_time: float = 0.0
|
|
59
|
+
|
|
60
|
+
asdict = dc.asdict
|
recs/base/prefix_dict.py
CHANGED
|
@@ -8,20 +8,19 @@ class PrefixDict(dict[str, T]):
|
|
|
8
8
|
try:
|
|
9
9
|
return super().__getitem__(key)
|
|
10
10
|
except KeyError:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
error = 'unknown'
|
|
14
|
+
if key := key.strip().lower():
|
|
14
15
|
try:
|
|
15
16
|
return super().__getitem__(key)
|
|
16
17
|
except KeyError:
|
|
17
18
|
pass
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
matches = [v for k, v in self.items() if k.lower().startswith(key)]
|
|
21
|
+
if len(matches) == 1:
|
|
22
|
+
return matches[0]
|
|
23
|
+
if matches:
|
|
24
|
+
error = 'ambiguous'
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
try:
|
|
25
|
-
return self[key]
|
|
26
|
-
except KeyError:
|
|
27
|
-
return default
|
|
26
|
+
raise KeyError(key, error)
|
recs/base/state.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from recs.cfg.time_settings import amplitude_to_db
|
|
5
|
+
|
|
6
|
+
INF = float('inf')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dc.dataclass(slots=True)
|
|
10
|
+
class ChannelState:
|
|
11
|
+
"""Represents the state of a single recording channel"""
|
|
12
|
+
|
|
13
|
+
condition: str = 'running'
|
|
14
|
+
|
|
15
|
+
file_count: int = 0
|
|
16
|
+
file_size: int = 0
|
|
17
|
+
|
|
18
|
+
is_active: bool = False
|
|
19
|
+
|
|
20
|
+
max_amp: float = -INF
|
|
21
|
+
min_amp: float = INF
|
|
22
|
+
|
|
23
|
+
recorded_time: float = 0
|
|
24
|
+
timestamp: float = dc.field(default_factory=time.time)
|
|
25
|
+
volume: tuple[float, ...] = ()
|
|
26
|
+
|
|
27
|
+
replace = dc.replace
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def amp(self) -> float:
|
|
31
|
+
if self.max_amp == -INF or self.min_amp == INF:
|
|
32
|
+
return 0
|
|
33
|
+
return (self.max_amp - self.min_amp) / 2
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def db_range(self) -> float:
|
|
37
|
+
return amplitude_to_db(self.amp)
|
|
38
|
+
|
|
39
|
+
def __iadd__(self, m: 'ChannelState') -> 'ChannelState':
|
|
40
|
+
self.file_count += m.file_count
|
|
41
|
+
self.file_size += m.file_size
|
|
42
|
+
self.recorded_time += m.recorded_time
|
|
43
|
+
|
|
44
|
+
# We copy these three when using +=, but not -=!
|
|
45
|
+
self.condition = m.condition
|
|
46
|
+
self.is_active = m.is_active
|
|
47
|
+
self.timestamp = m.timestamp
|
|
48
|
+
self.volume = m.volume
|
|
49
|
+
|
|
50
|
+
self.max_amp = max(self.max_amp, m.max_amp)
|
|
51
|
+
self.min_amp = min(self.min_amp, m.min_amp)
|
|
52
|
+
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __isub__(self, m: 'ChannelState') -> 'ChannelState':
|
|
56
|
+
self.file_count -= m.file_count
|
|
57
|
+
self.file_size -= m.file_size
|
|
58
|
+
self.recorded_time -= m.recorded_time
|
|
59
|
+
|
|
60
|
+
self.max_amp = max(self.max_amp, m.max_amp)
|
|
61
|
+
self.min_amp = min(self.min_amp, m.min_amp)
|
|
62
|
+
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __add__(self, m: 'ChannelState') -> 'ChannelState':
|
|
66
|
+
x = self.replace()
|
|
67
|
+
x += m
|
|
68
|
+
return x
|
|
69
|
+
|
|
70
|
+
def __sub__(self, m: 'ChannelState') -> 'ChannelState':
|
|
71
|
+
x = self.replace()
|
|
72
|
+
x -= m
|
|
73
|
+
return x
|
recs/base/times.py
CHANGED
|
@@ -1,66 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import math
|
|
3
|
-
import time as _time
|
|
4
|
-
import typing as t
|
|
5
|
-
from functools import cached_property
|
|
1
|
+
import time
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
sleep = _time.sleep
|
|
11
|
-
time = _time.time
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def db_to_amplitude(db: float) -> float:
|
|
15
|
-
return 10 ** (-db / 20)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def amplitude_to_db(amp: float) -> float:
|
|
19
|
-
return -20 * math.log10(amp)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@dc.dataclass(frozen=True)
|
|
23
|
-
class TimeSettings(t.Generic[T]):
|
|
24
|
-
"""Amounts of time are specified as seconds in the input but converted
|
|
25
|
-
to samples when we find out the sample rate
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
#: Longest amount of time per file: 0 means infinite
|
|
29
|
-
longest_file_time: T = t.cast(T, 0)
|
|
30
|
-
|
|
31
|
-
#: Shortest amount of time per file
|
|
32
|
-
shortest_file_time: T = t.cast(T, 0)
|
|
33
|
-
|
|
34
|
-
#: Amount of quiet at the start
|
|
35
|
-
quiet_before_start: T = t.cast(T, 0)
|
|
36
|
-
|
|
37
|
-
#: Amount of quiet at the end
|
|
38
|
-
quiet_after_end: T = t.cast(T, 0)
|
|
39
|
-
|
|
40
|
-
#: Amount of quiet before stopping a recording
|
|
41
|
-
stop_after_quiet: T = t.cast(T, 0)
|
|
42
|
-
|
|
43
|
-
# Time for moving averages for the meters
|
|
44
|
-
moving_average_time: T = t.cast(T, 0)
|
|
45
|
-
|
|
46
|
-
#: The noise floor in decibels
|
|
47
|
-
noise_floor: float = 70
|
|
48
|
-
|
|
49
|
-
#: Amount of total time to run. 0 or less means "run forever"
|
|
50
|
-
total_run_time: T = t.cast(T, 0)
|
|
51
|
-
|
|
52
|
-
@cached_property
|
|
53
|
-
def noise_floor_amplitude(self) -> float:
|
|
54
|
-
return db_to_amplitude(self.noise_floor)
|
|
55
|
-
|
|
56
|
-
def __post_init__(self):
|
|
57
|
-
if negative_fields := [k for k, v in dc.asdict(self).items() if v < 0]:
|
|
58
|
-
raise ValueError(f'TimeSettings cannot be negative: {negative_fields=}')
|
|
59
|
-
|
|
60
|
-
def scale(self, samplerate: float | int) -> 'TimeSettings[int]':
|
|
61
|
-
it = dc.asdict(self).items()
|
|
62
|
-
d = {k: v if k in NO_SCALE else round(samplerate * v) for k, v in it}
|
|
63
|
-
return TimeSettings[int](**d)
|
|
3
|
+
sleep = time.sleep
|
|
4
|
+
timestamp = time.time
|
|
64
5
|
|
|
65
6
|
|
|
66
7
|
def to_time(t: str) -> float:
|
recs/base/type_conversions.py
CHANGED
|
@@ -2,26 +2,27 @@ from .prefix_dict import PrefixDict
|
|
|
2
2
|
from .types import Format, SdType, Subtype
|
|
3
3
|
|
|
4
4
|
FORMATS = PrefixDict[Format]({str(s): s for s in Format})
|
|
5
|
+
SDTYPES = PrefixDict[SdType]({str(s): s for s in SdType})
|
|
5
6
|
SUBTYPES = PrefixDict[Subtype]({str(s): s for s in Subtype})
|
|
6
7
|
|
|
7
8
|
SUBTYPE_TO_SDTYPE = {
|
|
8
9
|
Subtype.alac_16: SdType.int16,
|
|
9
10
|
Subtype.alac_20: SdType.int32,
|
|
10
|
-
Subtype.alac_24: SdType.
|
|
11
|
+
Subtype.alac_24: SdType.int32,
|
|
11
12
|
Subtype.alac_32: SdType.int32,
|
|
12
13
|
Subtype.double: SdType.float32,
|
|
13
14
|
Subtype.dpcm_16: SdType.int16,
|
|
14
15
|
Subtype.dpcm_8: SdType.int16,
|
|
15
16
|
Subtype.dwvw_12: SdType.int16,
|
|
16
17
|
Subtype.dwvw_16: SdType.int16,
|
|
17
|
-
Subtype.dwvw_24: SdType.
|
|
18
|
+
Subtype.dwvw_24: SdType.int32,
|
|
18
19
|
Subtype.float: SdType.float32,
|
|
19
20
|
Subtype.ms_adpcm: SdType.int32,
|
|
20
21
|
Subtype.nms_adpcm_16: SdType.int16,
|
|
21
|
-
Subtype.nms_adpcm_24: SdType.
|
|
22
|
+
Subtype.nms_adpcm_24: SdType.int32,
|
|
22
23
|
Subtype.nms_adpcm_32: SdType.int32,
|
|
23
24
|
Subtype.pcm_16: SdType.int16,
|
|
24
|
-
Subtype.pcm_24: SdType.
|
|
25
|
+
Subtype.pcm_24: SdType.int32,
|
|
25
26
|
Subtype.pcm_32: SdType.int32,
|
|
26
27
|
Subtype.pcm_s8: SdType.int16,
|
|
27
28
|
Subtype.pcm_u8: SdType.int16,
|
|
@@ -29,7 +30,6 @@ SUBTYPE_TO_SDTYPE = {
|
|
|
29
30
|
|
|
30
31
|
SDTYPE_TO_SUBTYPE = {
|
|
31
32
|
SdType.int16: Subtype.pcm_16,
|
|
32
|
-
SdType.int24: Subtype.pcm_24,
|
|
33
33
|
SdType.int32: Subtype.pcm_32,
|
|
34
34
|
SdType.float32: Subtype.float,
|
|
35
35
|
}
|
recs/base/types.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import typing as t
|
|
1
2
|
from enum import auto
|
|
2
3
|
|
|
3
4
|
from strenum import StrEnum
|
|
4
5
|
|
|
5
6
|
DeviceDict = dict[str, float | int | str]
|
|
7
|
+
Stop = t.Callable[[], None]
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class Active(StrEnum):
|
|
@@ -14,7 +16,6 @@ class Active(StrEnum):
|
|
|
14
16
|
class SdType(StrEnum):
|
|
15
17
|
float32 = auto()
|
|
16
18
|
int16 = auto()
|
|
17
|
-
int24 = auto()
|
|
18
19
|
int32 = auto()
|
|
19
20
|
|
|
20
21
|
|
|
@@ -80,9 +81,3 @@ class Subtype(StrEnum):
|
|
|
80
81
|
ulaw = auto()
|
|
81
82
|
vorbis = auto()
|
|
82
83
|
vox_adpcm = auto()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class Subdirectory(StrEnum):
|
|
86
|
-
channel = auto()
|
|
87
|
-
device = auto()
|
|
88
|
-
time = auto()
|