Skip to content

Commit

Permalink
add audio frequency range choice and equal loudness normalisation opt…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
James Trayford committed Aug 15, 2024
1 parent 89d6fff commit 5c67736
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 22 deletions.
58 changes: 48 additions & 10 deletions jdaviz/configs/cubeviz/plugins/cube_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from tqdm import tqdm
from contextlib import contextmanager
import sys, os
import time
from astropy import units as u

# some beginner utility functions for STRAUSS + CubeViz

Expand All @@ -20,15 +22,18 @@ def suppress_stderr():
finally:
sys.stderr = old_stderr

def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fmin=40, fmax=1300):
def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fmin=40, fmax=1300, eln=False):
notes = [["A2"]]
score = Score(notes, duration)

#set up spectralizer generator
generator = Spectralizer(samprate=srate)

# Lets pick the mapping frequency range for the spectrum...
generator.modify_preset({'min_freq':fmin, 'max_freq':fmax})
generator.modify_preset({'min_freq':fmin, 'max_freq':fmax,
'fit_spec_multiples': False,
'interpolation_type': 'preserve_power',
'equal_loudness_normalisation': eln})

data = {'spectrum':[spec], 'pitch':[1]}

Expand All @@ -49,21 +54,31 @@ def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fm
return soni.loop_channels['0'].values

class CubeListenerData:
def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buffsize=1024, bdepth=16):
def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buffsize=1024, bdepth=16,
wl_bounds=None, wl_unit=None, audfrqmin=50, audfrqmax=1500, eln=False):
self.siglen = int(samplerate*(duration-overlap))
self.cube = cube
self.dur = duration
self.bdepth = bdepth
self.srate = samplerate
self.maxval = pow(2,bdepth-1) - 1
self.fadedx = 0


self.wl_bounds = wl_bounds
self.wl_unit = wl_unit
self.wlens = wlens

# control fades
fade = np.linspace(0,1, buffsize+1)
self.ifade = fade[:-1]
self.ofade = fade[::-1][:-1]

# mapping frequency limits in Hz
self.audfrqmin = audfrqmin
self.audfrqmax = audfrqmax

# do we normalise for equal loudness?
self.eln = eln

self.idx1 = 0
self.idx2 = 0
Expand All @@ -75,23 +90,46 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff
raise Exception("Cube projected to be > 2Gb!")

self.sigcube = np.zeros((*self.cube.shape[:2], self.siglen), dtype='int16')

def set_wl_bounds(self, w1, w2):
"""
set the wavelength bounds for indexing spectra
"""
wsrt = np.sort([w1,w2])
self.wl_bounds = tuple(wsrt)
print(w1,w2,'test')
print(self.wl_bounds)

def audify_cube(self, fmin=50, fmax=1500):
def audify_cube(self):
"""
Iterate through the cube, convert each spectrum to a signal, and store
in class attributes
"""
lo2hi = self.wlens.argsort()[::-1]
# if self.wl_bounds:
# si_wl_bounds = (self.wl_bounds * getattr(u, self.wl_unit)).to('m')
# wdx = np.logical_and(self.wlens >= si_wl_bounds[0].value,
# self.wlens <= si_wl_bounds[1].value)
# lo2hi = lo2hi[wdx]
# print (wdx, self.wlens, dir(self.wlens))
t0 = time.time()
for i in tqdm(range(self.cube.shape[0])):
for j in range(self.cube.shape[1]):
with suppress_stderr():
sig = audify_spectrum(self.cube[i,j,lo2hi], self.dur,
srate=self.srate,
fmin=fmin, fmax=fmax)
sig = (sig*self.maxval).astype('int16')
self.sigcube[i,j,:] = sig
if self.cube[i,j,lo2hi].any():
sig = audify_spectrum(self.cube[i,j,lo2hi], self.dur,
srate=self.srate,
fmin=self.audfrqmin,
fmax=self.audfrqmax,
eln=self.eln)
sig = (sig*self.maxval).astype('int16')
self.sigcube[i,j,:] = sig
else:
continue
self.cursig[:] = self.sigcube[self.idx1,self.idx2, :]
self.newsig[:] = self.cursig[:]
t1 = time.time()
print(f"Took {t1-t0}s to process {self.cube.shape[0]*self.cube.shape[1]} spaxels")

