From 6049d9aaa8c166309a6b1ebfe09faf6767b692e6 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 9 Jul 2024 13:14:10 -0400 Subject: [PATCH 01/64] Create audified cube and use with spectrum at spaxel tool --- .../configs/cubeviz/plugins/cube_listener.py | 107 ++++++++++++++++++ jdaviz/configs/cubeviz/plugins/tools.py | 37 ++++++ 2 files changed, 144 insertions(+) create mode 100644 jdaviz/configs/cubeviz/plugins/cube_listener.py diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py new file mode 100644 index 0000000000..5c1996bb50 --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -0,0 +1,107 @@ +from strauss.sonification import Sonification +from strauss.sources import Events, Objects +from strauss import channels +from strauss.score import Score +from strauss.generator import Spectralizer +import numpy as np +from tqdm import tqdm +from contextlib import contextmanager +import sys, os + +# some beginner utility functions for STRAUSS + CubeViz + +@contextmanager +def suppress_stderr(): + with open(os.devnull, "w") as devnull: + old_stderr = sys.stderr + sys.stderr = devnull + try: + yield + finally: + sys.stderr = old_stderr + +def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fmin=40, fmax=1300): + 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}) + + data = {'spectrum':[spec], 'pitch':[1]} + + # again, use maximal range for the mapped parameters + lims = {'spectrum': ('0','100')} + + # set up source + sources = Events(data.keys()) + sources.fromdict(data) + sources.apply_mapping_functions(map_lims=lims) + + # render and play sonification! + soni = Sonification(score, sources, generator, system, samprate=srate) + soni.render() + soni._make_seamless(overlap) + # print(soni.loop_channels) + # sd.play(soni.loop_channels['0'].values * 0.5,loop=True) + return soni.loop_channels['0'].values + +class CubeListenerData: + def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buffsize=1024, bdepth=16): + 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.wlens = wlens + + # control fades + fade = np.linspace(0,1, buffsize+1) + self.ifade = fade[:-1] + self.ofade = fade[::-1][:-1] + + self.idx1 = 0 + self.idx2 = 0 + self.cbuff = False + self.cursig = np.zeros(self.siglen, dtype='int16') + self.newsig = np.zeros(self.siglen, dtype='int16') + + if self.cursig.nbytes * pow(1024,-3) > 2: + raise Exception("Cube projected to be > 2Gb!") + + self.sigcube = np.zeros((self.siglen, *self.cube.shape[1:]), dtype='int16') + + def audify_cube(self, fmin=50, fmax=1500): + """ + Iterate through the cube, convert each spectrum to a signal, and store + in class attributes + """ + lo2hi = self.wlens.argsort()[::-1] + for i in tqdm(range(self.cube.shape[1])): + for j in range(self.cube.shape[2]): + with suppress_stderr(): + sig = audify_spectrum(self.cube[lo2hi,i,j], self.dur, + srate=self.srate, + fmin=fmin, fmax=fmax) + sig = (sig*self.maxval).astype('int16') + self.sigcube[:,i,j] = sig + self.cursig[:] = self.sigcube[:,self.idx1,self.idx2] + self.newsig[:] = self.cursig[:] + + def player_callback(self, outdata, frames, time, status): + cur = self.cursig + new = self.newsig + sdx = int(time.outputBufferDacTime*self.srate) + dxs = np.arange(sdx, sdx+frames).astype(int) % self.sigcube.shape[0] + if self.cbuff: + outdata[:,0] = (cur[dxs] * self.ofade).astype('int16') + outdata[:,0] += (new[dxs] * self.ifade).astype('int16') + self.cursig[:] = self.newsig[:] + self.cbuff = False + else: + outdata[:,0] = self.cursig[dxs] diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 0974b03299..4c247fea58 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -12,6 +12,10 @@ from jdaviz.core.tools import PanZoom, BoxZoom, SinglePixelRegion, _MatchedZoomMixin from jdaviz.core.marks import PluginLine +from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData +import sounddevice as sd + + __all__ = [] ICON_DIR = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'icons') @@ -95,6 +99,9 @@ def __init__(self, *args, **kwargs): self._mark = None self._data = None + self.audified_cube = None + self.stream = None + def _reset_spectrum_viewer_bounds(self): sv_state = self._spectrum_viewer.state sv_state.x_min = self._previous_bounds[0] @@ -116,22 +123,28 @@ 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] + + # Create sonified cube + self.get_sonified_cube() super().activate() def deactivate(self): self.viewer.remove_event_callback(self.on_mouse_move) self._reset_spectrum_viewer_bounds() + self.stream.stop() super().deactivate() def on_mouse_move(self, data): if data['event'] == 'mouseleave': self._mark.visible = False self._reset_spectrum_viewer_bounds() + self.stream.stop() return x = int(np.round(data['domain']['x'])) y = int(np.round(data['domain']['y'])) + # some alternative scaling # 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': @@ -165,6 +178,7 @@ def on_mouse_move(self, data): if x >= spectrum.flux.shape[0] or x < 0 or y >= spectrum.flux.shape[1] or y < 0: self._reset_spectrum_viewer_bounds() self._mark.visible = False + self.stream.stop() else: y_values = spectrum.flux[x, y, :] if np.all(np.isnan(y_values)): @@ -174,3 +188,26 @@ def on_mouse_move(self, data): self._mark.visible = True self._spectrum_viewer.state.y_max = np.nanmax(y_values.value) * 1.2 self._spectrum_viewer.state.y_min = np.nanmin(y_values.value) * 0.8 + + self.stream.start() + self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] + self.audified_cube.cbuff = True + + def get_sonified_cube(self): + spectrum = self.viewer.active_image_layer.layer.get_object(statistic=None) + srate = 44100 + bsize = 2048 + assidx = 2.5 + ssvidx = 0.65 + wavemin = 15800 + wavemax = 16000 + + clipped_arr = np.clip(spectrum.flux.value.T, 0, np.inf) + arr = spectrum[wavemin:wavemax].flux.value.T + self.audified_cube = CubeListenerData(clipped_arr ** assidx, spectrum.wavelength.value, duration=0.8, + samplerate=srate, buffsize=bsize) + self.audified_cube.audify_cube() + self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(clipped_arr.sum(0) / clipped_arr.sum(0).max(), ssvidx)).astype('int16') + self.stream = sd.OutputStream(samplerate=srate, blocksize=bsize, channels=1, dtype='int16', latency='low', + callback=self.audified_cube.player_callback) + self.audified_cube.cbuff = True From 627a34f8d71e8cad4d2218dab19b944e0ed9ee68 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 10 Jul 2024 10:30:05 -0400 Subject: [PATCH 02/64] Add Sonify Data plugin and connect to spectrum per spaxel tool --- jdaviz/app.py | 2 + jdaviz/configs/cubeviz/cubeviz.yaml | 1 + jdaviz/configs/cubeviz/plugins/__init__.py | 1 + .../cubeviz/plugins/sonify_data/__init__.py | 0 .../plugins/sonify_data/sonify_data.py | 89 ++++++++++++++++++ .../plugins/sonify_data/sonify_data.vue | 92 +++++++++++++++++++ jdaviz/configs/cubeviz/plugins/tools.py | 58 ++++++------ 7 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 jdaviz/configs/cubeviz/plugins/sonify_data/__init__.py create mode 100644 jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py create mode 100644 jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue diff --git a/jdaviz/app.py b/jdaviz/app.py index 15d721ee4f..0e8327dbb8 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -323,6 +323,8 @@ def __init__(self, configuration=None, *args, **kwargs): # data loading self.auto_link = kwargs.pop('auto_link', True) + self.sonification_enabled = False + # Imviz linking self._link_type = 'pixels' if self.config == "imviz": diff --git a/jdaviz/configs/cubeviz/cubeviz.yaml b/jdaviz/configs/cubeviz/cubeviz.yaml index 038df00431..77044885a1 100644 --- a/jdaviz/configs/cubeviz/cubeviz.yaml +++ b/jdaviz/configs/cubeviz/cubeviz.yaml @@ -33,6 +33,7 @@ tray: - specviz-line-analysis - cubeviz-moment-maps - imviz-aper-phot-simple + - cubeviz-sonify-data - export - about viewer_area: diff --git a/jdaviz/configs/cubeviz/plugins/__init__.py b/jdaviz/configs/cubeviz/plugins/__init__.py index 61390844ae..7636c2a96b 100644 --- a/jdaviz/configs/cubeviz/plugins/__init__.py +++ b/jdaviz/configs/cubeviz/plugins/__init__.py @@ -5,3 +5,4 @@ from .moment_maps.moment_maps import * # noqa from .slice.slice import * # noqa from .spectral_extraction.spectral_extraction import * # noqa +from .sonify_data.sonify_data import * # noqa diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/__init__.py b/jdaviz/configs/cubeviz/plugins/sonify_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py new file mode 100644 index 0000000000..04b33512b8 --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -0,0 +1,89 @@ +import os +from pathlib import Path + +import numpy as np +import specutils +from astropy import units as u +from astropy.nddata import CCDData +from astropy.utils import minversion +from traitlets import Bool, List, Unicode, observe +from specutils import manipulation, analysis, Spectrum1D + +from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty +from jdaviz.core.events import SnackbarMessage, GlobalDisplayUnitChanged +from jdaviz.core.registries import tray_registry +from jdaviz.core.template_mixin import (PluginTemplateMixin, + DatasetSelect, DatasetSelectMixin, + SpectralSubsetSelectMixin, + AddResultsMixin, + SelectPluginComponent, + SpectralContinuumMixin, + skip_if_no_updates_since_last_active, + with_spinner) +from jdaviz.core.validunits import check_if_unit_is_per_solid_angle +from jdaviz.core.user_api import PluginUserApi +from jdaviz.utils import flux_conversion + +from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData +import sounddevice as sd + + +__all__ = ['SonifyData'] + + +@tray_registry('cubeviz-sonify-data', label="Sonify Data", + viewer_requirements=['spectrum', 'image']) +class SonifyData(PluginTemplateMixin, DatasetSelectMixin): + template_file = __file__, "sonify_data.vue" + + sample_rate = IntHandleEmpty(44100).tag(sync=True) + buffer_size = IntHandleEmpty(2048).tag(sync=True) + assidx = FloatHandleEmpty(2.5).tag(sync=True) + ssvidx = FloatHandleEmpty(0.65).tag(sync=True) + wavemin = FloatHandleEmpty(15800).tag(sync=True) + wavemax = FloatHandleEmpty(16000).tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.audified_cube = None + self.stream = None + + def start_stream(self): + if self.stream: + self.stream.start() + else: + print("unable to start stream") + + def stop_stream(self): + if self.stream: + self.stream.stop() + else: + print("unable to stop stream") + + def update_cube(self, x, y): + if not hasattr(self.audified_cube, 'newsig') and not hasattr(self.audified_cube, 'sigcube'): + print("cube not initialized") + return + self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] + self.audified_cube.cbuff = True + + def vue_sonify_cube(self, *args): + self.get_sonified_cube() + + @with_spinner() + def get_sonified_cube(self): + viewer = self.app.get_viewer('flux-viewer') + spectrum = viewer.active_image_layer.layer.get_object(statistic=None) + + clipped_arr = np.clip(spectrum.flux.value.T, 0, np.inf) + # arr = spectrum[wavemin:wavemax].flux.value.T + self.audified_cube = CubeListenerData(clipped_arr ** self.assidx, spectrum.wavelength.value, duration=0.8, + samplerate=self.sample_rate, buffsize=self.buffer_size) + self.audified_cube.audify_cube() + self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(clipped_arr.sum(0) / clipped_arr.sum(0).max(), self.ssvidx)).astype('int16') + self.stream = sd.OutputStream(samplerate=self.sample_rate, blocksize=self.buffer_size, channels=1, dtype='int16', latency='low', + callback=self.audified_cube.player_callback) + self.audified_cube.cbuff = True + + self.app.sonification_enabled = True diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue new file mode 100644 index 0000000000..cfd0c01aa1 --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -0,0 +1,92 @@ + \ No newline at end of file diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 4c247fea58..d93b57cdc9 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -99,8 +99,7 @@ def __init__(self, *args, **kwargs): self._mark = None self._data = None - self.audified_cube = None - self.stream = None + self.sonify_data_plg = None def _reset_spectrum_viewer_bounds(self): sv_state = self._spectrum_viewer.state @@ -124,21 +123,23 @@ def activate(self): 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] - # Create sonified cube - self.get_sonified_cube() + # Retrieve sonified cube + if self.app.sonification_enabled: + self.sonify_data_plg = self.viewer.jdaviz_helper.app.get_tray_item_from_name('cubeviz-sonify-data') + self.sonify_data_plg.start_stream() super().activate() def deactivate(self): self.viewer.remove_event_callback(self.on_mouse_move) self._reset_spectrum_viewer_bounds() - self.stream.stop() + self.sonify_data_plg.stop_stream() super().deactivate() def on_mouse_move(self, data): if data['event'] == 'mouseleave': self._mark.visible = False self._reset_spectrum_viewer_bounds() - self.stream.stop() + self.sonify_data_plg.stop_stream() return x = int(np.round(data['domain']['x'])) @@ -178,7 +179,7 @@ def on_mouse_move(self, data): if x >= spectrum.flux.shape[0] or x < 0 or y >= spectrum.flux.shape[1] or y < 0: self._reset_spectrum_viewer_bounds() self._mark.visible = False - self.stream.stop() + self.sonify_data_plg.stop_stream() else: y_values = spectrum.flux[x, y, :] if np.all(np.isnan(y_values)): @@ -189,25 +190,24 @@ def on_mouse_move(self, data): self._spectrum_viewer.state.y_max = np.nanmax(y_values.value) * 1.2 self._spectrum_viewer.state.y_min = np.nanmin(y_values.value) * 0.8 - self.stream.start() - self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] - self.audified_cube.cbuff = True - - def get_sonified_cube(self): - spectrum = self.viewer.active_image_layer.layer.get_object(statistic=None) - srate = 44100 - bsize = 2048 - assidx = 2.5 - ssvidx = 0.65 - wavemin = 15800 - wavemax = 16000 - - clipped_arr = np.clip(spectrum.flux.value.T, 0, np.inf) - arr = spectrum[wavemin:wavemax].flux.value.T - self.audified_cube = CubeListenerData(clipped_arr ** assidx, spectrum.wavelength.value, duration=0.8, - samplerate=srate, buffsize=bsize) - self.audified_cube.audify_cube() - self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(clipped_arr.sum(0) / clipped_arr.sum(0).max(), ssvidx)).astype('int16') - self.stream = sd.OutputStream(samplerate=srate, blocksize=bsize, channels=1, dtype='int16', latency='low', - callback=self.audified_cube.player_callback) - self.audified_cube.cbuff = True + self.sonify_data_plg.start_stream() + self.sonify_data_plg.update_cube(x, y) + + # def get_sonified_cube(self): + # spectrum = self.viewer.active_image_layer.layer.get_object(statistic=None) + # srate = 44100 + # bsize = 2048 + # assidx = 2.5 + # ssvidx = 0.65 + # wavemin = 15800 + # wavemax = 16000 + # + # clipped_arr = np.clip(spectrum.flux.value.T, 0, np.inf) + # arr = spectrum[wavemin:wavemax].flux.value.T + # self.audified_cube = CubeListenerData(clipped_arr ** assidx, spectrum.wavelength.value, duration=0.8, + # samplerate=srate, buffsize=bsize) + # self.audified_cube.audify_cube() + # self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(clipped_arr.sum(0) / clipped_arr.sum(0).max(), ssvidx)).astype('int16') + # self.stream = sd.OutputStream(samplerate=srate, blocksize=bsize, channels=1, dtype='int16', latency='low', + # callback=self.audified_cube.player_callback) + # self.audified_cube.cbuff = True From 9e98b32721335f16368134b349958fd508757eb9 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 10 Jul 2024 10:36:28 -0400 Subject: [PATCH 03/64] Fix errors --- jdaviz/app.py | 2 -- jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py | 4 +--- jdaviz/configs/cubeviz/plugins/tools.py | 5 ++--- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 0e8327dbb8..15d721ee4f 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -323,8 +323,6 @@ def __init__(self, configuration=None, *args, **kwargs): # data loading self.auto_link = kwargs.pop('auto_link', True) - self.sonification_enabled = False - # Imviz linking self._link_type = 'pixels' if self.config == "imviz": diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 04b33512b8..661d5698c0 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -62,7 +62,7 @@ def stop_stream(self): print("unable to stop stream") def update_cube(self, x, y): - if not hasattr(self.audified_cube, 'newsig') and not hasattr(self.audified_cube, 'sigcube'): + if not self.audified_cube and not hasattr(self.audified_cube, 'newsig') and not hasattr(self.audified_cube, 'sigcube'): print("cube not initialized") return self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] @@ -85,5 +85,3 @@ def get_sonified_cube(self): self.stream = sd.OutputStream(samplerate=self.sample_rate, blocksize=self.buffer_size, channels=1, dtype='int16', latency='low', callback=self.audified_cube.player_callback) self.audified_cube.cbuff = True - - self.app.sonification_enabled = True diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index d93b57cdc9..d848d87e35 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -124,9 +124,8 @@ def activate(self): self._previous_bounds = [sv_state.x_min, sv_state.x_max, sv_state.y_min, sv_state.y_max] # Retrieve sonified cube - if self.app.sonification_enabled: - self.sonify_data_plg = self.viewer.jdaviz_helper.app.get_tray_item_from_name('cubeviz-sonify-data') - self.sonify_data_plg.start_stream() + self.sonify_data_plg = self.viewer._obj.jdaviz_helper.app.get_tray_item_from_name('cubeviz-sonify-data') + self.sonify_data_plg.start_stream() super().activate() def deactivate(self): From e6d52b48970a7a098235c033db216006d7d868ba Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 10 Jul 2024 10:55:10 -0400 Subject: [PATCH 04/64] Try moving code to mixin --- .../plugins/sonify_data/sonify_data.py | 26 ++----------------- jdaviz/configs/cubeviz/plugins/tools.py | 22 +++++++--------- jdaviz/core/template_mixin.py | 25 ++++++++++++++++++ 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 661d5698c0..0f561fefea 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -19,7 +19,7 @@ SelectPluginComponent, SpectralContinuumMixin, skip_if_no_updates_since_last_active, - with_spinner) + with_spinner, SonifiedCubeMixin) from jdaviz.core.validunits import check_if_unit_is_per_solid_angle from jdaviz.core.user_api import PluginUserApi from jdaviz.utils import flux_conversion @@ -33,7 +33,7 @@ @tray_registry('cubeviz-sonify-data', label="Sonify Data", viewer_requirements=['spectrum', 'image']) -class SonifyData(PluginTemplateMixin, DatasetSelectMixin): +class SonifyData(PluginTemplateMixin, DatasetSelectMixin, SonifiedCubeMixin): template_file = __file__, "sonify_data.vue" sample_rate = IntHandleEmpty(44100).tag(sync=True) @@ -46,28 +46,6 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.audified_cube = None - self.stream = None - - def start_stream(self): - if self.stream: - self.stream.start() - else: - print("unable to start stream") - - def stop_stream(self): - if self.stream: - self.stream.stop() - else: - print("unable to stop stream") - - def update_cube(self, x, y): - if not self.audified_cube and not hasattr(self.audified_cube, 'newsig') and not hasattr(self.audified_cube, 'sigcube'): - print("cube not initialized") - return - self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] - self.audified_cube.cbuff = True - def vue_sonify_cube(self, *args): self.get_sonified_cube() diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index d848d87e35..777afc4cef 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -11,9 +11,8 @@ from jdaviz.core.events import SliceToolStateMessage, SliceSelectSliceMessage from jdaviz.core.tools import PanZoom, BoxZoom, SinglePixelRegion, _MatchedZoomMixin from jdaviz.core.marks import PluginLine +from jdaviz.core.template_mixin import SonifiedCubeMixin -from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData -import sounddevice as sd __all__ = [] @@ -85,7 +84,7 @@ def on_mouse_event(self, data): @viewer_tool -class SpectrumPerSpaxel(SinglePixelRegion): +class SpectrumPerSpaxel(SinglePixelRegion, SonifiedCubeMixin): icon = os.path.join(ICON_DIR, 'pixelspectra.svg') tool_id = 'jdaviz:spectrumperspaxel' @@ -99,7 +98,7 @@ def __init__(self, *args, **kwargs): self._mark = None self._data = None - self.sonify_data_plg = None + # self.sonify_data_plg = None def _reset_spectrum_viewer_bounds(self): sv_state = self._spectrum_viewer.state @@ -123,22 +122,21 @@ def activate(self): 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] - # Retrieve sonified cube - self.sonify_data_plg = self.viewer._obj.jdaviz_helper.app.get_tray_item_from_name('cubeviz-sonify-data') - self.sonify_data_plg.start_stream() + # Retrieve sonified cube if present + self.start_stream() super().activate() def deactivate(self): self.viewer.remove_event_callback(self.on_mouse_move) self._reset_spectrum_viewer_bounds() - self.sonify_data_plg.stop_stream() + self.stop_stream() super().deactivate() def on_mouse_move(self, data): if data['event'] == 'mouseleave': self._mark.visible = False self._reset_spectrum_viewer_bounds() - self.sonify_data_plg.stop_stream() + self.stop_stream() return x = int(np.round(data['domain']['x'])) @@ -178,7 +176,7 @@ def on_mouse_move(self, data): if x >= spectrum.flux.shape[0] or x < 0 or y >= spectrum.flux.shape[1] or y < 0: self._reset_spectrum_viewer_bounds() self._mark.visible = False - self.sonify_data_plg.stop_stream() + self.stop_stream() else: y_values = spectrum.flux[x, y, :] if np.all(np.isnan(y_values)): @@ -189,8 +187,8 @@ def on_mouse_move(self, data): self._spectrum_viewer.state.y_max = np.nanmax(y_values.value) * 1.2 self._spectrum_viewer.state.y_min = np.nanmin(y_values.value) * 0.8 - self.sonify_data_plg.start_stream() - self.sonify_data_plg.update_cube(x, y) + self.start_stream() + self.update_cube(x, y) # def get_sonified_cube(self): # spectrum = self.viewer.active_image_layer.layer.get_object(statistic=None) diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index a5f1a775ed..0c943717dc 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -4972,3 +4972,28 @@ def clear_plot(self): Clear all data from the current plot. """ self.plot.clear_plot() + + +class SonifiedCubeMixin: + def __init__(self, *args, **kwargs): + self.audified_cube = None + self.stream = None + + def start_stream(self): + if hasattr(self, 'stream') and self.stream: + self.stream.start() + else: + print("unable to start stream") + + def stop_stream(self): + if hasattr(self, 'stream') and self.stream: + self.stream.stop() + else: + print("unable to stop stream") + + def update_cube(self, x, y): + if not hasattr(self, 'audified_cube') or not self.audified_cube or not hasattr(self.audified_cube, 'newsig') or not hasattr(self.audified_cube, 'sigcube'): + print("cube not initialized") + return + self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] + self.audified_cube.cbuff = True From 0080b2fd288f29d2b113f539dc07470251a616a8 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 10 Jul 2024 11:20:13 -0400 Subject: [PATCH 05/64] Move code to viewers.py --- .../plugins/sonify_data/sonify_data.py | 46 +---- jdaviz/configs/cubeviz/plugins/tools.py | 37 +--- jdaviz/configs/cubeviz/plugins/viewers.py | 181 +++++++++++++++++- jdaviz/core/template_mixin.py | 25 --- 4 files changed, 189 insertions(+), 100 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 0f561fefea..643dfd53c5 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -1,39 +1,13 @@ -import os -from pathlib import Path - -import numpy as np -import specutils -from astropy import units as u -from astropy.nddata import CCDData -from astropy.utils import minversion -from traitlets import Bool, List, Unicode, observe -from specutils import manipulation, analysis, Spectrum1D - from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty -from jdaviz.core.events import SnackbarMessage, GlobalDisplayUnitChanged from jdaviz.core.registries import tray_registry -from jdaviz.core.template_mixin import (PluginTemplateMixin, - DatasetSelect, DatasetSelectMixin, - SpectralSubsetSelectMixin, - AddResultsMixin, - SelectPluginComponent, - SpectralContinuumMixin, - skip_if_no_updates_since_last_active, - with_spinner, SonifiedCubeMixin) -from jdaviz.core.validunits import check_if_unit_is_per_solid_angle -from jdaviz.core.user_api import PluginUserApi -from jdaviz.utils import flux_conversion - -from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData -import sounddevice as sd - +from jdaviz.core.template_mixin import PluginTemplateMixin, DatasetSelectMixin __all__ = ['SonifyData'] @tray_registry('cubeviz-sonify-data', label="Sonify Data", viewer_requirements=['spectrum', 'image']) -class SonifyData(PluginTemplateMixin, DatasetSelectMixin, SonifiedCubeMixin): +class SonifyData(PluginTemplateMixin, DatasetSelectMixin): template_file = __file__, "sonify_data.vue" sample_rate = IntHandleEmpty(44100).tag(sync=True) @@ -47,19 +21,5 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def vue_sonify_cube(self, *args): - self.get_sonified_cube() - - @with_spinner() - def get_sonified_cube(self): viewer = self.app.get_viewer('flux-viewer') - spectrum = viewer.active_image_layer.layer.get_object(statistic=None) - - clipped_arr = np.clip(spectrum.flux.value.T, 0, np.inf) - # arr = spectrum[wavemin:wavemax].flux.value.T - self.audified_cube = CubeListenerData(clipped_arr ** self.assidx, spectrum.wavelength.value, duration=0.8, - samplerate=self.sample_rate, buffsize=self.buffer_size) - self.audified_cube.audify_cube() - self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(clipped_arr.sum(0) / clipped_arr.sum(0).max(), self.ssvidx)).astype('int16') - self.stream = sd.OutputStream(samplerate=self.sample_rate, blocksize=self.buffer_size, channels=1, dtype='int16', latency='low', - callback=self.audified_cube.player_callback) - self.audified_cube.cbuff = True + viewer.get_sonified_cube(self.sample_rate, self.buffer_size, self.assidx, self.ssvidx) diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 777afc4cef..10aa59a999 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -11,8 +11,6 @@ from jdaviz.core.events import SliceToolStateMessage, SliceSelectSliceMessage from jdaviz.core.tools import PanZoom, BoxZoom, SinglePixelRegion, _MatchedZoomMixin from jdaviz.core.marks import PluginLine -from jdaviz.core.template_mixin import SonifiedCubeMixin - __all__ = [] @@ -84,7 +82,7 @@ def on_mouse_event(self, data): @viewer_tool -class SpectrumPerSpaxel(SinglePixelRegion, SonifiedCubeMixin): +class SpectrumPerSpaxel(SinglePixelRegion): icon = os.path.join(ICON_DIR, 'pixelspectra.svg') tool_id = 'jdaviz:spectrumperspaxel' @@ -98,8 +96,6 @@ def __init__(self, *args, **kwargs): self._mark = None self._data = None - # self.sonify_data_plg = None - def _reset_spectrum_viewer_bounds(self): sv_state = self._spectrum_viewer.state sv_state.x_min = self._previous_bounds[0] @@ -122,21 +118,19 @@ def activate(self): 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] - # Retrieve sonified cube if present - self.start_stream() super().activate() def deactivate(self): self.viewer.remove_event_callback(self.on_mouse_move) self._reset_spectrum_viewer_bounds() - self.stop_stream() + self.viewer.stop_stream() super().deactivate() def on_mouse_move(self, data): if data['event'] == 'mouseleave': self._mark.visible = False self._reset_spectrum_viewer_bounds() - self.stop_stream() + self.viewer.stop_stream() return x = int(np.round(data['domain']['x'])) @@ -176,7 +170,7 @@ def on_mouse_move(self, data): if x >= spectrum.flux.shape[0] or x < 0 or y >= spectrum.flux.shape[1] or y < 0: self._reset_spectrum_viewer_bounds() self._mark.visible = False - self.stop_stream() + self.viewer.stop_stream() else: y_values = spectrum.flux[x, y, :] if np.all(np.isnan(y_values)): @@ -187,24 +181,5 @@ def on_mouse_move(self, data): self._spectrum_viewer.state.y_max = np.nanmax(y_values.value) * 1.2 self._spectrum_viewer.state.y_min = np.nanmin(y_values.value) * 0.8 - self.start_stream() - self.update_cube(x, y) - - # def get_sonified_cube(self): - # spectrum = self.viewer.active_image_layer.layer.get_object(statistic=None) - # srate = 44100 - # bsize = 2048 - # assidx = 2.5 - # ssvidx = 0.65 - # wavemin = 15800 - # wavemax = 16000 - # - # clipped_arr = np.clip(spectrum.flux.value.T, 0, np.inf) - # arr = spectrum[wavemin:wavemax].flux.value.T - # self.audified_cube = CubeListenerData(clipped_arr ** assidx, spectrum.wavelength.value, duration=0.8, - # samplerate=srate, buffsize=bsize) - # self.audified_cube.audify_cube() - # self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(clipped_arr.sum(0) / clipped_arr.sum(0).max(), ssvidx)).astype('int16') - # self.stream = sd.OutputStream(samplerate=srate, blocksize=bsize, channels=1, dtype='int16', latency='low', - # callback=self.audified_cube.player_callback) - # self.audified_cube.cbuff = True + self.viewer.start_stream() + self.viewer.update_cube(x, y) diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 64dea746e4..ad55a8bd1c 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -6,8 +6,152 @@ from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView from jdaviz.core.freezable_state import FreezableBqplotImageViewerState +from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData +import sounddevice as sd -__all__ = ['CubevizImageView', 'CubevizProfileView'] +__all__ = ['CubevizImageView', 'CubevizProfileView', + 'WithSliceIndicator', 'WithSliceSelection'] + + +class WithSliceIndicator: + @property + def slice_component_label(self): + return str(self.state.x_att) + + @property + def slice_display_unit_name(self): + return 'spectral' + + @cached_property + def slice_indicator(self): + # SliceIndicatorMarks does not yet exist + slice_indicator = SliceIndicatorMarks(self) + self.figure.marks = self.figure.marks + slice_indicator.marks + return slice_indicator + + @property + def slice_values(self): + # NOTE: these are cached at the slice-plugin level + # Retrieve display units + slice_display_units = self.jdaviz_app._get_display_unit( + self.slice_display_unit_name + ) + + def _get_component(layer): + try: + # Retrieve layer data and units + data_comp = layer.layer.data.get_component(self.slice_component_label) + except (AttributeError, KeyError): + # layer either does not have get_component (because its a subset) + # or slice_component_label is not a component in this layer + # either way, return an empty array and skip this layer + return np.array([]) + + # Convert axis if display units are set and are different + data_units = getattr(data_comp, 'units', None) + if slice_display_units and data_units and slice_display_units != data_units: + data = np.asarray(data_comp.data, dtype=float) * u.Unit(data_units) + return data.to_value(slice_display_units, + equivalencies=u.spectral()) + else: + return data_comp.data + try: + return np.asarray(np.unique(np.concatenate([_get_component(layer) for layer in self.layers])), # noqa + dtype=float) + except ValueError: + # NOTE: this will result in caching an empty list + return np.array([]) + + def _set_slice_indicator_value(self, value): + # this is a separate method so that viewers can override and map value if necessary + # NOTE: on first call, this will initialize the indicator itself + self.slice_indicator.value = value + + +class WithSliceSelection: + @property + def slice_index(self): + # index in state.slices corresponding to the slice axis + return 2 + + @property + def slice_component_label(self): + slice_plg = self.jdaviz_helper.plugins.get('Slice', None) + if slice_plg is None: # pragma: no cover + raise ValueError("slice plugin must be activated to access slice_component_label") + return slice_plg._obj.slice_indicator_viewers[0].slice_component_label + + @property + def slice_display_unit_name(self): + return 'spectral' + + @property + def slice_values(self): + # NOTE: these are cached at the slice-plugin level + # TODO: add support for multiple cubes (but then slice selection needs to be more complex) + # if slice_index is 0, then we want the equivalent of [:, 0, 0] + # if slice_index is 1, then we want the equivalent of [0, :, 0] + # if slice_index is 2, then we want the equivalent of [0, 0, :] + take_inds = [2, 1, 0] + take_inds.remove(self.slice_index) + converted_axis = np.array([]) + for layer in self.layers: + world_comp_ids = layer.layer.data.world_component_ids + if self.slice_index >= len(world_comp_ids): + # Case where 2D image is loaded in image viewer + continue + + # Retrieve display units + slice_display_units = self.jdaviz_app._get_display_unit( + self.slice_display_unit_name + ) + + try: + # Retrieve layer data and units using the slice index of the world components ids + data_comp = layer.layer.data.get_component(world_comp_ids[self.slice_index]) + except (AttributeError, KeyError): + continue + + data = np.asarray(data_comp.data.take(0, take_inds[0]).take(0, take_inds[1]), # noqa + dtype=float) + + # Convert to display units if applicable + data_units = getattr(data_comp, 'units', None) + if slice_display_units and data_units and slice_display_units != data_units: + converted_axis = (data * u.Unit(data_units)).to_value( + slice_display_units, + equivalencies=u.spectral() + u.pixel_scale(1*u.pix) + ) + else: + converted_axis = data + + return converted_axis + + @property + def slice(self): + return self.state.slices[self.slice_index] + + @slice.setter + def slice(self, slice): + # NOTE: not intended for user-access - this should be controlled through the slice plugin + # in order to sync with all other viewers/slice indicators + slices = [0, 0, 0] + slices[self.slice_index] = slice + self.state.slices = tuple(slices) + + @property + def slice_value(self): + return self.slice_values[self.slice] + + @slice_value.setter + def slice_value(self, slice_value): + # NOTE: not intended for user-access - this should be controlled through the slice plugin + # in order to sync with all other viewers/slice indicators + # find the slice nearest slice_value + slice_values = self.slice_values + if not len(slice_values): + return + self.slice = np.argmin(abs(slice_values - slice_value)) @viewer_registry("cubeviz-image-viewer", label="Image 2D (Cubeviz)") @@ -39,6 +183,9 @@ def __init__(self, *args, **kwargs): # Hide axes by default self.state.show_axes = False + self.audified_cube = None + self.stream = None + @property def _default_spectrum_viewer_reference_name(self): return self.jdaviz_helper._default_spectrum_viewer_reference_name @@ -80,6 +227,38 @@ def data(self, cls=None): if hasattr(layer_state, 'layer') and isinstance(layer_state.layer, BaseData)] + def start_stream(self): + if hasattr(self, 'stream') and self.stream: + self.stream.start() + else: + print("unable to start stream") + + def stop_stream(self): + if hasattr(self, 'stream') and self.stream: + self.stream.stop() + else: + print("unable to stop stream") + + def update_cube(self, x, y): + if not hasattr(self, 'audified_cube') or not self.audified_cube or not hasattr(self.audified_cube, 'newsig') or not hasattr(self.audified_cube, 'sigcube'): + print("cube not initialized") + return + 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): + spectrum = self.active_image_layer.layer.get_object(statistic=None) + + clipped_arr = np.clip(spectrum.flux.value.T, 0, np.inf) + # arr = spectrum[wavemin:wavemax].flux.value.T + self.audified_cube = CubeListenerData(clipped_arr ** assidx, spectrum.wavelength.value, duration=0.8, + samplerate=sample_rate, buffsize=buffer_size) + self.audified_cube.audify_cube() + self.audified_cube.sigcube = (self.audified_cube.sigcube * pow(clipped_arr.sum(0) / clipped_arr.sum(0).max(), ssvidx)).astype('int16') + self.stream = sd.OutputStream(samplerate=sample_rate, blocksize=buffer_size, channels=1, dtype='int16', latency='low', + callback=self.audified_cube.player_callback) + self.audified_cube.cbuff = True + @viewer_registry("cubeviz-profile-viewer", label="Profile 1D (Cubeviz)") class CubevizProfileView(SpecvizProfileView, WithSliceIndicator): diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 0c943717dc..a5f1a775ed 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -4972,28 +4972,3 @@ def clear_plot(self): Clear all data from the current plot. """ self.plot.clear_plot() - - -class SonifiedCubeMixin: - def __init__(self, *args, **kwargs): - self.audified_cube = None - self.stream = None - - def start_stream(self): - if hasattr(self, 'stream') and self.stream: - self.stream.start() - else: - print("unable to start stream") - - def stop_stream(self): - if hasattr(self, 'stream') and self.stream: - self.stream.stop() - else: - print("unable to stop stream") - - def update_cube(self, x, y): - if not hasattr(self, 'audified_cube') or not self.audified_cube or not hasattr(self.audified_cube, 'newsig') or not hasattr(self.audified_cube, 'sigcube'): - print("cube not initialized") - return - self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] - self.audified_cube.cbuff = True From 4b0130462c6b22a0a6b7c06e5637d9daf2077a27 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 10 Jul 2024 11:43:50 -0400 Subject: [PATCH 06/64] Remove print statements --- jdaviz/configs/cubeviz/plugins/viewers.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index ad55a8bd1c..8cac084c94 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -228,20 +228,15 @@ def data(self, cls=None): isinstance(layer_state.layer, BaseData)] def start_stream(self): - if hasattr(self, 'stream') and self.stream: + if self.stream: self.stream.start() - else: - print("unable to start stream") def stop_stream(self): - if hasattr(self, 'stream') and self.stream: + if self.stream: self.stream.stop() - else: - print("unable to stop stream") def update_cube(self, x, y): - if not hasattr(self, 'audified_cube') or not self.audified_cube or not hasattr(self.audified_cube, 'newsig') or not hasattr(self.audified_cube, 'sigcube'): - print("cube not initialized") + if not self.audified_cube or not hasattr(self.audified_cube, 'newsig') or not hasattr(self.audified_cube, 'sigcube'): return self.audified_cube.newsig = self.audified_cube.sigcube[:, x, y] self.audified_cube.cbuff = True From 505f73ff95cef813ae98e269d45844975dd3a843 Mon Sep 17 00:00:00 2001 From: James Trayford Date: Thu, 11 Jul 2024 10:49:44 +0100 Subject: [PATCH 07/64] Patches: - Fix sigcube alignment with other cubes [get rid of transpose] - Add percentile cut option that enables some rough feature isolation without full continuum subtraction - Protect against cubes w/NaNs --- .../configs/cubeviz/plugins/cube_listener.py | 14 ++++++------- .../plugins/sonify_data/sonify_data.py | 5 +++-- .../plugins/sonify_data/sonify_data.vue | 13 +++++++++++- jdaviz/configs/cubeviz/plugins/viewers.py | 21 ++++++++++++++----- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 5c1996bb50..9ce31f937e 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -74,7 +74,7 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff if self.cursig.nbytes * pow(1024,-3) > 2: raise Exception("Cube projected to be > 2Gb!") - self.sigcube = np.zeros((self.siglen, *self.cube.shape[1:]), dtype='int16') + self.sigcube = np.zeros((*self.cube.shape[:2], self.siglen), dtype='int16') def audify_cube(self, fmin=50, fmax=1500): """ @@ -82,22 +82,22 @@ def audify_cube(self, fmin=50, fmax=1500): in class attributes """ lo2hi = self.wlens.argsort()[::-1] - for i in tqdm(range(self.cube.shape[1])): - for j in range(self.cube.shape[2]): + 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[lo2hi,i,j], self.dur, + 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 - self.cursig[:] = self.sigcube[:,self.idx1,self.idx2] + self.sigcube[i,j,:] = sig + self.cursig[:] = self.sigcube[self.idx1,self.idx2, :] self.newsig[:] = self.cursig[:] def player_callback(self, outdata, frames, time, status): cur = self.cursig new = self.newsig sdx = int(time.outputBufferDacTime*self.srate) - dxs = np.arange(sdx, sdx+frames).astype(int) % self.sigcube.shape[0] + dxs = np.arange(sdx, sdx+frames).astype(int) % self.sigcube.shape[-1] if self.cbuff: outdata[:,0] = (cur[dxs] * self.ofade).astype('int16') outdata[:,0] += (new[dxs] * self.ifade).astype('int16') diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 643dfd53c5..e1403c995f 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -16,10 +16,11 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): ssvidx = FloatHandleEmpty(0.65).tag(sync=True) wavemin = FloatHandleEmpty(15800).tag(sync=True) wavemax = FloatHandleEmpty(16000).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) + viewer.get_sonified_cube(self.sample_rate, self.buffer_size, self.assidx, self.ssvidx, self.pccut) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index cfd0c01aa1..a9ce2e8f6d 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -71,7 +71,7 @@ persistent-hint > - + + + + + Date: Wed, 31 Jul 2024 17:55:26 +0100 Subject: [PATCH 08/64] fix np import --- jdaviz/configs/cubeviz/plugins/viewers.py | 146 +--------------------- 1 file changed, 2 insertions(+), 144 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 9752d6055e..dab4f58ebf 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -7,152 +7,10 @@ from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView from jdaviz.core.freezable_state import FreezableBqplotImageViewerState from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData +import numpy as np import sounddevice as sd -__all__ = ['CubevizImageView', 'CubevizProfileView', - 'WithSliceIndicator', 'WithSliceSelection'] - - -class WithSliceIndicator: - @property - def slice_component_label(self): - return str(self.state.x_att) - - @property - def slice_display_unit_name(self): - return 'spectral' - - @cached_property - def slice_indicator(self): - # SliceIndicatorMarks does not yet exist - slice_indicator = SliceIndicatorMarks(self) - self.figure.marks = self.figure.marks + slice_indicator.marks - return slice_indicator - - @property - def slice_values(self): - # NOTE: these are cached at the slice-plugin level - # Retrieve display units - slice_display_units = self.jdaviz_app._get_display_unit( - self.slice_display_unit_name - ) - - def _get_component(layer): - try: - # Retrieve layer data and units - data_comp = layer.layer.data.get_component(self.slice_component_label) - except (AttributeError, KeyError): - # layer either does not have get_component (because its a subset) - # or slice_component_label is not a component in this layer - # either way, return an empty array and skip this layer - return np.array([]) - - # Convert axis if display units are set and are different - data_units = getattr(data_comp, 'units', None) - if slice_display_units and data_units and slice_display_units != data_units: - data = np.asarray(data_comp.data, dtype=float) * u.Unit(data_units) - return data.to_value(slice_display_units, - equivalencies=u.spectral()) - else: - return data_comp.data - try: - return np.asarray(np.unique(np.concatenate([_get_component(layer) for layer in self.layers])), # noqa - dtype=float) - except ValueError: - # NOTE: this will result in caching an empty list - return np.array([]) - - def _set_slice_indicator_value(self, value): - # this is a separate method so that viewers can override and map value if necessary - # NOTE: on first call, this will initialize the indicator itself - self.slice_indicator.value = value - - -class WithSliceSelection: - @property - def slice_index(self): - # index in state.slices corresponding to the slice axis - return 2 - - @property - def slice_component_label(self): - slice_plg = self.jdaviz_helper.plugins.get('Slice', None) - if slice_plg is None: # pragma: no cover - raise ValueError("slice plugin must be activated to access slice_component_label") - return slice_plg._obj.slice_indicator_viewers[0].slice_component_label - - @property - def slice_display_unit_name(self): - return 'spectral' - - @property - def slice_values(self): - # NOTE: these are cached at the slice-plugin level - # TODO: add support for multiple cubes (but then slice selection needs to be more complex) - # if slice_index is 0, then we want the equivalent of [:, 0, 0] - # if slice_index is 1, then we want the equivalent of [0, :, 0] - # if slice_index is 2, then we want the equivalent of [0, 0, :] - take_inds = [2, 1, 0] - take_inds.remove(self.slice_index) - converted_axis = np.array([]) - for layer in self.layers: - world_comp_ids = layer.layer.data.world_component_ids - if self.slice_index >= len(world_comp_ids): - # Case where 2D image is loaded in image viewer - continue - - # Retrieve display units - slice_display_units = self.jdaviz_app._get_display_unit( - self.slice_display_unit_name - ) - - try: - # Retrieve layer data and units using the slice index of the world components ids - data_comp = layer.layer.data.get_component(world_comp_ids[self.slice_index]) - except (AttributeError, KeyError): - continue - - data = np.asarray(data_comp.data.take(0, take_inds[0]).take(0, take_inds[1]), # noqa - dtype=float) - - # Convert to display units if applicable - data_units = getattr(data_comp, 'units', None) - if slice_display_units and data_units and slice_display_units != data_units: - converted_axis = (data * u.Unit(data_units)).to_value( - slice_display_units, - equivalencies=u.spectral() + u.pixel_scale(1*u.pix) - ) - else: - converted_axis = data - - return converted_axis - - @property - def slice(self): - return self.state.slices[self.slice_index] - - @slice.setter - def slice(self, slice): - # NOTE: not intended for user-access - this should be controlled through the slice plugin - # in order to sync with all other viewers/slice indicators - slices = [0, 0, 0] - slices[self.slice_index] = slice - self.state.slices = tuple(slices) - - @property - def slice_value(self): - return self.slice_values[self.slice] - - @slice_value.setter - def slice_value(self, slice_value): - # NOTE: not intended for user-access - this should be controlled through the slice plugin - # in order to sync with all other viewers/slice indicators - # find the slice nearest slice_value - slice_values = self.slice_values - if not len(slice_values): - return - self.slice = np.argmin(abs(slice_values - slice_value)) - +__all__ = ['CubevizImageView', 'CubevizProfileView'] @viewer_registry("cubeviz-image-viewer", label="Image 2D (Cubeviz)") class CubevizImageView(JdavizViewerMixin, WithSliceSelection, BqplotImageView): From 5c67736859b46954e0579a717d4b0641f3d370b7 Mon Sep 17 00:00:00 2001 From: James Trayford Date: Thu, 15 Aug 2024 15:47:57 +0100 Subject: [PATCH 09/64] add audio frequency range choice and equal loudness normalisation options --- .../configs/cubeviz/plugins/cube_listener.py | 58 +++++++++++++++---- .../plugins/sonify_data/sonify_data.py | 10 +++- .../plugins/sonify_data/sonify_data.vue | 29 +++++++++- jdaviz/configs/cubeviz/plugins/tools.py | 7 ++- jdaviz/configs/cubeviz/plugins/viewers.py | 38 +++++++++--- 5 files changed, 120 insertions(+), 22 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 9ce31f937e..754c5b4c90 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -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 @@ -20,7 +22,7 @@ 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) @@ -28,7 +30,10 @@ def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fm 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]} @@ -49,7 +54,8 @@ 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 @@ -57,13 +63,22 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff 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 @@ -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 diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index e1403c995f..461f80645c 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -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'] @@ -14,8 +15,11 @@ 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): @@ -23,4 +27,6 @@ def __init__(self, *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) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index a9ce2e8f6d..2d1a8639ad 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -41,6 +41,26 @@ persistent-hint > + + + + + + - + + + = 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) @@ -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', From d8937c5b91a46aeb04f370a1020ec587e69e9110 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 9 Oct 2024 16:05:01 -0400 Subject: [PATCH 10/64] Create dropdown to select output sound device --- .../plugins/sonify_data/sonify_data.py | 15 +++++++++--- .../plugins/sonify_data/sonify_data.vue | 11 +++++++++ jdaviz/configs/cubeviz/plugins/viewers.py | 23 +++++++++++-------- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 461f80645c..343cfabfdd 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -1,7 +1,9 @@ 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 +from traitlets import Bool, List, Unicode, observe +import sounddevice as sd + __all__ = ['SonifyData'] @@ -21,12 +23,19 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): audfrqmin = FloatHandleEmpty(50).tag(sync=True) audfrqmax = FloatHandleEmpty(1500).tag(sync=True) pccut = IntHandleEmpty(20).tag(sync=True) + + sound_devices_items = List().tag(sync=True) + sound_devices_selected = Unicode('').tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.sound_devices_items = [device['name'] for device in sd.query_devices()] + self.sound_devices_selected = self.sound_devices_items[sd.default.device[1]] 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, self.audfrqmin, + # Get index of selected device since name may not be unique + selected_device_index = self.sound_devices_items.index(self.sound_devices_selected) + viewer.get_sonified_cube(self.sample_rate, self.buffer_size, selected_device_index, + self.assidx, self.ssvidx, self.pccut, self.audfrqmin, self.audfrqmax, self.eln) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index 2d1a8639ad..29e8d2b6cc 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -20,6 +20,17 @@ label="Data" hint="Select the data set." /> + + + = si_wl_bounds[0].value, wlens <= si_wl_bounds[1].value) wlens = wlens[wdx] - flux = flux[:,:,wdx] + flux = flux[:, :, wdx] - pc_cube = np.percentile(np.nan_to_num(flux), np.clip(pccut,0,99), axis=-1) + 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(flux, 0, np.inf), copy=False) @@ -136,14 +137,18 @@ def get_sonified_cube(self, sample_rate, buffer_size, assidx, ssvidx, pccut, aud # and re-clip clipped_arr = np.clip(clipped_arr, 0, np.inf) - # 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) + 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', + 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, device=device, + channels=1, dtype='int16', latency='low', callback=self.audified_cube.player_callback) self.audified_cube.cbuff = True From 75b7dedc0554179c105b6b2b359711d0b993c8d3 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 11 Oct 2024 16:27:22 -0400 Subject: [PATCH 11/64] Various updates and QOL improvements --- .../plugins/sonify_data/sonify_data.py | 34 +++++++++++++++---- .../plugins/sonify_data/sonify_data.vue | 7 +++- jdaviz/configs/cubeviz/plugins/viewers.py | 2 +- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 343cfabfdd..e57fd60876 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -1,8 +1,11 @@ +import sounddevice as sd + +from echo import delay_callback +from traitlets import Bool, List, Unicode, observe + 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, List, Unicode, observe -import sounddevice as sd +from jdaviz.core.template_mixin import PluginTemplateMixin, DatasetSelectMixin, with_spinner __all__ = ['SonifyData'] @@ -17,12 +20,13 @@ 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) + eln = Bool(False).tag(sync=True) + wavemin = FloatHandleEmpty().tag(sync=True) + wavemax = FloatHandleEmpty().tag(sync=True) audfrqmin = FloatHandleEmpty(50).tag(sync=True) audfrqmax = FloatHandleEmpty(1500).tag(sync=True) pccut = IntHandleEmpty(20).tag(sync=True) + volume = IntHandleEmpty(100).tag(sync=True) sound_devices_items = List().tag(sync=True) sound_devices_selected = Unicode('').tag(sync=True) @@ -32,6 +36,12 @@ def __init__(self, *args, **kwargs): self.sound_devices_items = [device['name'] for device in sd.query_devices()] self.sound_devices_selected = self.sound_devices_items[sd.default.device[1]] + # TODO: Remove hardcoded range viewer + self.spec_viewer = self.app.get_viewer('spectrum-viewer') + self.spec_viewer.state.add_callback("x_min", self._update_x_values) + self.spec_viewer.state.add_callback("x_max", self._update_x_values) + + @with_spinner() def vue_sonify_cube(self, *args): viewer = self.app.get_viewer('flux-viewer') # Get index of selected device since name may not be unique @@ -39,3 +49,15 @@ def vue_sonify_cube(self, *args): viewer.get_sonified_cube(self.sample_rate, self.buffer_size, selected_device_index, self.assidx, self.ssvidx, self.pccut, self.audfrqmin, self.audfrqmax, self.eln) + + # Automatically select spectrum-at-spaxel tool + viewer.toolbar.active_tool = viewer.toolbar.tools['jdaviz:spectrumperspaxel'] + + def _update_x_values(self, event): + with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): + self.wavemin, self.wavemax = self.spec_viewer.state.x_min, self.spec_viewer.state.x_max + + @observe('wavemin', 'wavemax') + def update_viewer_range(self, event): + with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): + self.spec_viewer.state.x_min, self.spec_viewer.state.x_max = self.wavemin, self.wavemax diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index 29e8d2b6cc..0ce88a3ad4 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -122,6 +122,10 @@ persistent-hint > + + Volume + + - Sonify data diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index b853bb655e..3109c38059 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -90,7 +90,7 @@ def data(self, cls=None): isinstance(layer_state.layer, BaseData)] def start_stream(self): - if self.stream: + if self.stream and not self.stream.closed: self.stream.start() def stop_stream(self): From 9d6844d8bf299c442b9c0f1646a245e5482a9bbb Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 15 Oct 2024 10:56:30 -0400 Subject: [PATCH 12/64] Connect volume level in viewer to sonify plugin --- .../cubeviz/plugins/sonify_data/sonify_data.py | 16 +++++++++++----- jdaviz/configs/cubeviz/plugins/viewers.py | 5 +++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index e57fd60876..6402f56e9b 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -38,20 +38,22 @@ def __init__(self, *args, **kwargs): # TODO: Remove hardcoded range viewer self.spec_viewer = self.app.get_viewer('spectrum-viewer') + self.flux_viewer = self.app.get_viewer('flux-viewer') self.spec_viewer.state.add_callback("x_min", self._update_x_values) self.spec_viewer.state.add_callback("x_max", self._update_x_values) @with_spinner() def vue_sonify_cube(self, *args): - viewer = self.app.get_viewer('flux-viewer') # Get index of selected device since name may not be unique selected_device_index = self.sound_devices_items.index(self.sound_devices_selected) - viewer.get_sonified_cube(self.sample_rate, self.buffer_size, selected_device_index, - self.assidx, self.ssvidx, self.pccut, self.audfrqmin, - self.audfrqmax, self.eln) + self.flux_viewer.get_sonified_cube(self.sample_rate, self.buffer_size, + selected_device_index, self.assidx, self.ssvidx, + self.pccut, self.audfrqmin, + self.audfrqmax, self.eln) # Automatically select spectrum-at-spaxel tool - viewer.toolbar.active_tool = viewer.toolbar.tools['jdaviz:spectrumperspaxel'] + spec_at_spaxel_tool = self.flux_viewer.toolbar.tools['jdaviz:spectrumperspaxel'] + self.flux_viewer.toolbar.active_tool = spec_at_spaxel_tool def _update_x_values(self, event): with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): @@ -61,3 +63,7 @@ def _update_x_values(self, event): def update_viewer_range(self, event): with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): self.spec_viewer.state.x_min, self.spec_viewer.state.x_max = self.wavemin, self.wavemax + + @observe('volume') + def update_volume_level(self, event): + self.flux_viewer.update_volume_level = event['new'] diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 3109c38059..64ada670d3 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -47,6 +47,7 @@ def __init__(self, *args, **kwargs): self.stream = None self.audification_wl_bounds = None self.audification_wl_unit = None + self.volume_level = None @property def _default_spectrum_viewer_reference_name(self): @@ -107,6 +108,10 @@ def update_listener_wl_bounds(self, w1,w2): if not self.audified_cube: return self.audified_cube.set_wl_bounds(w1, w2) + + def update_volume_level(self, level): + # TODO: Use volume attribute for sonified cube + self.volume_level = level def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, pccut, audfrqmin, audfrqmax, eln): From 1d1417c796dc9b594ad02d6f9136ff42f14ec147 Mon Sep 17 00:00:00 2001 From: James Trayford Date: Wed, 16 Oct 2024 20:54:48 +0100 Subject: [PATCH 13/64] add volume attenuation functionality --- jdaviz/configs/cubeviz/plugins/cube_listener.py | 9 ++++++++- .../configs/cubeviz/plugins/sonify_data/sonify_data.py | 2 +- jdaviz/configs/cubeviz/plugins/viewers.py | 6 +++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 754c5b4c90..06756490ca 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -55,7 +55,7 @@ def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fm class CubeListenerData: 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): + wl_bounds=None, wl_unit=None, audfrqmin=50, audfrqmax=1500, eln=False, vol=False): self.siglen = int(samplerate*(duration-overlap)) self.cube = cube self.dur = duration @@ -64,6 +64,12 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff self.maxval = pow(2,bdepth-1) - 1 self.fadedx = 0 + if vol: + self.atten_level = int(np.clip(100/vol, 2**15-1)) + else: + self.atten_level=1 + print(vol,self.atten_level) + self.wl_bounds = wl_bounds self.wl_unit = wl_unit self.wlens = wlens @@ -143,3 +149,4 @@ def player_callback(self, outdata, frames, time, status): self.cbuff = False else: outdata[:,0] = self.cursig[dxs] + outdata[:,0] //= self.atten_level diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 6402f56e9b..074aa58af3 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -66,4 +66,4 @@ def update_viewer_range(self, event): @observe('volume') def update_volume_level(self, event): - self.flux_viewer.update_volume_level = event['new'] + self.flux_viewer.update_volume_level(event['new']) diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 64ada670d3..145b350ca2 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -111,7 +111,10 @@ def update_listener_wl_bounds(self, w1,w2): def update_volume_level(self, level): # TODO: Use volume attribute for sonified cube + if not self.audified_cube: + return self.volume_level = level + self.audified_cube.atten_level = int(np.clip((100/level)**2, 0, 2**15-1)) def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, pccut, audfrqmin, audfrqmax, eln): @@ -147,7 +150,7 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, samplerate=sample_rate, buffsize=buffer_size, wl_bounds=self.audification_wl_bounds, wl_unit=self.audification_wl_unit, - audfrqmin=audfrqmin, audfrqmax=audfrqmax) + audfrqmin=audfrqmin, audfrqmax=audfrqmax, vol=self.volume_level) self.audified_cube.audify_cube() self.audified_cube.sigcube = ( self.audified_cube.sigcube * pow(whitelight / whitelight.max(), @@ -155,6 +158,7 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, self.stream = sd.OutputStream(samplerate=sample_rate, blocksize=buffer_size, device=device, channels=1, dtype='int16', latency='low', callback=self.audified_cube.player_callback) + print(sd.query_devices(), device) self.audified_cube.cbuff = True From 6421ee52d30112f91f69274e3cdc001fc42f91fa Mon Sep 17 00:00:00 2001 From: James Trayford Date: Thu, 17 Oct 2024 12:27:42 +0100 Subject: [PATCH 14/64] add sound device switching --- .../configs/cubeviz/plugins/cube_listener.py | 2 +- .../plugins/sonify_data/sonify_data.py | 35 ++++++++++++++++--- jdaviz/configs/cubeviz/plugins/viewers.py | 19 ++++++++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 06756490ca..991686d286 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -65,7 +65,7 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff self.fadedx = 0 if vol: - self.atten_level = int(np.clip(100/vol, 2**15-1)) + self.atten_level = int(np.clip(100/vol,0, 2**15-1)) else: self.atten_level=1 print(vol,self.atten_level) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 074aa58af3..461550abd3 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -28,24 +28,30 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): pccut = IntHandleEmpty(20).tag(sync=True) volume = IntHandleEmpty(100).tag(sync=True) + # TODO: can we referesh the list, so sounddevices are up-to-date when dropdown clicked? sound_devices_items = List().tag(sync=True) sound_devices_selected = Unicode('').tag(sync=True) + sound_device_indexes = {} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.sound_devices_items = [device['name'] for device in sd.query_devices()] - self.sound_devices_selected = self.sound_devices_items[sd.default.device[1]] - + # self.sound_devices_items = [device['name'] for device in sd.query_devices()] + # self.sound_devices_selected = sd.query_devices()[sd.default.device[1]]['name'] + devices, indexes = self.build_device_lists() + self.sound_device_indexes = dict(zip(devices, indexes)) + self.sound_devices_items = devices + self.sound_devices_selected = dict(zip(indexes, devices))[sd.default.device[1]] + # TODO: Remove hardcoded range viewer self.spec_viewer = self.app.get_viewer('spectrum-viewer') self.flux_viewer = self.app.get_viewer('flux-viewer') self.spec_viewer.state.add_callback("x_min", self._update_x_values) self.spec_viewer.state.add_callback("x_max", self._update_x_values) - + @with_spinner() def vue_sonify_cube(self, *args): # Get index of selected device since name may not be unique - selected_device_index = self.sound_devices_items.index(self.sound_devices_selected) + selected_device_index = self.sound_device_indexes[self.sound_devices_selected] #self.sound_devices_items.index(self.sound_devices_selected) self.flux_viewer.get_sonified_cube(self.sample_rate, self.buffer_size, selected_device_index, self.assidx, self.ssvidx, self.pccut, self.audfrqmin, @@ -67,3 +73,22 @@ def update_viewer_range(self, event): @observe('volume') def update_volume_level(self, event): self.flux_viewer.update_volume_level(event['new']) + + @observe('sound_devices_selected') + def update_sound_device(self, event): + if event['new'] != event['old']: + didx = dict(zip(*self.build_device_lists()))[event['new']] + self.flux_viewer.update_sound_device(didx) + + def build_device_lists(self): + # dedicated function to build the current *output* + # device and index lists + devdx = 0 + devices = [] + didxs = [] + for device in sd.query_devices(): + if device['max_output_channels'] > 0: + devices.append(device['name']) + didxs.append(devdx) + devdx += 1 + return devices, didxs diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 145b350ca2..9889ec2cfe 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -109,19 +109,31 @@ def update_listener_wl_bounds(self, w1,w2): return self.audified_cube.set_wl_bounds(w1, w2) + def update_sound_device(self, device_index): + # TODO: Use volume attribute for sonified cube + if not self.audified_cube: + return + + self.stop_stream() + self.stream = sd.OutputStream(samplerate=self.sample_rate, blocksize=self.buffer_size, + device=device_index, channels=1, dtype='int16', + latency='low', callback=self.audified_cube.player_callback) + def update_volume_level(self, level): # TODO: Use volume attribute for sonified cube if not self.audified_cube: return self.volume_level = level self.audified_cube.atten_level = int(np.clip((100/level)**2, 0, 2**15-1)) + def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, pccut, audfrqmin, audfrqmax, eln): - spectrum = self.active_image_layer.layer.get_object(statistic=None) - + spectrum = self.active_image_layer.layer.get_object(statistic=None) wlens = spectrum.wavelength.to('m').value flux = spectrum.flux.value + self.sample_rate = sample_rate + self.buffer_size = buffer_size if self.audification_wl_bounds: wl_unit = getattr(u, self.audification_wl_unit) @@ -150,7 +162,8 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, samplerate=sample_rate, buffsize=buffer_size, wl_bounds=self.audification_wl_bounds, wl_unit=self.audification_wl_unit, - audfrqmin=audfrqmin, audfrqmax=audfrqmax, vol=self.volume_level) + audfrqmin=audfrqmin, audfrqmax=audfrqmax, + vol=self.volume_level,) self.audified_cube.audify_cube() self.audified_cube.sigcube = ( self.audified_cube.sigcube * pow(whitelight / whitelight.max(), From 352eff9c03a071db7b98b0af2c541fc1efc85120 Mon Sep 17 00:00:00 2001 From: James Trayford Date: Thu, 17 Oct 2024 13:48:01 +0100 Subject: [PATCH 15/64] feed ELN flag to Image Viewer --- jdaviz/configs/cubeviz/plugins/viewers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 9889ec2cfe..ef1cae01b2 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -163,7 +163,7 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, wl_bounds=self.audification_wl_bounds, wl_unit=self.audification_wl_unit, audfrqmin=audfrqmin, audfrqmax=audfrqmax, - vol=self.volume_level,) + vol=self.volume_level,eln=eln) self.audified_cube.audify_cube() self.audified_cube.sigcube = ( self.audified_cube.sigcube * pow(whitelight / whitelight.max(), From ceafbb046cc221c0e32c9d725fd8d9cc05d355d3 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 17 Oct 2024 21:02:31 -0400 Subject: [PATCH 16/64] Enable start stop stream and strauss soft dependency --- .../configs/cubeviz/plugins/cube_listener.py | 54 ++++++++++--------- .../plugins/sonify_data/sonify_data.py | 29 ++++++---- .../plugins/sonify_data/sonify_data.vue | 10 ++++ jdaviz/configs/cubeviz/plugins/viewers.py | 8 +-- pyproject.toml | 3 ++ 5 files changed, 64 insertions(+), 40 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 991686d286..6603c7962d 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -12,6 +12,7 @@ # some beginner utility functions for STRAUSS + CubeViz + @contextmanager def suppress_stderr(): with open(os.devnull, "w") as devnull: @@ -21,24 +22,25 @@ def suppress_stderr(): yield finally: sys.stderr = old_stderr - -def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fmin=40, fmax=1300, eln=False): + + +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 + 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]} + data = {'spectrum': [spec], 'pitch': [1]} # again, use maximal range for the mapped parameters - lims = {'spectrum': ('0','100')} + lims = {'spectrum': ('0', '100')} # set up source sources = Events(data.keys()) @@ -53,29 +55,31 @@ def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fm # sd.play(soni.loop_channels['0'].values * 0.5,loop=True) return soni.loop_channels['0'].values + class CubeListenerData: - 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, vol=False): + 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, vol=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.maxval = pow(2, bdepth-1) - 1 self.fadedx = 0 if vol: - self.atten_level = int(np.clip(100/vol,0, 2**15-1)) + self.atten_level = int(np.clip(100/vol, 0, 2**15-1)) else: - self.atten_level=1 - print(vol,self.atten_level) + self.atten_level = 1 + print(vol, self.atten_level) self.wl_bounds = wl_bounds self.wl_unit = wl_unit self.wlens = wlens # control fades - fade = np.linspace(0,1, buffsize+1) + fade = np.linspace(0, 1, buffsize+1) self.ifade = fade[:-1] self.ofade = fade[::-1][:-1] @@ -101,9 +105,9 @@ def set_wl_bounds(self, w1, w2): """ set the wavelength bounds for indexing spectra """ - wsrt = np.sort([w1,w2]) + wsrt = np.sort([w1, w2]) self.wl_bounds = tuple(wsrt) - print(w1,w2,'test') + print(w1, w2, 'test') print(self.wl_bounds) def audify_cube(self): @@ -122,17 +126,17 @@ def audify_cube(self): for i in tqdm(range(self.cube.shape[0])): for j in range(self.cube.shape[1]): with suppress_stderr(): - if self.cube[i,j,lo2hi].any(): - sig = audify_spectrum(self.cube[i,j,lo2hi], self.dur, + 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 + self.sigcube[i, j, :] = sig else: continue - self.cursig[:] = self.sigcube[self.idx1,self.idx2, :] + 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") @@ -143,10 +147,10 @@ def player_callback(self, outdata, frames, time, status): sdx = int(time.outputBufferDacTime*self.srate) dxs = np.arange(sdx, sdx+frames).astype(int) % self.sigcube.shape[-1] if self.cbuff: - outdata[:,0] = (cur[dxs] * self.ofade).astype('int16') - outdata[:,0] += (new[dxs] * self.ifade).astype('int16') + outdata[:, 0] = (cur[dxs] * self.ofade).astype('int16') + outdata[:, 0] += (new[dxs] * self.ifade).astype('int16') self.cursig[:] = self.newsig[:] self.cbuff = False else: - outdata[:,0] = self.cursig[dxs] - outdata[:,0] //= self.atten_level + outdata[:, 0] = self.cursig[dxs] + outdata[:, 0] //= self.atten_level diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 461550abd3..9fef5b015b 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -27,22 +27,23 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): audfrqmax = FloatHandleEmpty(1500).tag(sync=True) pccut = IntHandleEmpty(20).tag(sync=True) volume = IntHandleEmpty(100).tag(sync=True) + stream_active = Bool(True).tag(sync=True) # TODO: can we referesh the list, so sounddevices are up-to-date when dropdown clicked? sound_devices_items = List().tag(sync=True) sound_devices_selected = Unicode('').tag(sync=True) - sound_device_indexes = {} - + # sound_device_indexes = {} + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # self.sound_devices_items = [device['name'] for device in sd.query_devices()] - # self.sound_devices_selected = sd.query_devices()[sd.default.device[1]]['name'] - devices, indexes = self.build_device_lists() - self.sound_device_indexes = dict(zip(devices, indexes)) - self.sound_devices_items = devices - self.sound_devices_selected = dict(zip(indexes, devices))[sd.default.device[1]] - - # TODO: Remove hardcoded range viewer + self.sound_devices_items = [device['name'] for device in sd.query_devices()] + self.sound_devices_selected = sd.query_devices()[sd.default.device[1]]['name'] + # devices, indexes = self.build_device_lists() + # self.sound_device_indexes = dict(zip(devices, indexes)) + # self.sound_devices_items = devices + # self.sound_devices_selected = dict(zip(indexes, devices))[sd.default.device[1]] + + # TODO: Remove hardcoded range and flux viewer self.spec_viewer = self.app.get_viewer('spectrum-viewer') self.flux_viewer = self.app.get_viewer('flux-viewer') self.spec_viewer.state.add_callback("x_min", self._update_x_values) @@ -51,7 +52,9 @@ def __init__(self, *args, **kwargs): @with_spinner() def vue_sonify_cube(self, *args): # Get index of selected device since name may not be unique - selected_device_index = self.sound_device_indexes[self.sound_devices_selected] #self.sound_devices_items.index(self.sound_devices_selected) + # selected_device_index = self.sound_device_indexes[self.sound_devices_selected] # + selected_device_index = self.sound_devices_items.index(self.sound_devices_selected) + print(selected_device_index) self.flux_viewer.get_sonified_cube(self.sample_rate, self.buffer_size, selected_device_index, self.assidx, self.ssvidx, self.pccut, self.audfrqmin, @@ -61,6 +64,10 @@ def vue_sonify_cube(self, *args): spec_at_spaxel_tool = self.flux_viewer.toolbar.tools['jdaviz:spectrumperspaxel'] self.flux_viewer.toolbar.active_tool = spec_at_spaxel_tool + def vue_start_stop_stream(self, *args): + self.stream_active = not self.stream_active + self.flux_viewer.stream_active = not self.flux_viewer.stream_active + def _update_x_values(self, event): with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): self.wavemin, self.wavemax = self.spec_viewer.state.x_min, self.spec_viewer.state.x_max diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index 0ce88a3ad4..b6f0a20beb 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -141,6 +141,16 @@ > Sonify data + + Start stream + + + Stop stream + \ No newline at end of file diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index ef1cae01b2..5510071f84 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -48,6 +48,7 @@ def __init__(self, *args, **kwargs): self.audification_wl_bounds = None self.audification_wl_unit = None self.volume_level = None + self.stream_active = True @property def _default_spectrum_viewer_reference_name(self): @@ -91,11 +92,11 @@ def data(self, cls=None): isinstance(layer_state.layer, BaseData)] def start_stream(self): - if self.stream and not self.stream.closed: + if self.stream and not self.stream.closed and self.stream_active: self.stream.start() def stop_stream(self): - if self.stream: + if self.stream and not self.stream.closed and self.stream_active: self.stream.stop() def update_cube(self, x, y): @@ -126,7 +127,6 @@ def update_volume_level(self, level): self.volume_level = level self.audified_cube.atten_level = int(np.clip((100/level)**2, 0, 2**15-1)) - def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, pccut, audfrqmin, audfrqmax, eln): spectrum = self.active_image_layer.layer.get_object(statistic=None) @@ -163,7 +163,7 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, wl_bounds=self.audification_wl_bounds, wl_unit=self.audification_wl_unit, audfrqmin=audfrqmin, audfrqmax=audfrqmax, - vol=self.volume_level,eln=eln) + eln=eln, vol=self.volume_level) self.audified_cube.audify_cube() self.audified_cube.sigcube = ( self.audified_cube.sigcube * pow(whitelight / whitelight.max(), diff --git a/pyproject.toml b/pyproject.toml index 3897a25e7a..4c548eebb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,9 @@ docs = [ roman = [ "roman_datamodels>=0.17.1", ] +strauss = [ + "strauss" +] [build-system] requires = [ From a4811e494cb066ec6006151624b77895a28aa071 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 23 Oct 2024 14:54:29 -0400 Subject: [PATCH 17/64] Add note to plugin when strauss is not downloaded --- jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py | 8 ++++++++ .../configs/cubeviz/plugins/sonify_data/sonify_data.vue | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 9fef5b015b..245fb9c556 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -10,6 +10,13 @@ __all__ = ['SonifyData'] +try: + import strauss +except ImportError: + _has_strauss = False +else: + _has_strauss = True + @tray_registry('cubeviz-sonify-data', label="Sonify Data", viewer_requirements=['spectrum', 'image']) @@ -28,6 +35,7 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): pccut = IntHandleEmpty(20).tag(sync=True) volume = IntHandleEmpty(100).tag(sync=True) stream_active = Bool(True).tag(sync=True) + has_strauss = Bool(_has_strauss).tag(sync=True) # TODO: can we referesh the list, so sounddevices are up-to-date when dropdown clicked? sound_devices_items = List().tag(sync=True) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index b6f0a20beb..bbbd94ed9f 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -8,7 +8,10 @@ :popout_button="popout_button" :scroll_to.sync="scroll_to"> - Cube + Sonify Cube + + To use Sonify Data, install strauss and restart jdaviz. + Choose the input cube and spectral subset. From 2a270ecc8ae7d973dfe846c4496e30343f775851 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 25 Oct 2024 10:03:24 -0400 Subject: [PATCH 18/64] Add strauss as soft dependency --- .../configs/cubeviz/plugins/cube_listener.py | 19 ++++++++++++------- .../plugins/sonify_data/sonify_data.py | 8 ++++---- .../plugins/sonify_data/sonify_data.vue | 1 + jdaviz/configs/cubeviz/plugins/tools.py | 1 - jdaviz/configs/cubeviz/plugins/viewers.py | 8 +++++--- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 6603c7962d..d4221a5c0f 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -1,15 +1,20 @@ -from strauss.sonification import Sonification -from strauss.sources import Events, Objects -from strauss import channels -from strauss.score import Score -from strauss.generator import Spectralizer import numpy as np -from tqdm import tqdm from contextlib import contextmanager -import sys, os +import sys +import os import time from astropy import units as u +try: + from strauss.sonification import Sonification + from strauss.sources import Events, Objects + from strauss import channels + from strauss.score import Score + from strauss.generator import Spectralizer + from tqdm import tqdm +except ImportError: + pass + # some beginner utility functions for STRAUSS + CubeViz diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 245fb9c556..aaaa2d4c0e 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -1,5 +1,3 @@ -import sounddevice as sd - from echo import delay_callback from traitlets import Bool, List, Unicode, observe @@ -12,6 +10,7 @@ try: import strauss + import sounddevice as sd except ImportError: _has_strauss = False else: @@ -44,8 +43,9 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.sound_devices_items = [device['name'] for device in sd.query_devices()] - self.sound_devices_selected = sd.query_devices()[sd.default.device[1]]['name'] + if self.has_strauss: + self.sound_devices_items = [device['name'] for device in sd.query_devices()] + self.sound_devices_selected = sd.query_devices()[sd.default.device[1]]['name'] # devices, indexes = self.build_device_lists() # self.sound_device_indexes = dict(zip(devices, indexes)) # self.sound_devices_items = devices diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index bbbd94ed9f..1053338445 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -139,6 +139,7 @@ diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 38250a6434..5663503183 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -118,7 +118,6 @@ def activate(self): 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() diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 5510071f84..69990e20b0 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -8,9 +8,13 @@ from jdaviz.core.freezable_state import FreezableBqplotImageViewerState from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData import numpy as np -import sounddevice as sd from astropy import units as u +try: + import sounddevice as sd +except ImportError: + pass + __all__ = ['CubevizImageView', 'CubevizProfileView'] @viewer_registry("cubeviz-image-viewer", label="Image 2D (Cubeviz)") @@ -111,7 +115,6 @@ def update_listener_wl_bounds(self, w1,w2): self.audified_cube.set_wl_bounds(w1, w2) def update_sound_device(self, device_index): - # TODO: Use volume attribute for sonified cube if not self.audified_cube: return @@ -121,7 +124,6 @@ def update_sound_device(self, device_index): latency='low', callback=self.audified_cube.player_callback) def update_volume_level(self, level): - # TODO: Use volume attribute for sonified cube if not self.audified_cube: return self.volume_level = level From 13f37a8e724354c0ed9912c8aea08b5b903b2fcc Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 31 Oct 2024 15:44:07 -0400 Subject: [PATCH 19/64] Get build devices method working on windows --- .../configs/cubeviz/plugins/cube_listener.py | 6 +--- .../plugins/sonify_data/sonify_data.py | 29 +++++++------------ jdaviz/configs/cubeviz/plugins/viewers.py | 2 -- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index d4221a5c0f..0b6d7e183a 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -3,7 +3,6 @@ import sys import os import time -from astropy import units as u try: from strauss.sonification import Sonification @@ -77,8 +76,7 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff self.atten_level = int(np.clip(100/vol, 0, 2**15-1)) else: self.atten_level = 1 - print(vol, self.atten_level) - + self.wl_bounds = wl_bounds self.wl_unit = wl_unit self.wlens = wlens @@ -112,8 +110,6 @@ def set_wl_bounds(self, w1, w2): """ wsrt = np.sort([w1, w2]) self.wl_bounds = tuple(wsrt) - print(w1, w2, 'test') - print(self.wl_bounds) def audify_cube(self): """ diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index aaaa2d4c0e..4481035437 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -39,17 +39,14 @@ class SonifyData(PluginTemplateMixin, DatasetSelectMixin): # TODO: can we referesh the list, so sounddevices are up-to-date when dropdown clicked? sound_devices_items = List().tag(sync=True) sound_devices_selected = Unicode('').tag(sync=True) - # sound_device_indexes = {} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.has_strauss: - self.sound_devices_items = [device['name'] for device in sd.query_devices()] - self.sound_devices_selected = sd.query_devices()[sd.default.device[1]]['name'] - # devices, indexes = self.build_device_lists() - # self.sound_device_indexes = dict(zip(devices, indexes)) - # self.sound_devices_items = devices - # self.sound_devices_selected = dict(zip(indexes, devices))[sd.default.device[1]] + devices, indexes = self.build_device_lists() + self.sound_device_indexes = dict(zip(devices, indexes)) + self.sound_devices_items = devices + self.sound_devices_selected = dict(zip(indexes, devices))[sd.default.device[1]] # TODO: Remove hardcoded range and flux viewer self.spec_viewer = self.app.get_viewer('spectrum-viewer') @@ -59,10 +56,8 @@ def __init__(self, *args, **kwargs): @with_spinner() def vue_sonify_cube(self, *args): - # Get index of selected device since name may not be unique - # selected_device_index = self.sound_device_indexes[self.sound_devices_selected] # - selected_device_index = self.sound_devices_items.index(self.sound_devices_selected) - print(selected_device_index) + # Get index of selected device + selected_device_index = self.sound_device_indexes[self.sound_devices_selected] self.flux_viewer.get_sonified_cube(self.sample_rate, self.buffer_size, selected_device_index, self.assidx, self.ssvidx, self.pccut, self.audfrqmin, @@ -98,12 +93,10 @@ def update_sound_device(self, event): def build_device_lists(self): # dedicated function to build the current *output* # device and index lists - devdx = 0 devices = [] - didxs = [] - for device in sd.query_devices(): - if device['max_output_channels'] > 0: + device_indexes = [] + for index, device in enumerate(sd.query_devices()): + if device['max_output_channels'] > 0 and device['name'] not in devices: devices.append(device['name']) - didxs.append(devdx) - devdx += 1 - return devices, didxs + device_indexes.append(index) + return devices, device_indexes diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 69990e20b0..c75bafa390 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -159,7 +159,6 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, # and re-clip clipped_arr = np.clip(clipped_arr, 0, np.inf) - 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, @@ -173,7 +172,6 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, self.stream = sd.OutputStream(samplerate=sample_rate, blocksize=buffer_size, device=device, channels=1, dtype='int16', latency='low', callback=self.audified_cube.player_callback) - print(sd.query_devices(), device) self.audified_cube.cbuff = True From 4a1d6281fecba521017f51604c44a52d2478660c Mon Sep 17 00:00:00 2001 From: James Trayford Date: Fri, 25 Oct 2024 10:28:26 -0400 Subject: [PATCH 20/64] ensure sound generation always uses the current spectrum-at-spaxel wl range --- jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py | 5 ++++- jdaviz/configs/cubeviz/plugins/tools.py | 1 + jdaviz/configs/cubeviz/plugins/viewers.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index 4481035437..a1bcef6758 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -78,8 +78,11 @@ def _update_x_values(self, event): @observe('wavemin', 'wavemax') def update_viewer_range(self, event): with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): + # print(event.name, event.new, self.wavemin) self.spec_viewer.state.x_min, self.spec_viewer.state.x_max = self.wavemin, self.wavemax - + self.flux_viewer.update_listener_wls(self.wavemin, self.wavemax, self.spec_viewer.state.x_display_unit) + + @observe('volume') def update_volume_level(self, event): self.flux_viewer.update_volume_level(event['new']) diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 5663503183..1803060bb9 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -117,6 +117,7 @@ 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] + print(self._previous_bounds) # update listener bounds self.viewer.audification_wl_bounds = (sv_state.x_min, sv_state.x_max) self.viewer.audification_wl_unit = sv_state.x_display_unit diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index c75bafa390..40ca0e4b52 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -114,6 +114,12 @@ def update_listener_wl_bounds(self, w1,w2): return self.audified_cube.set_wl_bounds(w1, w2) + def update_listener_wls(self, w1,w2, wunit): + if not self.audified_cube: + return + self.audification_wl_bounds = (w1, w2) + self.audification_wl_unit = wunit + def update_sound_device(self, device_index): if not self.audified_cube: return From a05937d08406a14aecf0bb781eb2a5f5f43391b6 Mon Sep 17 00:00:00 2001 From: James Trayford Date: Sun, 3 Nov 2024 10:29:06 -0500 Subject: [PATCH 21/64] post rebase clean-up (remove prints and rogue spaces) --- jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py | 4 +--- jdaviz/configs/cubeviz/plugins/tools.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index a1bcef6758..e23d91d724 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -78,11 +78,9 @@ def _update_x_values(self, event): @observe('wavemin', 'wavemax') def update_viewer_range(self, event): with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): - # print(event.name, event.new, self.wavemin) self.spec_viewer.state.x_min, self.spec_viewer.state.x_max = self.wavemin, self.wavemax self.flux_viewer.update_listener_wls(self.wavemin, self.wavemax, self.spec_viewer.state.x_display_unit) - - + @observe('volume') def update_volume_level(self, event): self.flux_viewer.update_volume_level(event['new']) diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 1803060bb9..5663503183 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -117,7 +117,6 @@ 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] - print(self._previous_bounds) # update listener bounds self.viewer.audification_wl_bounds = (sv_state.x_min, sv_state.x_max) self.viewer.audification_wl_unit = sv_state.x_display_unit From 0f0eb87b5476528178718b99f3483e4f1e7c1bd5 Mon Sep 17 00:00:00 2001 From: James Trayford Date: Sun, 3 Nov 2024 17:30:57 -0500 Subject: [PATCH 22/64] this syntax seems to work to install strauss on our specific git branch for now, while awaitin PyPI release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4c548eebb0..d080b534a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ roman = [ "roman_datamodels>=0.17.1", ] strauss = [ - "strauss" + "strauss@git+https://github.com/james-trayford/strauss@equal_loudness_normalisation" ] [build-system] From 18634ea3e568fa18749a0954d8b75a01e52e4c02 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 6 Nov 2024 13:43:04 -0500 Subject: [PATCH 23/64] Update code to be PEP8 --- .../configs/cubeviz/plugins/cube_listener.py | 24 +++++++++---------- .../plugins/sonify_data/sonify_data.py | 5 ++-- jdaviz/configs/cubeviz/plugins/tools.py | 2 +- jdaviz/configs/cubeviz/plugins/viewers.py | 18 +++++++------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 0b6d7e183a..66cdb77856 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -22,7 +22,7 @@ def suppress_stderr(): with open(os.devnull, "w") as devnull: old_stderr = sys.stderr sys.stderr = devnull - try: + try: yield finally: sys.stderr = old_stderr @@ -42,15 +42,15 @@ def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fm 'equal_loudness_normalisation': eln}) data = {'spectrum': [spec], 'pitch': [1]} - + # again, use maximal range for the mapped parameters lims = {'spectrum': ('0', '100')} - + # set up source sources = Events(data.keys()) sources.fromdict(data) sources.apply_mapping_functions(map_lims=lims) - + # render and play sonification! soni = Sonification(score, sources, generator, system, samprate=srate) soni.render() @@ -80,7 +80,7 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff 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] @@ -92,16 +92,16 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff # do we normalise for equal loudness? self.eln = eln - + self.idx1 = 0 self.idx2 = 0 self.cbuff = False self.cursig = np.zeros(self.siglen, dtype='int16') self.newsig = np.zeros(self.siglen, dtype='int16') - - if self.cursig.nbytes * pow(1024,-3) > 2: + + if self.cursig.nbytes * pow(1024, -3) > 2: 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): @@ -110,7 +110,7 @@ def set_wl_bounds(self, w1, w2): """ wsrt = np.sort([w1, w2]) self.wl_bounds = tuple(wsrt) - + def audify_cube(self): """ Iterate through the cube, convert each spectrum to a signal, and store @@ -129,7 +129,7 @@ def audify_cube(self): with suppress_stderr(): if self.cube[i, j, lo2hi].any(): sig = audify_spectrum(self.cube[i, j, lo2hi], self.dur, - srate=self.srate, + srate=self.srate, fmin=self.audfrqmin, fmax=self.audfrqmax, eln=self.eln) @@ -146,7 +146,7 @@ def player_callback(self, outdata, frames, time, status): cur = self.cursig new = self.newsig sdx = int(time.outputBufferDacTime*self.srate) - dxs = np.arange(sdx, sdx+frames).astype(int) % self.sigcube.shape[-1] + dxs = np.arange(sdx, sdx+frames).astype(int) % self.sigcube.shape[-1] if self.cbuff: outdata[:, 0] = (cur[dxs] * self.ofade).astype('int16') outdata[:, 0] += (new[dxs] * self.ifade).astype('int16') diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index e23d91d724..db4936ba45 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -53,7 +53,7 @@ def __init__(self, *args, **kwargs): self.flux_viewer = self.app.get_viewer('flux-viewer') self.spec_viewer.state.add_callback("x_min", self._update_x_values) self.spec_viewer.state.add_callback("x_max", self._update_x_values) - + @with_spinner() def vue_sonify_cube(self, *args): # Get index of selected device @@ -79,7 +79,8 @@ def _update_x_values(self, event): def update_viewer_range(self, event): with delay_callback(self.spec_viewer.state, 'x_min', 'x_max'): self.spec_viewer.state.x_min, self.spec_viewer.state.x_max = self.wavemin, self.wavemax - self.flux_viewer.update_listener_wls(self.wavemin, self.wavemax, self.spec_viewer.state.x_display_unit) + self.flux_viewer.update_listener_wls(self.wavemin, self.wavemax, + self.spec_viewer.state.x_display_unit) @observe('volume') def update_volume_level(self, event): diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 5663503183..5c3b8cf1a2 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -142,7 +142,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 diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 40ca0e4b52..dafefc2bf0 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -17,6 +17,7 @@ __all__ = ['CubevizImageView', 'CubevizProfileView'] + @viewer_registry("cubeviz-image-viewer", label="Image 2D (Cubeviz)") class CubevizImageView(JdavizViewerMixin, WithSliceSelection, BqplotImageView): # categories: zoom resets, (zoom, pan), subset, select tools, shortcuts @@ -37,7 +38,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 @@ -53,7 +54,7 @@ def __init__(self, *args, **kwargs): self.audification_wl_unit = None self.volume_level = None self.stream_active = True - + @property def _default_spectrum_viewer_reference_name(self): return self.jdaviz_helper._default_spectrum_viewer_reference_name @@ -104,17 +105,18 @@ def stop_stream(self): self.stream.stop() def update_cube(self, x, y): - if not self.audified_cube or not hasattr(self.audified_cube, 'newsig') or not hasattr(self.audified_cube, 'sigcube'): + if (not self.audified_cube or not hasattr(self.audified_cube, 'newsig') + or not hasattr(self.audified_cube, 'sigcube')): return self.audified_cube.newsig = self.audified_cube.sigcube[x, y, :] self.audified_cube.cbuff = True - def update_listener_wl_bounds(self, w1,w2): + def update_listener_wl_bounds(self, w1, w2): if not self.audified_cube: return self.audified_cube.set_wl_bounds(w1, w2) - def update_listener_wls(self, w1,w2, wunit): + def update_listener_wls(self, w1, w2, wunit): if not self.audified_cube: return self.audification_wl_bounds = (w1, w2) @@ -137,12 +139,12 @@ def update_volume_level(self, level): def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, pccut, audfrqmin, audfrqmax, eln): - spectrum = self.active_image_layer.layer.get_object(statistic=None) + spectrum = self.active_image_layer.layer.get_object(statistic=None) wlens = spectrum.wavelength.to('m').value flux = spectrum.flux.value self.sample_rate = sample_rate self.buffer_size = buffer_size - + if self.audification_wl_bounds: wl_unit = getattr(u, self.audification_wl_unit) si_wl_bounds = (self.audification_wl_bounds * wl_unit).to('m') @@ -150,7 +152,7 @@ def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, 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 From cc51183580734b4700355afbbfea11df5e42441a Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 7 Nov 2024 10:07:40 -0500 Subject: [PATCH 24/64] PEP8 fixes --- jdaviz/configs/cubeviz/plugins/cube_listener.py | 3 +-- jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 66cdb77856..72c24b86ff 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -6,8 +6,7 @@ try: from strauss.sonification import Sonification - from strauss.sources import Events, Objects - from strauss import channels + from strauss.sources import Events from strauss.score import Score from strauss.generator import Spectralizer from tqdm import tqdm diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index db4936ba45..5347d672dc 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -9,7 +9,7 @@ __all__ = ['SonifyData'] try: - import strauss + import strauss # noqa import sounddevice as sd except ImportError: _has_strauss = False From c6de0137965e0dbaf5464f0c5b90b324e7b2d5bb Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 7 Nov 2024 10:42:48 -0500 Subject: [PATCH 25/64] Remove old code --- jdaviz/configs/cubeviz/plugins/cube_listener.py | 12 ++---------- jdaviz/configs/cubeviz/plugins/tools.py | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 72c24b86ff..456b8eacaa 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -13,8 +13,6 @@ except ImportError: pass -# some beginner utility functions for STRAUSS + CubeViz - @contextmanager def suppress_stderr(): @@ -54,8 +52,7 @@ def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fm soni = Sonification(score, sources, generator, system, samprate=srate) soni.render() soni._make_seamless(overlap) - # print(soni.loop_channels) - # sd.play(soni.loop_channels['0'].values * 0.5,loop=True) + return soni.loop_channels['0'].values @@ -116,12 +113,7 @@ def audify_cube(self): 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]): diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index a2f5c9d6c5..2b8694da54 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -97,7 +97,7 @@ def __init__(self, *args, **kwargs): self._is_moving = False def activate(self): - sv_state = self._spectrum_viewer.state + sv_state = self._profile_viewer.state self._previous_bounds = [sv_state.x_min, sv_state.x_max, sv_state.y_min, sv_state.y_max] # update listener bounds self.viewer.audification_wl_bounds = (sv_state.x_min, sv_state.x_max) @@ -111,7 +111,7 @@ def deactivate(self): self._profile_viewer.state.remove_callback(k, self.on_limits_change) self.viewer.remove_event_callback(self.on_mouse_move) - self._reset_spectrum_viewer_bounds() + self._reset_profile_viewer_bounds() self.viewer.stop_stream() super().deactivate() From eaae60f1bc73d62929a9eac543775faa7044034f Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 7 Nov 2024 13:24:13 -0500 Subject: [PATCH 26/64] Fix test --- jdaviz/configs/cubeviz/plugins/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 2b8694da54..1c886f6e8d 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -97,14 +97,14 @@ def __init__(self, *args, **kwargs): self._is_moving = False def activate(self): + super().activate() + for k in ("y_min", "y_max"): + self._profile_viewer.state.add_callback(k, self.on_limits_change) sv_state = self._profile_viewer.state self._previous_bounds = [sv_state.x_min, sv_state.x_max, sv_state.y_min, sv_state.y_max] # update listener bounds 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() - for k in ("y_min", "y_max"): - self._profile_viewer.state.add_callback(k, self.on_limits_change) def deactivate(self): for k in ("y_min", "y_max"): From 9682321964d8d99ea187394e1a629b7f8293a40d Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 7 Nov 2024 13:35:14 -0500 Subject: [PATCH 27/64] Fix test 2 --- jdaviz/configs/cubeviz/plugins/tools.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 1c886f6e8d..8acf5beee3 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -109,8 +109,6 @@ def activate(self): def deactivate(self): for k in ("y_min", "y_max"): self._profile_viewer.state.remove_callback(k, self.on_limits_change) - - self.viewer.remove_event_callback(self.on_mouse_move) self._reset_profile_viewer_bounds() self.viewer.stop_stream() super().deactivate() From 630be77c571d531e7a4beee29821b07bf5a9d6ac Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 7 Nov 2024 14:14:00 -0500 Subject: [PATCH 28/64] Update docs link in plugin --- jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index 1053338445..79ddd68a66 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -1,7 +1,7 @@ \ No newline at end of file From 15a867189902a7c6cb080ce7f80602ffe2fe5795 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 8 Nov 2024 15:32:41 -0500 Subject: [PATCH 32/64] Fix code style --- jdaviz/configs/cubeviz/plugins/cube_listener.py | 8 +++++--- jdaviz/configs/cubeviz/plugins/viewers.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py index 5677e7958d..8f6a4a140e 100644 --- a/jdaviz/configs/cubeviz/plugins/cube_listener.py +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -15,6 +15,7 @@ MINVOL = 1/(2**15 - 1) + @contextmanager def suppress_stderr(): with open(os.devnull, "w") as devnull: @@ -25,6 +26,7 @@ def suppress_stderr(): finally: sys.stderr = old_stderr + def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fmin=40, fmax=1300, eln=False): notes = [["A2"]] @@ -59,7 +61,7 @@ def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fm class CubeListenerData: 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, vol=False): + eln=False, vol=None): self.siglen = int(samplerate*(duration-overlap)) self.cube = cube self.dur = duration @@ -68,10 +70,10 @@ def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buff self.maxval = pow(2, bdepth-1) - 1 self.fadedx = 0 - if vol == None: + if vol is None: self.atten_level = 1 else: - self.atten_level = int(np.clip((level/100)**2, MINVOL, 1)) + self.atten_level = int(np.clip((vol/100)**2, MINVOL, 1)) self.wl_bounds = wl_bounds self.wl_unit = wl_unit diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 0ab30bab3b..4201b05c32 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -107,8 +107,8 @@ def stop_stream(self): self.stream.stop() def update_cube(self, x, y): - if (not self.audified_cube or not hasattr(self.audified_cube, 'newsig') - or not hasattr(self.audified_cube, 'sigcube')): + if (not self.audified_cube or not hasattr(self.audified_cube, 'newsig') or + not hasattr(self.audified_cube, 'sigcube')): return self.audified_cube.newsig = self.audified_cube.sigcube[x, y, :] self.audified_cube.cbuff = True @@ -131,7 +131,7 @@ def update_volume_level(self, level): return self.volume_level = level self.audified_cube.atten_level = int(1/np.clip((level/100.)**2, MINVOL, 1)) - + def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, pccut, audfrqmin, audfrqmax, eln): spectrum = self.active_image_layer.layer.get_object(statistic=None) From a5ef38954f0cea3448b215c640eb2a43daa95f46 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 8 Nov 2024 15:49:29 -0500 Subject: [PATCH 33/64] Remove unused import --- jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py index e196b1cdb8..cd44fbb980 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -1,4 +1,3 @@ -from echo import delay_callback from traitlets import Bool, List, Unicode, observe import astropy.units as u From a66beef5c6dae8bc7cea4291dfcec808ac67d409 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 14 Nov 2024 10:27:52 -0500 Subject: [PATCH 34/64] Add documentation and a test --- docs/cubeviz/plugins.rst | 12 ++++++++++++ .../cubeviz/plugins/sonify_data/sonify_data.vue | 12 ++---------- .../cubeviz/plugins/sonify_data/tests/__init__.py | 0 .../plugins/sonify_data/tests/test_sonify_data.py | 5 +++++ 4 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 jdaviz/configs/cubeviz/plugins/sonify_data/tests/__init__.py create mode 100644 jdaviz/configs/cubeviz/plugins/sonify_data/tests/test_sonify_data.py diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 55fa3c0276..5d9f19b8a7 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -335,6 +335,18 @@ have valid flux units. For 3D data, the current :ref:`slice` is used. :ref:`Imviz Aperture Photometry ` Imviz documentation describing the concept of aperture photometry in Jdaviz. +.. _cubeviz-sonify-data: + +Sonify Data +=========== + +This plugin uses the Strauss package to turn data cubes into audio grids (by pressing the +:guilabel:`Sonify Data` button) that can be played while the spectrum-at-spaxel tool is active +and the mouse is hovering over the flux viewer. A range of the cube can be sonified by creating +and selecting a spectral subset from the :guilabel:`Spectral range` dropdown and then pressing +the :guilabel:`Sonify Data` button. The output device for sound can be changed by using the +:guilabel:`Sound device` dropdown. + .. _cubeviz-export-plot: Export diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue index 25bdaa6627..9927b1f6e1 100644 --- a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -1,7 +1,7 @@