Skip to content

Commit ad6ad81

Browse files
committed
add audio read plugin for EPG Systems' AQ8 files
1 parent 731734f commit ad6ad81

File tree

9 files changed

+172
-28
lines changed

9 files changed

+172
-28
lines changed

src/audio-read-plugin.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
# a function that inputs the full path to a file containing the audio recording,
2-
# an interval of time, and some keyword arguments and returns the sampling
3-
# rate, shape of entire recording (not just the interval), and requested data as int16
1+
#a function that inputs the full path (including possibly a recording letter) to
2+
#a file containing the audio recording, an interval of time, and some keyword
3+
#arguments and returns the sampling rate, shape of entire recording (not just
4+
#the interval), and requested data as int16. if {start,stop}_tic are None, return
5+
#the entire recording
46
def audio_read(fullpath, start_tic, stop_tic, **kw):
57

68
# load data, determine sampling rate and length, and do any special processing
79

810
return sampling_rate, nsamples_nchannels, slice_of_data
11+
12+
# a function that returns a list of file extensions which this plugin can handle
13+
def audio_read_exts(**kw):
14+
return [] # e.g. ['.wav', '.WAV']
15+
16+
# a function that returns a dictionary that maps logical recordings to channels in the file
17+
def audio_read_rec2ch(**kw):
18+
return {} # e.g. {'A':[0], 'B':[1]}, or {'A':[0,1]}
19+
20+
def audio_read_init(**kw):
21+
pass