def player_callback(self, outdata, frames, time, status):
cur = self.cursig
Expand Down
10 changes: 8 additions & 2 deletions jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import PluginTemplateMixin, DatasetSelectMixin
from traitlets import Bool

__all__ = ['SonifyData']

Expand All @@ -14,13 +15,18 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin):
buffer_size = IntHandleEmpty(2048).tag(sync=True)
assidx = FloatHandleEmpty(2.5).tag(sync=True)
ssvidx = FloatHandleEmpty(0.65).tag(sync=True)
eln = Bool(False).tag(sync=True)
wavemin = FloatHandleEmpty(15800).tag(sync=True)
wavemax = FloatHandleEmpty(16000).tag(sync=True)
wavemax = FloatHandleEmpty(16000).tag(sync=True)
audfrqmin = FloatHandleEmpty(50).tag(sync=True)
audfrqmax = FloatHandleEmpty(1500).tag(sync=True)
pccut = IntHandleEmpty(20).tag(sync=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def vue_sonify_cube(self, *args):
viewer = self.app.get_viewer('flux-viewer')
viewer.get_sonified_cube(self.sample_rate, self.buffer_size, self.assidx, self.ssvidx, self.pccut)
viewer.get_sonified_cube(self.sample_rate, self.buffer_size, self.assidx,
self.ssvidx, self.pccut, self.audfrqmin,
self.audfrqmax, self.eln)
29 changes: 28 additions & 1 deletion jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@
persistent-hint
></v-text-field>
</v-row>
<v-row>
<v-text-field
ref="audfrqmin"
type="number"
label="Minimum Audio Frequency"
v-model.number="audfrqmin"
hint="The minimum audio frequency used to represent the spectra (Hz)"
persistent-hint
></v-text-field>
</v-row>
<v-row>
<v-text-field
ref="audfrqmax"
type="number"
label="Maximum Audio Frequency"
v-model.number="audfrqmax"
hint="The maximum audio frequency used to represent the spectra (Hz)"
persistent-hint
></v-text-field>
</v-row>
<v-row>
<v-text-field
ref="assidx"
Expand Down Expand Up @@ -91,7 +111,14 @@
persistent-hint
></v-text-field>
</v-row>

<v-row>
<v-switch
v-model="eln"
label="Equal Loudness Equalisation"
hint="Whether to equalise for uniform perceived loudness"
persistent-hint
></v-switch>
</v-row>
<v-row>
<plugin-action-button
@click="sonify_cube"
Expand Down
7 changes: 5 additions & 2 deletions jdaviz/configs/cubeviz/plugins/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ def activate(self):
# Store these so we can revert to previous user-set zoom after preview view
sv_state = self._spectrum_viewer.state
self._previous_bounds = [sv_state.x_min, sv_state.x_max, sv_state.y_min, sv_state.y_max]

# update listener bounds
print(sv_state.x_min, sv_state.x_display_unit)
self.viewer.audification_wl_bounds = (sv_state.x_min, sv_state.x_max)
self.viewer.audification_wl_unit = sv_state.x_display_unit
super().activate()

def deactivate(self):
Expand All @@ -140,7 +143,7 @@ def on_mouse_move(self, data):
# Use the selected layer from coords_info as long as it's 3D
coords_dataset = self.viewer.session.application._tools['g-coords-info'].dataset.selected
if coords_dataset == 'auto':
cube_data = self.viewer.active_image_layer.layer
cube_data = self.viewer.active_image_layer.layer
elif coords_dataset == 'none':
if len(self.viewer.layers):
cube_data = self.viewer.layers[0].layer
Expand Down
38 changes: 31 additions & 7 deletions jdaviz/configs/cubeviz/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData
import numpy as np
import sounddevice as sd
from astropy import units as u

__all__ = ['CubevizImageView', 'CubevizProfileView']

Expand All @@ -32,6 +33,7 @@ class CubevizImageView(JdavizViewerMixin, WithSliceSelection, BqplotImageView):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# provide reference from state back to viewer to use for zoom syncing
self.state._viewer = self

Expand All @@ -43,7 +45,9 @@ def __init__(self, *args, **kwargs):

self.audified_cube = None
self.stream = None

self.audification_wl_bounds = None
self.audification_wl_unit = None

@property
def _default_spectrum_viewer_reference_name(self):
return self.jdaviz_helper._default_spectrum_viewer_reference_name
Expand Down Expand Up @@ -99,12 +103,29 @@ def update_cube(self, x, y):
self.audified_cube.newsig = self.audified_cube.sigcube[x, y, :]
self.audified_cube.cbuff = True

def get_sonified_cube(self, sample_rate, buffer_size, assidx, ssvidx, pccut):
def update_listener_wl_bounds(self, w1,w2):
if not self.audified_cube:
return
self.audified_cube.set_wl_bounds(w1, w2)

def get_sonified_cube(self, sample_rate, buffer_size, assidx, ssvidx, pccut, audfrqmin, audfrqmax, eln):
spectrum = self.active_image_layer.layer.get_object(statistic=None)
pc_cube = np.percentile(np.nan_to_num(spectrum.flux.value), np.clip(pccut,0,99), axis=-1)

wlens = spectrum.wavelength.to('m').value
flux = spectrum.flux.value

if self.audification_wl_bounds:
wl_unit = getattr(u, self.audification_wl_unit)
si_wl_bounds = (self.audification_wl_bounds * wl_unit).to('m')
wdx = np.logical_and(wlens >= si_wl_bounds[0].value,
wlens <= si_wl_bounds[1].value)
wlens = wlens[wdx]
flux = flux[:,:,wdx]

pc_cube = np.percentile(np.nan_to_num(flux), np.clip(pccut,0,99), axis=-1)

# clip zeros and remove NaNs
clipped_arr = np.nan_to_num(np.clip(spectrum.flux.value, 0, np.inf), copy=False)
clipped_arr = np.nan_to_num(np.clip(flux, 0, np.inf), copy=False)

# make a rough white-light image from the clipped array
whitelight = np.expand_dims(clipped_arr.sum(-1), axis=2)
Expand All @@ -114,9 +135,12 @@ def get_sonified_cube(self, sample_rate, buffer_size, assidx, ssvidx, pccut):

# and re-clip
clipped_arr = np.clip(clipped_arr, 0, np.inf)

self.audified_cube = CubeListenerData(clipped_arr ** assidx, spectrum.wavelength.value, duration=0.8,
samplerate=sample_rate, buffsize=buffer_size)

# print(self.state.x_min, self.state.x_max, self._spectrum_viewer.state.x_min, self._spectrum_viewer.state.x_maX)
print(f"making cube with {self.audification_wl_bounds}")
self.audified_cube = CubeListenerData(clipped_arr ** assidx, wlens, duration=0.8,
samplerate=sample_rate, buffsize=buffer_size, wl_bounds=self.audification_wl_bounds,
wl_unit=self.audification_wl_unit, audfrqmin=audfrqmin, audfrqmax=audfrqmax)
self.audified_cube.audify_cube()
self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(whitelight / whitelight.max(), ssvidx)).astype('int16')
self.stream = sd.OutputStream(samplerate=sample_rate, blocksize=buffer_size, channels=1, dtype='int16', latency='low',
Expand Down

0 comments on commit 5c67736

Please sign in to comment.