recs 0.3.1__tar.gz → 0.10.0__tar.gz
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-0.3.1 → recs-0.10.0}/PKG-INFO +14 -14
- recs-0.10.0/pyproject.toml +67 -0
- recs-0.10.0/recs/.DS_Store +0 -0
- recs-0.10.0/recs/audio/.DS_Store +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/audio/block.py +6 -4
- recs-0.10.0/recs/audio/channel_writer.py +238 -0
- recs-0.10.0/recs/audio/file_opener.py +50 -0
- {recs-0.3.1 → recs-0.10.0}/recs/audio/header_size.py +7 -7
- recs-0.10.0/recs/base/.DS_Store +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/base/_query_device.py +3 -3
- {recs-0.3.1 → recs-0.10.0}/recs/base/cfg_raw.py +5 -4
- {recs-0.3.1 → recs-0.10.0}/recs/base/pyproject.py +1 -1
- {recs-0.3.1 → recs-0.10.0}/recs/base/type_conversions.py +0 -9
- {recs-0.3.1 → recs-0.10.0}/recs/base/types.py +2 -34
- recs-0.10.0/recs/cfg/.DS_Store +0 -0
- recs-0.10.0/recs/cfg/__init__.py +8 -0
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/aliases.py +13 -17
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/app.py +7 -5
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/cfg.py +24 -12
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/cli.py +37 -18
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/device.py +30 -42
- recs-0.10.0/recs/cfg/file_source.py +61 -0
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/hash_cmp.py +2 -2
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/metadata.py +2 -5
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/path_pattern.py +13 -5
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/run_cli.py +8 -17
- recs-0.10.0/recs/cfg/source.py +42 -0
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/time_settings.py +4 -1
- {recs-0.3.1 → recs-0.10.0}/recs/cfg/track.py +12 -9
- recs-0.10.0/recs/misc/.DS_Store +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/misc/contexts.py +1 -1
- {recs-0.3.1 → recs-0.10.0}/recs/misc/counter.py +12 -4
- {recs-0.3.1 → recs-0.10.0}/recs/misc/file_list.py +1 -1
- {recs-0.3.1 → recs-0.10.0}/recs/misc/log.py +5 -5
- recs-0.10.0/recs/ui/.DS_Store +0 -0
- recs-0.10.0/recs/ui/__init__.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/ui/full_state.py +11 -6
- {recs-0.3.1 → recs-0.10.0}/recs/ui/live.py +9 -9
- recs-0.10.0/recs/ui/recorder.py +65 -0
- recs-0.10.0/recs/ui/source_recorder.py +76 -0
- recs-0.3.1/recs/ui/device_tracks.py → recs-0.10.0/recs/ui/source_tracks.py +19 -17
- {recs-0.3.1 → recs-0.10.0}/recs/ui/table.py +2 -2
- recs-0.3.1/LICENSE +0 -21
- recs-0.3.1/pyproject.toml +0 -77
- recs-0.3.1/recs/audio/channel_writer.py +0 -194
- recs-0.3.1/recs/audio/file_opener.py +0 -48
- recs-0.3.1/recs/cfg/__init__.py +0 -15
- recs-0.3.1/recs/misc/__init__.py +0 -1
- recs-0.3.1/recs/ui/device_process.py +0 -37
- recs-0.3.1/recs/ui/device_recorder.py +0 -83
- recs-0.3.1/recs/ui/recorder.py +0 -74
- {recs-0.3.1 → recs-0.10.0}/README.md +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/__init__.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/__main__.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/audio/__init__.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/base/__init__.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/base/prefix_dict.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/base/state.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/base/times.py +0 -0
- {recs-0.3.1/recs/ui → recs-0.10.0/recs/misc}/__init__.py +0 -0
- {recs-0.3.1 → recs-0.10.0}/recs/misc/legal_filename.py +0 -0
|
@@ -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,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "recs"
|
|
3
|
+
version = "0.10.0"
|
|
4
|
+
description = "🎙 The Universal Recorder 🎙"
|
|
5
|
+
authors = [{ name = "Tom Ritchford", email = "tom@swirly.com" }]
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
classifiers = ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14"]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"dtyper",
|
|
12
|
+
"numpy",
|
|
13
|
+
"pyaudio",
|
|
14
|
+
"rich",
|
|
15
|
+
"sounddevice",
|
|
16
|
+
"soundfile",
|
|
17
|
+
"threa>=1.9.0",
|
|
18
|
+
"typer",
|
|
19
|
+
"impall",
|
|
20
|
+
"overrides",
|
|
21
|
+
"coverage",
|
|
22
|
+
"strenum",
|
|
23
|
+
"humanfriendly",
|
|
24
|
+
"tomli",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
recs = "recs.base.cli:run"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"coverage>=7.4.1",
|
|
33
|
+
"pytest",
|
|
34
|
+
"pyupgrade>=3.21.2",
|
|
35
|
+
"ruff",
|
|
36
|
+
"tdir",
|
|
37
|
+
"ty>=0.0.14",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.uv]
|
|
41
|
+
|
|
42
|
+
[tool.uv.build-backend]
|
|
43
|
+
module-root = ""
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["uv_build>=0.9.0,<0.10.0"]
|
|
47
|
+
build-backend = "uv_build"
|
|
48
|
+
|
|
49
|
+
[tool.coverage.run]
|
|
50
|
+
branch = true
|
|
51
|
+
source = ["recs"]
|
|
52
|
+
omit = [
|
|
53
|
+
"recs/__main__.py",
|
|
54
|
+
"recs/cfg/cli.py",
|
|
55
|
+
"recs/cfg/app.py",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.report]
|
|
59
|
+
fail_under = 91
|
|
60
|
+
skip_covered = true
|
|
61
|
+
exclude_lines = ["pragma: no cover", "if False:", "if __name__ == .__main__.:", "raise NotImplementedError"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 88
|
|
65
|
+
|
|
66
|
+
[tool.ruff.format]
|
|
67
|
+
quote-style = "single"
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import sys
|
|
3
|
+
import typing as t
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from threading import Lock
|
|
7
|
+
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
from overrides import override
|
|
10
|
+
from soundfile import SoundFile
|
|
11
|
+
from threa import Runnable
|
|
12
|
+
|
|
13
|
+
from recs.base.state import ChannelState
|
|
14
|
+
from recs.base.type_conversions import SUBTYPE_TO_SDTYPE
|
|
15
|
+
from recs.base.types import SDTYPE, Active, Format, SdType
|
|
16
|
+
from recs.cfg import Cfg, Track, time_settings
|
|
17
|
+
from recs.misc import counter, file_list
|
|
18
|
+
|
|
19
|
+
from .block import Block, Blocks
|
|
20
|
+
from .file_opener import FileOpener
|
|
21
|
+
from .header_size import header_size
|
|
22
|
+
|
|
23
|
+
URL = 'https://github.com/rec/recs'
|
|
24
|
+
|
|
25
|
+
BUFFER = 0x80
|
|
26
|
+
MAX_WAV_SIZE = 0x1_0000_0000 - BUFFER
|
|
27
|
+
|
|
28
|
+
ITEMSIZE = {
|
|
29
|
+
SdType.float32: 4,
|
|
30
|
+
SdType.int16: 2,
|
|
31
|
+
SdType.int32: 4,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
BLOCK_FUZZ = 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ChannelWriter(Runnable):
|
|
38
|
+
bytes_in_file: int = 0
|
|
39
|
+
|
|
40
|
+
frames_in_file: int = 0
|
|
41
|
+
frames_written: int = 0 # Used elsewhere
|
|
42
|
+
|
|
43
|
+
largest_file_size: int = 0
|
|
44
|
+
longest_file_frames: int = 0
|
|
45
|
+
|
|
46
|
+
timestamp: float = 0
|
|
47
|
+
|
|
48
|
+
_sfs: t.Sequence[SoundFile] = ()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def active(self) -> Active:
|
|
52
|
+
return Active.active if self._sfs else Active.inactive
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self, cfg: Cfg, times: time_settings.TimeSettings[int], track: Track
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__()
|
|
58
|
+
|
|
59
|
+
self.cfg = cfg
|
|
60
|
+
self.do_not_record = cfg.dry_run or cfg.calibrate
|
|
61
|
+
self.metadata = cfg.metadata
|
|
62
|
+
self.times = times
|
|
63
|
+
self.track = track
|
|
64
|
+
|
|
65
|
+
self._blocks = Blocks()
|
|
66
|
+
self._lock = Lock()
|
|
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
|
+
|
|
83
|
+
self.files_written = file_list.FileList()
|
|
84
|
+
self.frame_size = ITEMSIZE[sdtype] * len(track.channels)
|
|
85
|
+
self.longest_file_frames = times.longest_file_time
|
|
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
|
+
]
|
|
96
|
+
self._volume = counter.MovingBlock(times.moving_average_time)
|
|
97
|
+
|
|
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])
|
|
107
|
+
|
|
108
|
+
def receive_update(
|
|
109
|
+
self, block: Block, timestamp: float, should_record: bool = False
|
|
110
|
+
) -> ChannelState:
|
|
111
|
+
with self._lock:
|
|
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
|
+
)
|
|
120
|
+
|
|
121
|
+
@override
|
|
122
|
+
def stop(self) -> None:
|
|
123
|
+
with self._lock:
|
|
124
|
+
self.running = False
|
|
125
|
+
self._write_and_close()
|
|
126
|
+
self.stopped = True
|
|
127
|
+
|
|
128
|
+
def _close(self) -> None:
|
|
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)
|
|
133
|
+
sf.close()
|
|
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()
|
|
143
|
+
index = 1 + len(self.files_written)
|
|
144
|
+
metadata = {'date': date, 'software': URL, 'tracknumber': str(index)}
|
|
145
|
+
metadata |= self.metadata
|
|
146
|
+
|
|
147
|
+
self.bytes_in_file = max(header_size(metadata, f) for f in self.cfg.formats)
|
|
148
|
+
self.frames_in_file = 0
|
|
149
|
+
|
|
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
|
|
156
|
+
|
|
157
|
+
def _receive_block(
|
|
158
|
+
self, block: Block, timestamp: float, should_record: bool
|
|
159
|
+
) -> ChannelState:
|
|
160
|
+
saved_state = self._state(
|
|
161
|
+
max_amp=max(block.max) / block.scale,
|
|
162
|
+
min_amp=min(block.min) / block.scale,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
dt = self.timestamp - timestamp
|
|
166
|
+
self.timestamp = timestamp
|
|
167
|
+
self._volume(block)
|
|
168
|
+
|
|
169
|
+
if not self.do_not_record and (self._sfs or not self.stopped):
|
|
170
|
+
expected_dt = len(block) / self.track.source.samplerate
|
|
171
|
+
|
|
172
|
+
if dt > expected_dt * BLOCK_FUZZ: # We were asleep, or otherwise lost time
|
|
173
|
+
self._write_and_close()
|
|
174
|
+
|
|
175
|
+
self._blocks.append(block)
|
|
176
|
+
|
|
177
|
+
if should_record:
|
|
178
|
+
if not self._sfs: # Record some quiet before the first block
|
|
179
|
+
length = self.times.quiet_before_start + len(self._blocks[-1])
|
|
180
|
+
self._blocks.clip(length, from_start=True)
|
|
181
|
+
|
|
182
|
+
self._write_blocks(self._blocks)
|
|
183
|
+
self._blocks.clear()
|
|
184
|
+
|
|
185
|
+
if self.stopped or self._blocks.duration > self.times.stop_after_quiet:
|
|
186
|
+
self._write_and_close()
|
|
187
|
+
|
|
188
|
+
return self._state() - saved_state
|
|
189
|
+
|
|
190
|
+
def _state(self, **kwargs: t.Any) -> ChannelState:
|
|
191
|
+
return ChannelState(
|
|
192
|
+
file_count=len(self.files_written),
|
|
193
|
+
file_size=self.files_written.total_size,
|
|
194
|
+
is_active=bool(self._sfs),
|
|
195
|
+
recorded_time=self.frames_written / self.track.source.samplerate,
|
|
196
|
+
timestamp=self.timestamp,
|
|
197
|
+
volume=tuple(self._volume.mean()),
|
|
198
|
+
**kwargs,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _write_and_close(self) -> None:
|
|
202
|
+
# Record some quiet after the last block
|
|
203
|
+
removed = self._blocks.clip(self.times.quiet_after_end, from_start=False)
|
|
204
|
+
|
|
205
|
+
if self._sfs and removed:
|
|
206
|
+
self._write_blocks(reversed(removed))
|
|
207
|
+
|
|
208
|
+
self._close()
|
|
209
|
+
|
|
210
|
+
def _write_blocks(self, blox: t.Iterable[Block]) -> None:
|
|
211
|
+
blocks = list(blox)
|
|
212
|
+
|
|
213
|
+
# The last block in the list ends at self.timestamp so
|
|
214
|
+
# we keep track of the sample offset before that
|
|
215
|
+
offset = -sum(len(b) for b in blocks)
|
|
216
|
+
|
|
217
|
+
for b in blocks:
|
|
218
|
+
# Check if this block will overrun the file size or length
|
|
219
|
+
remains: list[int] = []
|
|
220
|
+
|
|
221
|
+
if self.longest_file_frames:
|
|
222
|
+
remains.append(self.longest_file_frames - self.frames_in_file)
|
|
223
|
+
|
|
224
|
+
if self._sfs and self.largest_file_size:
|
|
225
|
+
file_bytes = self.largest_file_size - self.bytes_in_file
|
|
226
|
+
remains.append(file_bytes // self.frame_size)
|
|
227
|
+
|
|
228
|
+
if remains and min(remains) <= len(b):
|
|
229
|
+
self._close()
|
|
230
|
+
|
|
231
|
+
self._sfs = self._sfs or self._open(offset)
|
|
232
|
+
for sf in self._sfs:
|
|
233
|
+
sf.write(b.block)
|
|
234
|
+
offset += len(b)
|
|
235
|
+
|
|
236
|
+
self.frames_in_file += len(b)
|
|
237
|
+
self.frames_written += len(b)
|
|
238
|
+
self.bytes_in_file += len(b) * self.frame_size
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import itertools
|
|
3
|
+
import typing as t
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import soundfile
|
|
7
|
+
|
|
8
|
+
from recs.base.types import Format, Subtype
|
|
9
|
+
from recs.cfg.metadata import ALLOWS_METADATA
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dc.dataclass
|
|
13
|
+
class FileOpener:
|
|
14
|
+
format: Format
|
|
15
|
+
channels: int = 1
|
|
16
|
+
samplerate: int = 48_000
|
|
17
|
+
subtype: Subtype | None = None
|
|
18
|
+
|
|
19
|
+
def open(
|
|
20
|
+
self, path: Path | str, metadata: t.Mapping[str, str], overwrite: bool = False
|
|
21
|
+
) -> soundfile.SoundFile:
|
|
22
|
+
path = Path(path).with_suffix('.' + self.format)
|
|
23
|
+
if not overwrite and path.exists():
|
|
24
|
+
raise FileExistsError(str(path))
|
|
25
|
+
|
|
26
|
+
fp = soundfile.SoundFile(
|
|
27
|
+
channels=self.channels,
|
|
28
|
+
file=path,
|
|
29
|
+
format=self.format,
|
|
30
|
+
mode='w',
|
|
31
|
+
samplerate=self.samplerate,
|
|
32
|
+
subtype=self.subtype,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if self.format in ALLOWS_METADATA:
|
|
36
|
+
for k, v in metadata.items():
|
|
37
|
+
setattr(fp, k, v)
|
|
38
|
+
|
|
39
|
+
return fp
|
|
40
|
+
|
|
41
|
+
def create(self, metadata: t.Mapping[str, str], path: Path) -> soundfile.SoundFile:
|
|
42
|
+
path.parent.mkdir(exist_ok=True, parents=True)
|
|
43
|
+
|
|
44
|
+
for i in itertools.count():
|
|
45
|
+
f = path.parent / (path.name + bool(i) * f'_{i}')
|
|
46
|
+
try:
|
|
47
|
+
return self.open(f, metadata)
|
|
48
|
+
except FileExistsError:
|
|
49
|
+
pass
|
|
50
|
+
raise FileNotFoundError
|
|
@@ -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:
|
|
5
|
-
|
|
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
|
|
|
Binary file
|
|
@@ -8,14 +8,14 @@ import json
|
|
|
8
8
|
import typing as t
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def _query_devices() -> t.Any:
|
|
12
12
|
try:
|
|
13
13
|
import sounddevice
|
|
14
14
|
|
|
15
15
|
return sounddevice.query_devices()
|
|
16
|
-
except
|
|
16
|
+
except BaseException:
|
|
17
17
|
return []
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
if __name__ == '__main__':
|
|
21
|
-
print(json.dumps(
|
|
21
|
+
print(json.dumps(_query_devices(), indent=4))
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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,
|
|
@@ -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
|
-
|
|
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()
|
|
Binary file
|