src/data.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,15 @@ def __init__(self, data_dir,
118118
self.np_rng = np.random.default_rng(None if random_seed_batch==-1 else random_seed_batch)
119119

120120
sys.path.append(os.path.dirname(audio_read_plugin))
121-
self.audio_read_plugin = os.path.basename(audio_read_plugin)
121+
audio_read_plugin = os.path.basename(audio_read_plugin)
122122
self.audio_read_plugin_kwargs = audio_read_plugin_kwargs
123+
self.audio_read_module = importlib.import_module(audio_read_plugin)
124+
self.audio_read_module.audio_read_init(**self.audio_read_plugin_kwargs)
123125

124126
sys.path.append(os.path.dirname(video_read_plugin))
125-
self.video_read_plugin = os.path.basename(video_read_plugin)
127+
video_read_plugin = os.path.basename(video_read_plugin)
126128
self.video_read_plugin_kwargs = video_read_plugin_kwargs
129+
self.video_read_module = importlib.import_module(video_read_plugin)
127130

128131
self.prepare_data_index(shiftby,
129132
labels_touse, kinds_touse,
@@ -139,14 +142,12 @@ def __init__(self, data_dir,
139142
signal.signal(signal.SIGTERM, term)
140143

141144
def audio_read(self, fullpath, start_tic=None, stop_tic=None):
142-
audio_read_module = importlib.import_module(self.audio_read_plugin)
143-
return audio_read_module.audio_read(fullpath, start_tic, stop_tic,
144-
**self.audio_read_plugin_kwargs)
145+
return self.audio_read_module.audio_read(fullpath, start_tic, stop_tic,
146+
**self.audio_read_plugin_kwargs)
145147

146148
def video_read(self, fullpath, start_frame=None, stop_frame=None):
147-
video_read_module = importlib.import_module(self.video_read_plugin)
148-
return video_read_module.video_read(fullpath, start_frame, stop_frame,
149-
**self.video_read_plugin_kwargs)
149+
return self.video_read_module.video_read(fullpath, start_frame, stop_frame,
150+
**self.video_read_plugin_kwargs)
150151

151152
def catalog_overlaps(self, data):
152153
data.sort(key=lambda x: x['ticks'][0])

src/gui/controller.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,15 +1043,17 @@ def _validation_test_files(files_string, comma=True):
10431043
return [','.join(wavfiles)] if comma else list(wavfiles)
10441044
elif os.path.dirname(files_string.rstrip(os.sep)) == V.groundtruth_folder.value.rstrip(os.sep):
10451045
dfs = []
1046-
for csvfile in filter(lambda x: x.endswith('.csv'), os.listdir(files_string)):
1046+
for csvfile in filter(lambda x: os.path.splitext(x)[1] in M.audio_read_exts(),
1047+
os.listdir(files_string)):
10471048
filepath = os.path.join(files_string, csvfile)
10481049
if os.path.getsize(filepath) > 0:
10491050
dfs.append(pd.read_csv(filepath, header=None, index_col=False))
10501051
if dfs:
10511052
df = pd.concat(dfs)
10521053
wavfiles = sorted(list(set(df.loc[df[3]=="annotated"][0])))
10531054
return [','.join(wavfiles)] if comma else list(wavfiles)
1054-
elif files_string.lower().endswith('.wav'):
1055+
elif os.path.splitext(files_string[:-2 if len(M.audio_read_rec2ch())>1 else None])[1] \
1056+
in M.audio_read_exts():
10551057
return [files_string] if comma else files_string.split(',')
10561058
elif files_string!='':
10571059
with open(files_string, "r") as fid:
@@ -2098,12 +2100,18 @@ def wavcsv_files_callback():
20982100
if len(V.file_dialog_source.selected.indices)==0:
20992101
bokehlog.info('ERROR: a file(s) must be selected in the file browser')
21002102
return
2101-
filename = V.file_dialog_source.data['names'][V.file_dialog_source.selected.indices[0]]
2102-
files = os.path.join(M.file_dialog_root, filename)
2103-
for i in range(1, len(V.file_dialog_source.selected.indices)):
2103+
files = []
2104+
for i in range(len(V.file_dialog_source.selected.indices)):
21042105
filename = V.file_dialog_source.data['names'][V.file_dialog_source.selected.indices[i]]
2105-
files += ','+os.path.join(M.file_dialog_root, filename)
2106-
V.wavcsv_files.value = files
2106+
if os.path.splitext(filename)[1] in M.audio_read_exts():
2107+
if len(M.audio_read_rec2ch()) == 1:
2108+
files.append(os.path.join(M.file_dialog_root, filename))
2109+
else:
2110+
files.extend([os.path.join(M.file_dialog_root, filename)+'-'+k
2111+
for k in M.audio_read_rec2ch().keys()])
2112+
else:
2113+
files.append(os.path.join(M.file_dialog_root, filename))
2114+
V.wavcsv_files.value = ','.join(files)
21072115

21082116
def groundtruth_callback():
21092117
if len(V.file_dialog_source.selected.indices)>=2:
@@ -2123,8 +2131,12 @@ def _validation_test_files_callback():
21232131
filename = V.file_dialog_source.data['names'][V.file_dialog_source.selected.indices[0]]
21242132
filepath = os.path.join(M.file_dialog_root, filename)
21252133
if nindices<2:
2126-
if filepath.lower().endswith('.wav'):
2127-
return os.path.basename(filepath)
2134+
if os.path.splitext(filepath)[1] in M.audio_read_exts():
2135+
if len(M.audio_read_rec2ch()) == 1:
2136+
return os.path.basename(filepath)
2137+
else:
2138+
return ','.join([os.path.basename(filepath)+'-'+k
2139+
for k in M.audio_read_rec2ch().keys()])
21282140
else:
21292141
return filepath
21302142
else:

src/gui/model.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,14 @@ def save_annotations():
8484
fids = {}
8585
csvwriters = {}
8686
csvfiles_current = set([])
87-
for wavfile in set([os.path.join(*x['file']) for x in annotated_sounds if x["label"]!=""]):
88-
csvfile = wavfile[:-4]+"-annotated-"+songexplorer_starttime+".csv"
87+
wavfiles = set()
88+
for sound in annotated_sounds:
89+
if not sound["label"]: continue
90+
wavfile = os.path.join(*sound["file"])
91+
wavfile_norec = ''.join(wavfile.split('-')[:-1]) if len(audio_read_rec2ch())>1 else wavfile
92+
wavfiles |= set([wavfile_norec])
93+
for wavfile in wavfiles:
94+
csvfile = os.path.splitext(wavfile)[0]+"-annotated-"+songexplorer_starttime+".csv"
8995
annotated_csvfiles_all.add(csvfile)
9096
csvfiles_current.add(csvfile)
9197
fids[wavfile] = open(os.path.join(V.groundtruth_folder.value, csvfile),
@@ -98,7 +104,9 @@ def save_annotations():
98104
corrected_sounds=[]
99105
for annotation in annotated_sounds:
100106
if annotation['label']!="" and not annotation['label'].isspace():
101-
csvwriters[os.path.join(*annotation['file'])].writerow(
107+
wavfile = os.path.join(*annotation['file'])
108+
wavfile_norec = ''.join(wavfile.split('-')[:-1]) if len(audio_read_rec2ch())>1 else wavfile
109+
csvwriters[wavfile_norec].writerow(
102110
[annotation['file'][1],
103111
annotation['ticks'][0], annotation['ticks'][1],
104112
'annotated', annotation['label']])
@@ -110,10 +118,15 @@ def save_annotations():
110118
x['ticks'][1], 'annotated', x['label']] \
111119
for x in corrected_sounds], \
112120
columns=['file','start','stop','kind','label'])
113-
for wavfile in set([os.path.join(*x['file']) for x in corrected_sounds]):
121+
wavfiles = set()
122+
for sound in corrected_sounds:
123+
wavfile = os.path.join(*sound["file"])
124+
wavfile_norec = ''.join(wavfile.split('-')[:-1]) if len(audio_read_rec2ch())>1 else wavfile
125+
wavfiles |= set([wavfile_norec])
126+
for wavfile in wavfiles:
114127
wavdir, wavbase = os.path.split(wavfile)
115128
wavpath = os.path.join(V.groundtruth_folder.value, wavdir)
116-
for csvbase in filter(lambda x: x.startswith(wavbase[:-4]) and
129+
for csvbase in filter(lambda x: x.startswith(os.path.splitext(wavbase)[0]) and
117130
x.endswith(".csv") and
118131
"-annotated" in x and
119132
songexplorer_starttime not in x,
@@ -235,7 +248,8 @@ def init(_bokeh_document, _configuration_file, _use_aitch):
235248
global context_width_sec0, context_offset_sec0
236249
global xcluster, ycluster, zcluster, ndcluster, tic2pix_max, snippet_width_pix, ilayer, ispecies, iword, inohyphen, ikind, nlayers, layers, species, words, nohyphens, kinds, used_labels, snippets_gap_sec, snippets_tic, snippets_gap_tic, snippets_decimate_by, snippets_pix, snippets_gap_pix, context_decimate_by, context_width_tic, context_offset_tic, context_sound, isnippet, xsnippet, ysnippet, file_nframes, context_midpoint_tic, ilabel, used_sounds, used_starts_sorted, used_stops, iused_stops_sorted, annotated_sounds, annotated_starts_sorted, annotated_stops, iannotated_stops_sorted, annotated_csvfiles_all, nrecent_annotations, clustered_sounds, clustered_activations, used_recording2firstsound, clustered_starts_sorted, clustered_stops, iclustered_stops_sorted, songexplorer_starttime, history_stack, history_idx, wizard, action, function, statepath, state, file_dialog_root, file_dialog_filter, nearest_sounds, status_ticker_queue, waitfor_job, dfs, remaining_isounds
237250
global user_changed_recording, user_copied_parameters
238-
global audio_read, video_read, detect_labels, doubleclick_annotation, context_data, context_data_istart, model, video_findfile
251+
global audio_read, audio_read_exts, audio_read_rec2ch
252+
global video_read, detect_labels, doubleclick_annotation, context_data, context_data_istart, model, video_findfile
239253
global detect_parameters, doubleclick_parameters, model_parameters, cluster_parameters
240254

241255
bokeh_document = _bokeh_document
@@ -253,9 +267,11 @@ def init(_bokeh_document, _configuration_file, _use_aitch):
253267

254268
sys.path.insert(0,os.path.dirname(audio_read_plugin))
255269
audio_read_module = importlib.import_module(os.path.basename(audio_read_plugin))
270+
audio_read_module.audio_read_init(**audio_read_plugin_kwargs)
256271
def audio_read(wav_path, start_tic=None, stop_tic=None):
257-
return audio_read_module.audio_read(wav_path, start_tic, stop_tic,
258-
**audio_read_plugin_kwargs)
272+
return audio_read_module.audio_read(wav_path, start_tic, stop_tic, **audio_read_plugin_kwargs)
273+
def audio_read_exts(): return audio_read_module.audio_read_exts(**audio_read_plugin_kwargs)
274+
def audio_read_rec2ch(): return audio_read_module.audio_read_rec2ch(**audio_read_plugin_kwargs)
259275

260276
sys.path.insert(0,os.path.dirname(video_read_plugin))
261277
video_read_module = importlib.import_module(os.path.basename(video_read_plugin))

src/highpass-filter.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,12 @@ def audio_read(wav_path, start_tic, stop_tic, cutoff=1, order=2):
3333
data_unpadded = data_filtered[padlenL:-padlenR or None, :]
3434

3535
return sampling_rate, data.shape, data_unpadded
36+
37+
def audio_read_exts(**kw):
38+
return ['.wav', '.WAV']
39+
40+
def audio_read_rec2ch(**kw):
41+
return {'A':[0]}
42+
43+
def audio_read_init(**kw):
44+
pass

src/load-epg-lut.npy

189 KB
Binary file not shown.

src/load-epg-make-lut.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
3+
# to have songexplorer automatically convert the values stored in EPG .aq8
4+
# files into voltages, export the binary data in some .DOx files as plain text
5+
# .A0x files using the “Save measured data as ASCII” function in the stylet+
6+
# software. then use this script to generate a lookup table, and specify the
7+
# path to the generated .npy file in the keyword arguments to the load-epg
8+
# audio_read plugin in configuration.py.
9+
10+
# e.g. src/load-epg-make-lut.py <path-to-folder-of-DOx-and-A0x-files>
11+
12+
import sys
13+
import os
14+
import numpy as np
15+
16+
_, path2data = sys.argv
17+
18+
lut = np.empty((0,2))
19+
for asciifile in filter(lambda x: os.path.splitext(x)[1].startswith('.A'),
20+
os.listdir(path2data)):
21+
print(asciifile)
22+
asciidata = np.loadtxt(os.path.join(path2data, asciifile), delimiter=';')
23+
binaryfile = asciifile[:-3]+'D'+asciifile[-2:]
24+
binarydata = np.fromfile(os.path.join(path2data, binaryfile), dtype=np.uint32)
25+
this_lut = np.hstack((np.expand_dims(binarydata, axis=1), asciidata[:,[1]]))
26+
lut = np.unique(np.vstack((lut, np.unique(this_lut, axis=0))), axis=0)
27+
28+
isort = np.argsort(lut[:,0])
29+
lut = lut[isort,:]
30+
31+
np.save(path2data+".npy", lut)

src/load-epg.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# to analyze Electrical Penetration Graph (EGP; https://epgsystems.eu) data,
2+
# first create a lookup table using load-epg-make-lut.py. then use the .aq8
3+
# files directly using this plugin. the .D0x files that stylet+ automatically
4+
# creates can be deleted (as can any .A0x files).
5+
6+
#audio_read_plugin="load-epg"
7+
#audio_read_plugin_kwargs={"nchan":8, "lut_file":"load-epg-lut.npy",
8+
# "ncomments":3, "Fs":"smpl.frq= ([0-9.]+)Hz"}
9+
10+
import re
11+
import numpy as np
12+
import os
13+
14+
def audio_read(fullpath_aq8_rec, start_tic, stop_tic,
15+
nchan=8, ncomments=3, Fs="smpl.frq= ([0-9.]+)Hz", **kw):
16+
fullpath_aq8, rec = fullpath_aq8_rec[:-2], fullpath_aq8_rec[-1]
17+
18+
if not start_tic: start_tic=0
19+
20+
with open(fullpath_aq8, 'rb') as fid:
21+
for _ in range(ncomments):
22+
line = fid.readline().decode()
23+
m = re.search(Fs, line)
24+
if m: sampling_rate = float(m.group(1))
25+
n0 = fid.tell()
26+
n1 = fid.seek(0,2)
27+
nsamples = (n1-n0)//4//nchan
28+
fid.seek(n0)
29+
if not stop_tic: stop_tic=nsamples
30+
fid.seek(4*nchan*start_tic, 1)
31+
b = fid.read(4*nchan*(stop_tic-start_tic))
32+
33+
v = np.frombuffer(b, dtype=np.uint32)
34+
a = np.reshape(v, (-1,nchan))
35+
36+
chs = audio_read_rec2ch()[rec]
37+
s = a[:, chs]
38+
39+
i = np.searchsorted(lut[:,0], s)
40+
m = np.take(lut[:,1], i)
41+
c = (m / 10 * np.iinfo(np.int16).max).astype(np.int16)
42+
43+
return sampling_rate, (nsamples,len(chs)), c
44+
45+
def audio_read_exts(nchan=8, **kw):
46+
return ['.aq'+str(nchan)]
47+
48+
def audio_read_rec2ch(nchan=8, **kw):
49+
return {chr(65+i):[i] for i in range(nchan)}
50+
51+
def audio_read_init(lut_file="load-epg-lut.npy", **kw):
52+
script_dir = os.path.abspath(os.path.dirname(__file__))
53+
global lut = np.load(os.path.join(script_dir, lut_file))

src/load-wav.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,12 @@ def audio_read(wav_path, start_tic, stop_tic, mmap=True):
1919
data_sliced = data[start_tic_clamped : stop_tic_clamped, :]
2020

2121
return sampling_rate, data.shape, data_sliced
22+
23+
def audio_read_exts(**kw):
24+
return ['.wav', '.WAV']
25+
26+
def audio_read_rec2ch(**kw):
27+
return {'A':[0]}
28+
29+
def audio_read_init(**kw):
30+
pass

0 commit comments

Comments
 (0)