asmu 0.0.1__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.
Potentially problematic release.
This version of asmu might be problematic. Click here for more details.
- asmu/__init__.py +9 -0
- asmu/analysis.py +68 -0
- asmu/generator.py +158 -0
- asmu/inout.py +79 -0
- asmu/interface.py +107 -0
- asmu/recording.py +195 -0
- asmu/setup.py +79 -0
- asmu/view.py +138 -0
- asmu-0.0.1.dist-info/LICENSE +674 -0
- asmu-0.0.1.dist-info/METADATA +19 -0
- asmu-0.0.1.dist-info/RECORD +13 -0
- asmu-0.0.1.dist-info/WHEEL +5 -0
- asmu-0.0.1.dist-info/top_level.txt +1 -0
asmu/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .analysis import get_corrs_sampleshifts, calSPL
|
|
2
|
+
from .generator import SincBurst, Sine, WhiteNoise, SineBurst, Chirp
|
|
3
|
+
from .inout import Input, Output
|
|
4
|
+
from .interface import Interface
|
|
5
|
+
from .recording import Recording
|
|
6
|
+
from .setup import Setup
|
|
7
|
+
#NOT IMPLEMENTED from .view import TimeView, Corrview
|
|
8
|
+
|
|
9
|
+
__all__ = ["get_corrs_sampleshifts", "calSPL", "SincBurst", "Sine", "WhiteNoise", "SineBurst", "Chirp", "generator", "Input", "Output", "Interface", "Recording", "Setup"]
|
asmu/analysis.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
3
|
+
import queue
|
|
4
|
+
|
|
5
|
+
class Analyzer():
|
|
6
|
+
def __init__(self, chunk, queuesize):
|
|
7
|
+
self._chunk = chunk
|
|
8
|
+
|
|
9
|
+
self._w = np.hanning(chunk)
|
|
10
|
+
self._q = queue.Queue(queuesize)
|
|
11
|
+
|
|
12
|
+
def reset(self):
|
|
13
|
+
# process full queue
|
|
14
|
+
while not self._q.empty():
|
|
15
|
+
self.process_queue()
|
|
16
|
+
|
|
17
|
+
def put_queue(self, data):
|
|
18
|
+
self._q.put(data.copy())
|
|
19
|
+
|
|
20
|
+
def process_queue(self):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def _get_fft(self) -> np.ndarray:
|
|
24
|
+
return np.fft.rfft(self._q.get()*self._w, axis=0, norm="forward")*2/np.mean(self._w)
|
|
25
|
+
|
|
26
|
+
class calSPL(Analyzer):
|
|
27
|
+
def __init__(self, chunk, queuesize: int=10):
|
|
28
|
+
self._uks = []
|
|
29
|
+
self._ks = []
|
|
30
|
+
super().__init__(chunk, queuesize)
|
|
31
|
+
|
|
32
|
+
def process_queue(self):
|
|
33
|
+
ukm = self._get_fft()
|
|
34
|
+
|
|
35
|
+
kmax = np.argmax(np.abs(ukm))
|
|
36
|
+
self._uks.append(np.abs(ukm[kmax]))
|
|
37
|
+
self._ks.append(kmax)
|
|
38
|
+
|
|
39
|
+
def get_cSPL(self, spl):
|
|
40
|
+
self.reset()
|
|
41
|
+
|
|
42
|
+
p0 = 2e-5 # [Pa]Rms
|
|
43
|
+
p = p0 * 10 ** (spl / 20) # [Pa]Rms
|
|
44
|
+
pcal = p * np.sqrt(2) # [Pa]
|
|
45
|
+
|
|
46
|
+
return pcal/np.mean(self._uks[2:]) # dont use the first three measurements
|
|
47
|
+
|
|
48
|
+
def get_kSPL(self):
|
|
49
|
+
return round(np.mean(self._ks))
|
|
50
|
+
|
|
51
|
+
def get_corrs_sampleshifts(indata: np.ndarray, refdata: np.ndarray, burstLength: int) -> tuple[np.ndarray, np.ndarray]:
|
|
52
|
+
corrs = []
|
|
53
|
+
sampleshifts = []
|
|
54
|
+
n = int(np.size(refdata))
|
|
55
|
+
for cidx in range(np.ma.size(indata, axis=1)):
|
|
56
|
+
v = indata[:, cidx]
|
|
57
|
+
corr = sp.signal.correlate(v, refdata)
|
|
58
|
+
corr /= np.max(corr)
|
|
59
|
+
peaks, _ = sp.signal.find_peaks(corr, prominence=0.8, distance=burstLength, height=0.5)
|
|
60
|
+
|
|
61
|
+
# shift correlation correctly
|
|
62
|
+
corrs.append(corr[n-1:])
|
|
63
|
+
sampleshifts.append(peaks[0]-(n-1))
|
|
64
|
+
return np.array(corrs).T, np.array(sampleshifts)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
asmu/generator.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
3
|
+
import threading
|
|
4
|
+
import queue
|
|
5
|
+
|
|
6
|
+
class Generator():
|
|
7
|
+
def __init__(self, queuesize) -> None:
|
|
8
|
+
self._q = queue.Queue(queuesize)
|
|
9
|
+
self._prefill_queue()
|
|
10
|
+
|
|
11
|
+
def get_queue(self) -> queue.Queue:
|
|
12
|
+
return self._q.get_nowait()
|
|
13
|
+
|
|
14
|
+
def _prefill_queue(self) -> None:
|
|
15
|
+
while self._q.full() is False:
|
|
16
|
+
self.refill_queue(block=False)
|
|
17
|
+
|
|
18
|
+
def refill_queue(self, block=True) -> None:
|
|
19
|
+
#if self._q.full() is False:
|
|
20
|
+
data = self._gen()
|
|
21
|
+
self._q.put(data, block=block, timeout=0.2)
|
|
22
|
+
|
|
23
|
+
def start_refill_thread(self, stream):
|
|
24
|
+
def runner(self, stream):
|
|
25
|
+
while stream.active:
|
|
26
|
+
try:
|
|
27
|
+
self.refill_queue()
|
|
28
|
+
except queue.Full:
|
|
29
|
+
pass
|
|
30
|
+
threading.Thread(target=runner, args=(self, stream, )).start()
|
|
31
|
+
|
|
32
|
+
def _gen(self) -> None:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
class Chirp(Generator):
|
|
36
|
+
def __init__(self, rate: int, chunk: int, f0: float, f1: float, duration: int, queuesize: int=10) -> None:
|
|
37
|
+
self._rate = rate
|
|
38
|
+
self._chunk = chunk
|
|
39
|
+
|
|
40
|
+
t = np.arange(0, int(duration*rate)) / rate
|
|
41
|
+
self._chirp = sp.signal.chirp(t, f0=f0, t1=duration, f1=f1, method="logarithmic")
|
|
42
|
+
self._idx = 0
|
|
43
|
+
super().__init__(queuesize)
|
|
44
|
+
|
|
45
|
+
def _gen(self) -> np.ndarray:
|
|
46
|
+
data = self._chirp[self._idx:self._idx+self._chunk]
|
|
47
|
+
self._idx += self._chunk
|
|
48
|
+
|
|
49
|
+
length = np.ma.size(data, axis=0)
|
|
50
|
+
if data.size == 0:
|
|
51
|
+
return None
|
|
52
|
+
elif length < self._chunk:
|
|
53
|
+
pad = np.zeros(self._chunk)
|
|
54
|
+
pad[:length] = data
|
|
55
|
+
return pad
|
|
56
|
+
else:
|
|
57
|
+
return data
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SineBurst(Generator):
|
|
61
|
+
def __init__(self, rate: int, chunk: int, f: float, n: int, loop: float=-1, queuesize: int=10) -> None:
|
|
62
|
+
self._rate = rate
|
|
63
|
+
self._chunk = chunk
|
|
64
|
+
self._freq = f
|
|
65
|
+
|
|
66
|
+
# generate wave parameters
|
|
67
|
+
self._omegas = np.linspace(0, 2*np.pi*f*chunk/rate, chunk, endpoint=False, dtype=np.float32)
|
|
68
|
+
self._phi = 0
|
|
69
|
+
self._maxang = 2*np.pi*n
|
|
70
|
+
self._loopang = 2*np.pi*loop*f
|
|
71
|
+
|
|
72
|
+
super().__init__(queuesize)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def burstlen(self):
|
|
76
|
+
return self._rate/(2*np.pi*self._freq)*self._maxang
|
|
77
|
+
|
|
78
|
+
def _gen(self) -> np.ndarray:
|
|
79
|
+
ang = self._omegas + self._phi
|
|
80
|
+
if self._loopang > 0: ang = np.mod(ang, self._loopang)
|
|
81
|
+
sineburst = np.sin(ang)
|
|
82
|
+
sineburst[ang > self._maxang] = 0
|
|
83
|
+
self._phi += 2*np.pi*self._freq*self._chunk/self._rate
|
|
84
|
+
return sineburst
|
|
85
|
+
|
|
86
|
+
class SincBurst(Generator):
|
|
87
|
+
def __init__(self, rate: int, chunk: int, f: float, n: int, loop: float=-1, queuesize: int=10) -> None:
|
|
88
|
+
self._rate = rate
|
|
89
|
+
self._chunk = chunk
|
|
90
|
+
self._freq = f
|
|
91
|
+
|
|
92
|
+
# generate wave parameters
|
|
93
|
+
self._omegas = np.linspace(0, 2*np.pi*f*chunk/rate, chunk, endpoint=False, dtype=np.float32)
|
|
94
|
+
self._phi = 0
|
|
95
|
+
self._maxang = 2*np.pi*n
|
|
96
|
+
self._loopang = 2*np.pi*loop*f
|
|
97
|
+
|
|
98
|
+
super().__init__(queuesize)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def burstlen(self):
|
|
102
|
+
return self._rate/(2*np.pi*self._freq)*self._maxang
|
|
103
|
+
|
|
104
|
+
def _gen(self) -> np.ndarray:
|
|
105
|
+
ang = self._omegas + self._phi
|
|
106
|
+
if self._loopang > 0: ang = np.mod(ang, self._loopang)
|
|
107
|
+
sincburst = np.sinc((ang-self._maxang/2)/np.pi)
|
|
108
|
+
sincburst[ang > self._maxang] = 0
|
|
109
|
+
self._phi += 2*np.pi*self._freq*self._chunk/self._rate
|
|
110
|
+
return sincburst
|
|
111
|
+
|
|
112
|
+
class WhiteNoise(Generator):
|
|
113
|
+
def __init__(self, chunk: int, queuesize: int=10) -> None:
|
|
114
|
+
self._chunk = chunk
|
|
115
|
+
|
|
116
|
+
super().__init__(queuesize)
|
|
117
|
+
|
|
118
|
+
def _gen(self) -> np.ndarray:
|
|
119
|
+
fwhite = np.fft.rfft(np.random.randn(self._chunk))
|
|
120
|
+
return np.clip(np.fft.irfft(fwhite)/4, -1, 1)
|
|
121
|
+
|
|
122
|
+
class PinkNoise(Generator):
|
|
123
|
+
def __init__(self, chunk: int, queuesize: int=10) -> None:
|
|
124
|
+
self._chunk = chunk
|
|
125
|
+
|
|
126
|
+
super().__init__(queuesize)
|
|
127
|
+
|
|
128
|
+
def _gen(self) -> np.ndarray:
|
|
129
|
+
fwhite = np.fft.rfft(np.random.randn(self._chunk))
|
|
130
|
+
|
|
131
|
+
# apply noise color and normalize for power
|
|
132
|
+
f = np.fft.rfftfreq(self._chunk)
|
|
133
|
+
S = 1/np.where(f == 0, float('inf'), np.sqrt(f))
|
|
134
|
+
S = S / np.sqrt(np.mean(S**2))
|
|
135
|
+
fpink = fwhite * S
|
|
136
|
+
|
|
137
|
+
return np.clip(np.fft.irfft(fpink)/4, -1, 1)
|
|
138
|
+
|
|
139
|
+
class Sine(Generator):
|
|
140
|
+
def __init__(self, rate: int, chunk: int, f: float, queuesize: int=10) -> None:
|
|
141
|
+
self._rate = rate
|
|
142
|
+
self._chunk = chunk
|
|
143
|
+
self._freq = f
|
|
144
|
+
|
|
145
|
+
# generate wave parameters
|
|
146
|
+
self._omegas = np.linspace(0, 2*np.pi*f*chunk/rate, chunk, endpoint=False, dtype=np.float32)
|
|
147
|
+
self._phi = 0
|
|
148
|
+
|
|
149
|
+
super().__init__(queuesize)
|
|
150
|
+
|
|
151
|
+
def _gen(self) -> np.ndarray:
|
|
152
|
+
ang = self._omegas + self._phi
|
|
153
|
+
self._phi += 2*np.pi*self._freq*self._chunk/self._rate
|
|
154
|
+
self._phi %= 2*np.pi
|
|
155
|
+
return np.sin(ang)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
asmu/inout.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
class IO():
|
|
4
|
+
def __init__(self, IOsetup: dict) -> None:
|
|
5
|
+
self._IOSetup = IOsetup
|
|
6
|
+
|
|
7
|
+
def __repr__(self):
|
|
8
|
+
return self.name
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def name(self):
|
|
12
|
+
return self._IOSetup["name"]
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def channel(self):
|
|
16
|
+
return int(self._IOSetup["channel"])
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def color(self):
|
|
20
|
+
return self._IOSetup["color"]
|
|
21
|
+
@color.setter
|
|
22
|
+
def color(self, value):
|
|
23
|
+
self._IOSetup["color"] = str(value)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def pos(self):
|
|
27
|
+
return np.array(self._IOSetup["pos"])
|
|
28
|
+
@pos.setter
|
|
29
|
+
def pos(self, value):
|
|
30
|
+
self._IOSetup["pos"] = value
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def reference(self):
|
|
34
|
+
try:
|
|
35
|
+
return bool(self._IOSetup["reference"])
|
|
36
|
+
except KeyError:
|
|
37
|
+
return False
|
|
38
|
+
@reference.setter
|
|
39
|
+
def reference(self, value):
|
|
40
|
+
if value:
|
|
41
|
+
self._IOSetup["reference"] = True
|
|
42
|
+
else:
|
|
43
|
+
try:
|
|
44
|
+
del self._IOSetup["reference"]
|
|
45
|
+
except KeyError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
class Input(IO):
|
|
49
|
+
def __init__(self, inputSetup: dict) -> None:
|
|
50
|
+
super().__init__(inputSetup)
|
|
51
|
+
self._inputSetup = inputSetup
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def cSPL(self):
|
|
55
|
+
return float(self._inputSetup["cSPL"])
|
|
56
|
+
@cSPL.setter
|
|
57
|
+
def cSPL(self, value:float):
|
|
58
|
+
self._inputSetup["cSPL"] = value
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def kSPL(self):
|
|
62
|
+
return int(self._inputSetup["kSPL"])
|
|
63
|
+
@kSPL.setter
|
|
64
|
+
def kSPL(self, value:int):
|
|
65
|
+
self._inputSetup["kSPL"] = value
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def cFR(self):
|
|
69
|
+
return self._inputSetup["cFR"]
|
|
70
|
+
@cFR.setter
|
|
71
|
+
def cFR(self, value):
|
|
72
|
+
self._inputSetup["cFR"] = np.array(value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Output(IO):
|
|
76
|
+
def __init__(self, outputSetup: dict) -> None:
|
|
77
|
+
super().__init__(outputSetup)
|
|
78
|
+
|
|
79
|
+
|
asmu/interface.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sounddevice as sd
|
|
3
|
+
|
|
4
|
+
DEBUG = True
|
|
5
|
+
|
|
6
|
+
class Interface():
|
|
7
|
+
def __init__(self, interfaceSetup: dict) -> None:
|
|
8
|
+
"""Class for Audiointerface communication and settings.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
interfaceSetup (dict): Interface setup.
|
|
12
|
+
"""
|
|
13
|
+
self._interfaceSetup = interfaceSetup
|
|
14
|
+
self.callback = None
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def name(self):
|
|
18
|
+
return self._interfaceSetup["name"]
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def device(self):
|
|
22
|
+
return self._interfaceSetup["device"]
|
|
23
|
+
@device.setter
|
|
24
|
+
def device(self, value):
|
|
25
|
+
self._interfaceSetup["device"] = value
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def rate(self):
|
|
29
|
+
return int(self._interfaceSetup["rate"])
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def chunk(self):
|
|
33
|
+
return int(self._interfaceSetup['chunk'])
|
|
34
|
+
|
|
35
|
+
# SOUNDDEVICE
|
|
36
|
+
def initSounddevice(self, channels: tuple[int, int]) -> None:
|
|
37
|
+
sd.default.samplerate = self.rate
|
|
38
|
+
sd.default.dtype = np.float32
|
|
39
|
+
sd.default.blocksize = self.chunk
|
|
40
|
+
try:
|
|
41
|
+
sd.default.device = self.device # TODO - different input and output device
|
|
42
|
+
except KeyError:
|
|
43
|
+
print("interface - no device specified, using default")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if "ASIO" in self.device:
|
|
47
|
+
if channels[0]:
|
|
48
|
+
inChannels = [c - 1 for c in channels[0]] # convert to channel names starting with 0
|
|
49
|
+
asio_in = sd.AsioSettings(channel_selectors=inChannels)
|
|
50
|
+
|
|
51
|
+
if not channels[1]:
|
|
52
|
+
sd.default.extra_settings = asio_in
|
|
53
|
+
sd.default.channels = len(inChannels)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if channels[1]:
|
|
57
|
+
outChannels = [c - 1 for c in channels[1]]
|
|
58
|
+
asio_out = sd.AsioSettings(channel_selectors=outChannels)
|
|
59
|
+
|
|
60
|
+
if not channels[0]:
|
|
61
|
+
sd.default.extra_settings = asio_out
|
|
62
|
+
sd.default.channels = len(outChannels)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if channels[0] and channels[1]:
|
|
66
|
+
sd.default.extra_settings = (asio_in, asio_out)
|
|
67
|
+
sd.default.channels = (len(inChannels), len(outChannels))
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
elif "CoreAudio" in self.device: # TODO - update this
|
|
71
|
+
inChannels = [c - 1 for c in channels[0]] # convert to channel names starting with 0
|
|
72
|
+
ca_in = sd.CoreAudioSettings(channel_map=inChannels)
|
|
73
|
+
|
|
74
|
+
if channels[1]:
|
|
75
|
+
outChannels = [-1]*sd.query_devices(device=self.device, kind="output")["max_output_channels"]
|
|
76
|
+
for idx, c in enumerate(channels[1]):
|
|
77
|
+
outChannels[c-1] = idx
|
|
78
|
+
ca_out = sd.CoreAudioSettings(channel_map=outChannels)
|
|
79
|
+
|
|
80
|
+
sd.default.extra_settings = (ca_in, ca_out)
|
|
81
|
+
sd.default.channels = (len(inChannels), len(outChannels))
|
|
82
|
+
else:
|
|
83
|
+
sd.default.extra_settings = ca_in
|
|
84
|
+
sd.default.channels = len(inChannels)
|
|
85
|
+
else:
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
def _callback(self, indata: np.ndarray, outdata: np.ndarray, frames: int, time, status) -> None:
|
|
89
|
+
if status:
|
|
90
|
+
print(status, flush=True)
|
|
91
|
+
|
|
92
|
+
outdata.fill(0)
|
|
93
|
+
self.callback(indata, outdata, frames, time)
|
|
94
|
+
|
|
95
|
+
def start_stream(self, channels:tuple, finished_callback=None) -> None:
|
|
96
|
+
self.initSounddevice(channels)
|
|
97
|
+
return sd.Stream(callback=self._callback, finished_callback=finished_callback)
|
|
98
|
+
|
|
99
|
+
def stop_stream(self) -> None:
|
|
100
|
+
raise sd.CallbackStop
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
asmu/recording.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
import queue
|
|
4
|
+
import soundfile as sf
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
class Recording():
|
|
13
|
+
def __init__(self, recordingPath: str, mode: str, setupName: str, channels: int, rate: int, chunk: int, queuesize=10) -> None:
|
|
14
|
+
"""Constructs all the necessary attributes for the Recording object.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
recordingPath (str): Path to an .asm_rec file
|
|
18
|
+
mode ("r", "w"): Set recording mode to read "r", or write "w"
|
|
19
|
+
setupName (str): Name of the current setup for comparison with the recordings setupName
|
|
20
|
+
channels (int): Channel count
|
|
21
|
+
rate (int): Samplerate
|
|
22
|
+
chunk (int): Chunksize
|
|
23
|
+
queuesize (int, optional): The size of the internal queue. Defaults to 10.
|
|
24
|
+
"""
|
|
25
|
+
# path and recording info
|
|
26
|
+
self._path, file = os.path.split(recordingPath)
|
|
27
|
+
self._name = file.replace(".asm_recording", "") # extract name without file ending
|
|
28
|
+
now = datetime.now()
|
|
29
|
+
self._recordingData = {
|
|
30
|
+
"setupName" : setupName,
|
|
31
|
+
"date" : now.strftime("%d/%m/%Y"),
|
|
32
|
+
"time" : now.strftime("%H:%M:%S"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# save parameters
|
|
36
|
+
self._mode = mode
|
|
37
|
+
self._rate = rate
|
|
38
|
+
self._chunk = chunk
|
|
39
|
+
self._channels = channels
|
|
40
|
+
|
|
41
|
+
# create buffer queue for transfer between threads
|
|
42
|
+
self._rq = queue.Queue(queuesize)
|
|
43
|
+
self._pq = queue.Queue(queuesize)
|
|
44
|
+
|
|
45
|
+
def __enter__(self):
|
|
46
|
+
if self._mode=="r":
|
|
47
|
+
self.load_file(self.setupName)
|
|
48
|
+
elif self._mode=="w":
|
|
49
|
+
self.create_file()
|
|
50
|
+
else:
|
|
51
|
+
raise Exception(f"Recording - invalid mode specified, use \"r\" or \"w\".")
|
|
52
|
+
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
56
|
+
self._filesf.close()
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self):
|
|
60
|
+
return self._name
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def path(self):
|
|
64
|
+
return self._path
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def setupName(self):
|
|
68
|
+
return self._recordingData["setupName"]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def date(self):
|
|
72
|
+
return self._recordingData["date"]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def time(self):
|
|
76
|
+
return self._recordingData["time"]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def extra_settings(self):
|
|
80
|
+
""":obj:`dict` of :obj:`dict`: Store extra settings in .asm_rec file.
|
|
81
|
+
|
|
82
|
+
Setting the dict overwrites existing extra_settings!
|
|
83
|
+
"""
|
|
84
|
+
return self._recordingData["extra_settings"]
|
|
85
|
+
@extra_settings.setter
|
|
86
|
+
def extra_settings(self, value):
|
|
87
|
+
self._recordingData["extra_settings"] = value
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def latency(self):
|
|
91
|
+
try:
|
|
92
|
+
return self._recordingData["latency"]
|
|
93
|
+
except KeyError:
|
|
94
|
+
return None
|
|
95
|
+
@latency.setter
|
|
96
|
+
def latency(self, value):
|
|
97
|
+
self._recordingData["latency"] = int(value)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def data(self):
|
|
101
|
+
# process full queue
|
|
102
|
+
while not self._rq.empty():
|
|
103
|
+
self.process_queue()
|
|
104
|
+
|
|
105
|
+
self._filesf.flush()
|
|
106
|
+
self._filesf.seek(0)
|
|
107
|
+
return self._filesf.read(dtype="float32", always_2d=True)
|
|
108
|
+
|
|
109
|
+
# LATENCY
|
|
110
|
+
def set_latency(self, time):
|
|
111
|
+
self.latency = round((time.outputBufferDacTime-time.inputBufferAdcTime)*self._rate + 1) # the +1 was measured experimentally (could be the cable?)
|
|
112
|
+
|
|
113
|
+
# PLAYBACK
|
|
114
|
+
def get_queue(self):
|
|
115
|
+
return self._pq.get_nowait()
|
|
116
|
+
|
|
117
|
+
def prefill_queue(self):
|
|
118
|
+
self.empty_queue()
|
|
119
|
+
self._filesf.seek(0)
|
|
120
|
+
while self._pq.full() is False:
|
|
121
|
+
self.refill_queue(block=False)
|
|
122
|
+
|
|
123
|
+
def refill_queue(self, block=True):
|
|
124
|
+
if self._pq.full() is False:
|
|
125
|
+
data = self._gen()
|
|
126
|
+
self._pq.put(data, block=block)
|
|
127
|
+
|
|
128
|
+
def empty_queue(self):
|
|
129
|
+
while not self._pq.empty():
|
|
130
|
+
self.get_queue()
|
|
131
|
+
|
|
132
|
+
def _gen(self):
|
|
133
|
+
data = self._filesf.read(self._chunk, dtype="float32", always_2d=True)
|
|
134
|
+
length = np.ma.size(data, axis=0)
|
|
135
|
+
if data.size == 0:
|
|
136
|
+
return None
|
|
137
|
+
elif length < self._chunk:
|
|
138
|
+
pad = np.zeros((self._chunk, self._channels))
|
|
139
|
+
pad[:length, :] = data
|
|
140
|
+
return pad
|
|
141
|
+
else:
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
# RECORDING
|
|
145
|
+
def create_file(self, subtype="PCM_24"):
|
|
146
|
+
self._file = tempfile.NamedTemporaryFile(prefix=f"{self.name}", suffix='.wav', dir="", delete=True)
|
|
147
|
+
self._filesf = sf.SoundFile(self._file, mode='w+', samplerate=self._rate, channels=self._channels, subtype=subtype, format="WAV")
|
|
148
|
+
|
|
149
|
+
def put_queue(self, data):
|
|
150
|
+
self._rq.put(data.copy())
|
|
151
|
+
|
|
152
|
+
def process_queue(self):
|
|
153
|
+
self._filesf.write(self._rq.get(timeout=0.2))
|
|
154
|
+
|
|
155
|
+
def start_process_thread(self, stream):
|
|
156
|
+
def runner(self, stream):
|
|
157
|
+
while stream.active:
|
|
158
|
+
try:
|
|
159
|
+
self.process_queue()
|
|
160
|
+
except queue.Empty:
|
|
161
|
+
pass
|
|
162
|
+
threading.Thread(target=runner, args=(self, stream, )).start()
|
|
163
|
+
|
|
164
|
+
# SAVE/LOAD
|
|
165
|
+
def save_file(self, extra_settings={}):
|
|
166
|
+
# add new extra_settings
|
|
167
|
+
if extra_settings:
|
|
168
|
+
self.extra_settings = extra_settings
|
|
169
|
+
|
|
170
|
+
# save .asm_rec file
|
|
171
|
+
with open(f"{self.path}/{self.name}.asm_rec", 'w') as file:
|
|
172
|
+
file.write(json.dumps(self._recordingData))
|
|
173
|
+
|
|
174
|
+
# process full queue
|
|
175
|
+
while not self._rq.empty():
|
|
176
|
+
self.process_queue()
|
|
177
|
+
|
|
178
|
+
# persist temporary files in specified recording location
|
|
179
|
+
self._file.flush()
|
|
180
|
+
self._file.seek(0)
|
|
181
|
+
with open(f"{self.path}/{self.name}.wav", 'wb') as file:
|
|
182
|
+
shutil.copyfileobj(self._file, file)
|
|
183
|
+
|
|
184
|
+
def load_file(self, currentSetupName):
|
|
185
|
+
# open .asm_rec file
|
|
186
|
+
with open(f"{self.path}/{self.name}.asm_rec", 'r') as file:
|
|
187
|
+
self._recordingData = json.load(file)
|
|
188
|
+
|
|
189
|
+
# check if setups are equal
|
|
190
|
+
if self.setupName != currentSetupName:
|
|
191
|
+
raise Exception(f"Recording - current setup does not match setup of {self.setupName}.asm_rec")
|
|
192
|
+
|
|
193
|
+
# open .wav file
|
|
194
|
+
self._filesf = sf.SoundFile(f"{self.path}/{self.name}.wav")
|
|
195
|
+
self.prefill_queue()
|
asmu/setup.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
class Setup():
|
|
6
|
+
def __init__(self, setupPath: str) -> None:
|
|
7
|
+
"""Class to handle setups.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
setupPath (str): Path to .asm_setup file
|
|
11
|
+
"""
|
|
12
|
+
self._path, file = os.path.split(setupPath)
|
|
13
|
+
self._name = file.replace(".asm_setup", "") # extract name without file ending
|
|
14
|
+
self.load_file(setupPath)
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def name(self):
|
|
18
|
+
return self._name
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def path(self):
|
|
22
|
+
return self._path
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def inputs(self):
|
|
26
|
+
try:
|
|
27
|
+
value = self._setup["inputs"]
|
|
28
|
+
return value
|
|
29
|
+
except KeyError:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def outputs(self):
|
|
34
|
+
try:
|
|
35
|
+
value = self._setup["outputs"]
|
|
36
|
+
return value
|
|
37
|
+
except KeyError:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def interface(self):
|
|
42
|
+
return self._setup["interface"]
|
|
43
|
+
|
|
44
|
+
def load_file(self, setupPath):
|
|
45
|
+
with open(setupPath, "r") as file:
|
|
46
|
+
self._setup = json.load(file)
|
|
47
|
+
|
|
48
|
+
# load numpy arrays from external files
|
|
49
|
+
for io in self.inputs+self.outputs:
|
|
50
|
+
try:
|
|
51
|
+
path = f"{self.path}/{self.name}/cFR_{io["name"]}.npy"
|
|
52
|
+
io["cFR"] = np.load(path)
|
|
53
|
+
except FileNotFoundError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def save_file(self, setupPath=None):
|
|
57
|
+
if setupPath is not None:
|
|
58
|
+
self.__init__(setupPath)
|
|
59
|
+
setupPath = f"{self.path}/{self.name}.asm_setup"
|
|
60
|
+
|
|
61
|
+
# save numpy arrays to external files
|
|
62
|
+
for io in self.inputs+self.outputs:
|
|
63
|
+
try:
|
|
64
|
+
if isinstance(io["cFR"], np.ndarray):
|
|
65
|
+
path = f"{self.path}/{self.name}/cFR_{io["name"]}.npy"
|
|
66
|
+
np.save(path, io["cFR"])
|
|
67
|
+
io.pop("cFR")
|
|
68
|
+
except KeyError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# save .asm_setup file
|
|
72
|
+
with open(setupPath, 'w') as file:
|
|
73
|
+
file.write(json.dumps(self._setup, sort_keys=True, indent=4, separators=(',', ': ')))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|