Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sonify plugin updates #3269

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6049d9a
Create audified cube and use with spectrum at spaxel tool
javerbukh Jul 9, 2024
627a34f
Add Sonify Data plugin and connect to spectrum per spaxel tool
javerbukh Jul 10, 2024
9e98b32
Fix errors
javerbukh Jul 10, 2024
e6d52b4
Try moving code to mixin
javerbukh Jul 10, 2024
0080b2f
Move code to viewers.py
javerbukh Jul 10, 2024
4b01304
Remove print statements
javerbukh Jul 10, 2024
505f73f
Patches:
Jul 11, 2024
89d6fff
fix np import
Jul 31, 2024
5c67736
add audio frequency range choice and equal loudness normalisation opt…
Aug 15, 2024
d8937c5
Create dropdown to select output sound device
javerbukh Oct 9, 2024
75b7ded
Various updates and QOL improvements
javerbukh Oct 11, 2024
9d6844d
Connect volume level in viewer to sonify plugin
javerbukh Oct 15, 2024
1d1417c
add volume attenuation functionality
Oct 16, 2024
6421ee5
add sound device switching
Oct 17, 2024
352eff9
feed ELN flag to Image Viewer
Oct 17, 2024
a7d501e
Merge pull request #18 from james-trayford/sonify-plugin-updates-jt
javerbukh Oct 17, 2024
ceafbb0
Enable start stop stream and strauss soft dependency
javerbukh Oct 18, 2024
a4811e4
Add note to plugin when strauss is not downloaded
javerbukh Oct 23, 2024
2a270ec
Add strauss as soft dependency
javerbukh Oct 25, 2024
13f37a8
Get build devices method working on windows
javerbukh Oct 31, 2024
4a1d628
ensure sound generation always uses the current spectrum-at-spaxel wl…
Oct 25, 2024
a05937d
post rebase clean-up (remove prints and rogue spaces)
Nov 3, 2024
0f0eb87
this syntax seems to work to install strauss on our specific git bran…
Nov 3, 2024
a56b26e
Merge pull request #19 from james-trayford/sonify-plugin-updates-jt
javerbukh Nov 6, 2024
18634ea
Update code to be PEP8
javerbukh Nov 6, 2024
cc51183
PEP8 fixes
javerbukh Nov 7, 2024
5d308ea
Merge branch 'main' into sonify-plugin-updates
javerbukh Nov 7, 2024
c6de013
Remove old code
javerbukh Nov 7, 2024
eaae60f
Fix test
javerbukh Nov 7, 2024
9682321
Fix test 2
javerbukh Nov 7, 2024
630be77
Update docs link in plugin
javerbukh Nov 7, 2024
51a4154
Use spectral subset for range and move advanced options to accordion
javerbukh Nov 8, 2024
cd1963e
fix volume bug (doesn't crash on vol=0)
Nov 8, 2024
de530d9
Rearrange order in plugin
javerbukh Nov 8, 2024
f3e65df
Merge pull request #20 from james-trayford/sonify-plugin-updates-jt
javerbukh Nov 8, 2024
15a8671
Fix code style
javerbukh Nov 8, 2024
a5ef389
Remove unused import
javerbukh Nov 8, 2024
a66beef
Add documentation and a test
javerbukh Nov 14, 2024
b4b64e0
Add install instructions to warning when package is not present
javerbukh Nov 14, 2024
99d04e8
Fix test failure
javerbukh Nov 14, 2024
9747650
Add plugin description
javerbukh Nov 14, 2024
b1d5806
Add plugin description
javerbukh Nov 15, 2024
2d0c5a4
Finish test and add standalone hook
javerbukh Nov 15, 2024
ae98601
Grey out start/stop stream and fix code style
javerbukh Nov 18, 2024
91ed211
Update docs and change disable message for plugin without strauss ins…
javerbukh Dec 3, 2024
30ddca8
Update jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py
javerbukh Dec 5, 2024
1ab90f6
Merge branch 'main' into sonify-plugin-updates
javerbukh Dec 5, 2024
434a42a
Add cron job for strauss
javerbukh Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/cubeviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,18 @@ have valid flux units. For 3D data, the current :ref:`slice` is used.
:ref:`Imviz Aperture Photometry <aper-phot-simple>`
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
javerbukh marked this conversation as resolved.
Show resolved Hide resolved
: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
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/cubeviz/cubeviz.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tray:
- specviz-line-analysis
- cubeviz-moment-maps
- imviz-aper-phot-simple
- cubeviz-sonify-data
- export
- about
viewer_area:
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/cubeviz/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
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
from .tools import * # noqa
150 changes: 150 additions & 0 deletions jdaviz/configs/cubeviz/plugins/cube_listener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import numpy as np
from contextlib import contextmanager
import sys
import os
import time

try:
from strauss.sonification import Sonification
from strauss.sources import Events
from strauss.score import Score
from strauss.generator import Spectralizer
from tqdm import tqdm

Check warning on line 12 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L9-L12

Added lines #L9 - L12 were not covered by tests
except ImportError:
pass
Comment on lines +7 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible for any of these imports to fail but there still be a call to any of the logic below (either manual API calls or if none of the imports in the plugin itself fail and so _has_strauss = True but tqdm failed to import, for example)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the methods are public API and tqdm is a dependency of Strauss, but I can see that being an issue down the road. Is it alright for this to be a follow-up?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can either put the import tqdm in the else or import _has_strauss from the plugin. But yes, probably can be a follow-up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we decide to move most of the cube_listener.py code to the sonify plugin then we can include this effort in #3330 .


MINVOL = 1/(2**15 - 1)


@contextmanager
def suppress_stderr():
with open(os.devnull, "w") as devnull:
old_stderr = sys.stderr
sys.stderr = devnull
try:
yield

Check warning on line 25 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L21-L25

Added lines #L21 - L25 were not covered by tests
finally:
sys.stderr = old_stderr

Check warning on line 27 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L27

Added line #L27 was not covered by tests


def audify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fmin=40, fmax=1300,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole function is never reached by tests - is it possible to add coverage (or if out of scope, can we create a follow-up)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on closer look, it seems that the existing tests added in this PR are probably never actually running on CI because of dependencies. Can we add strauss to at least one of the runners so they do run?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I add the Strauss package to all in pyproject.toml or should I create a new workflow for Strauss?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's discuss - it might be time to make a convention for this and do the same thing for footprints, Roman, and strauss.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any resolution to the test coverage situation?

eln=False):
notes = [["A2"]]
score = Score(notes, duration)

Check warning on line 33 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L32-L33

Added lines #L32 - L33 were not covered by tests
# set up spectralizer generator
generator = Spectralizer(samprate=srate)

Check warning on line 35 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L35

Added line #L35 was not covered by tests

# Lets pick the mapping frequency range for the spectrum...
generator.modify_preset({'min_freq': fmin, 'max_freq': fmax,

Check warning on line 38 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L38

Added line #L38 was not covered by tests
'fit_spec_multiples': False,
'interpolation_type': 'preserve_power',
'equal_loudness_normalisation': eln})

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

Check warning on line 43 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L43

Added line #L43 was not covered by tests

# again, use maximal range for the mapped parameters
lims = {'spectrum': ('0', '100')}

Check warning on line 46 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L46

Added line #L46 was not covered by tests

# set up source
sources = Events(data.keys())
sources.fromdict(data)
sources.apply_mapping_functions(map_lims=lims)

Check warning on line 51 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L49-L51

Added lines #L49 - L51 were not covered by tests

# render and play sonification!
soni = Sonification(score, sources, generator, system, samprate=srate)
soni.render()
soni._make_seamless(overlap)

Check warning on line 56 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L54-L56

Added lines #L54 - L56 were not covered by tests

return soni.loop_channels['0'].values

Check warning on line 58 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L58

Added line #L58 was not covered by tests


class CubeListenerData:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above re test coverage

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=None):
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

Check warning on line 71 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L65-L71

Added lines #L65 - L71 were not covered by tests

if vol is None:
self.atten_level = 1

Check warning on line 74 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L73-L74

Added lines #L73 - L74 were not covered by tests
else:
self.atten_level = int(np.clip((vol/100)**2, MINVOL, 1))

Check warning on line 76 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L76

Added line #L76 was not covered by tests

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

Check warning on line 80 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L78-L80

Added lines #L78 - L80 were not covered by tests

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

Check warning on line 85 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L83-L85

Added lines #L83 - L85 were not covered by tests

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

Check warning on line 89 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L88-L89

Added lines #L88 - L89 were not covered by tests

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

Check warning on line 92 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L92

Added line #L92 was not covered by tests

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')

Check warning on line 98 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L94-L98

Added lines #L94 - L98 were not covered by tests

if self.cursig.nbytes * pow(1024, -3) > 2:
raise Exception("Cube projected to be > 2Gb!")

Check warning on line 101 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L100-L101

Added lines #L100 - L101 were not covered by tests

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

Check warning on line 103 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L103

Added line #L103 was not covered by tests

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

Check warning on line 110 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L109-L110

Added lines #L109 - L110 were not covered by tests

def audify_cube(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that audify_spectrum is outside the class as an independent function, and this pretty much just uses self.cube from self, I wonder if this could/should be pulled out into a function that takes cube as input and returns what's needed to the class 🤔. Probably not important, it just seemed a little wonky to have audify_spectrum outside this class and this inside. Maybe it's just that audify_spectrum would be better off in the sonify plugin file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that is what #3330 is hoping to fix by moving some/all of the cube_listener.py code into the sonify data plugin.

"""
Iterate through the cube, convert each spectrum to a signal, and store
in class attributes
"""
lo2hi = self.wlens.argsort()[::-1]

Check warning on line 117 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L117

Added line #L117 was not covered by tests

t0 = time.time()
for i in tqdm(range(self.cube.shape[0])):
for j in range(self.cube.shape[1]):
Comment on lines +120 to +121
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any time I see nested for loops it makes me twitch - I forget how long this takes, but I would advocate for a follow-up ticket to multiprocess this (given that it doesn't seem amenable to vectorization) like we do for cube model fitting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, tracking that issue at #3339 .

with suppress_stderr():
if self.cube[i, j, lo2hi].any():
sig = audify_spectrum(self.cube[i, j, lo2hi], self.dur,

Check warning on line 124 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L119-L124

Added lines #L119 - L124 were not covered by tests
srate=self.srate,
fmin=self.audfrqmin,
fmax=self.audfrqmax,
eln=self.eln)
sig = (sig*self.maxval).astype('int16')
self.sigcube[i, j, :] = sig

Check warning on line 130 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L129-L130

Added lines #L129 - L130 were not covered by tests
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")

Check warning on line 136 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L132-L136

Added lines #L132 - L136 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this print statement a temporary way to inform the user or for debugging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was originally for debugging but now it might be useful for logging performance. cube_listener.py is pretty isolated right now but maybe it could be a snackbar message once we figure out where this code should live.


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]
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

Check warning on line 147 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L139-L147

Added lines #L139 - L147 were not covered by tests
else:
outdata[:, 0] = self.cursig[dxs]
outdata[:, 0] //= self.atten_level

Check warning on line 150 in jdaviz/configs/cubeviz/plugins/cube_listener.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/cube_listener.py#L149-L150

Added lines #L149 - L150 were not covered by tests
Empty file.
118 changes: 118 additions & 0 deletions jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from traitlets import Bool, List, Unicode, observe
import astropy.units as u

from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (PluginTemplateMixin, DatasetSelectMixin,
SpectralSubsetSelectMixin, with_spinner)
from jdaviz.core.user_api import PluginUserApi


__all__ = ['SonifyData']

try:
import strauss # noqa
import sounddevice as sd

Check warning on line 15 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L15

Added line #L15 was not covered by tests
except ImportError:
_has_strauss = False
else:
_has_strauss = True

Check warning on line 19 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L19

Added line #L19 was not covered by tests


@tray_registry('cubeviz-sonify-data', label="Sonify Data",
viewer_requirements=['spectrum', 'image'])
class SonifyData(PluginTemplateMixin, DatasetSelectMixin, SpectralSubsetSelectMixin):
"""
See the :ref:`Sonify Data <cubeviz-sonify-data>` for more details.
javerbukh marked this conversation as resolved.
Show resolved Hide resolved

Only the following attributes and methods are available through the
:ref:`public plugin API <plugin-apis>`:

* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
kecnry marked this conversation as resolved.
Show resolved Hide resolved
"""
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)
eln = Bool(False).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)
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?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a follow-up for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will create one and link it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sound_devices_items = List().tag(sync=True)
sound_devices_selected = Unicode('').tag(sync=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._plugin_description = 'Sonify a data cube'
self.docs_description = 'Sonify a data cube using the Strauss package.'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any in-UI instructions on how to "play" the cube once generated. Maybe either here or in a message after pressing sonify, we should point to the tool with some instructions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have information in the documentation (linked in the UI) for how to listen to the cube after pressing the sonify button.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can see what users say too - but I suspect the connection with the spaxel tool might not be obvious and an alert below the button to generate the cube would help (but can be follow-up if you want).


if self.has_strauss:
javerbukh marked this conversation as resolved.
Show resolved Hide resolved
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]]

Check warning on line 62 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L59-L62

Added lines #L59 - L62 were not covered by tests

# 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')

@property
def user_api(self):
expose = []
return PluginUserApi(self, expose)

@with_spinner()
def vue_sonify_cube(self, *args):
# 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,

Check warning on line 77 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L76-L77

Added lines #L76 - L77 were not covered by tests
selected_device_index, self.assidx, self.ssvidx,
self.pccut, self.audfrqmin,
self.audfrqmax, self.eln)

# Automatically select spectrum-at-spaxel tool
spec_at_spaxel_tool = self.flux_viewer.toolbar.tools['jdaviz:spectrumperspaxel']
self.flux_viewer.toolbar.active_tool = spec_at_spaxel_tool

Check warning on line 84 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L83-L84

Added lines #L83 - L84 were not covered by tests
Comment on lines +94 to +96
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't decide if this is convenient or could be confusing, it isn't a pattern we currently use anywhere although I think we have discussed the ability to control tool selection from plugins. Maybe @Jenneh will have thoughts (whether the "spectrum at spaxel" tool in the flux cube viewer should automatically activate after sonification is complete, perhaps deactivating any other tool the user had enabled previously).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it is not expected behavior but early user testing informed us that we needed to automatically have the sound play after the user presses sonify data and the audified cube is loaded. I can create a follow-up ticket to decide exactly how we want to do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, we can do this for now, but let's please revisit. Maybe (eventually) a message with a button in the plugin itself to first activate the tool would be ideal to help teach how to toggle it later - we had considered this for other plugins as well but don't yet have the infrastructure to do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also #3269 (comment) - we could switch from the tool to having it dependent on the "active" state of the plugin. That might then avoid user-confusion and the need to instruct altogether and would be consistent with other plugin-owned mouseover events.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may become a moot point once we have the sonified cube as its own layer in the viewer. We will still need to instruct the user how to get the sound to turn on/off (depending on the default behavior) but by that point it will be out of the spectrum-at-spaxel tool. Related tickets #3329 #3330 #3331


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

Check warning on line 88 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L87-L88

Added lines #L87 - L88 were not covered by tests

@observe('spectral_subset_selected')
def update_wavelength_range(self, event):
if not hasattr(self, 'spec_viewer'):
return
display_unit = self.spec_viewer.state.x_display_unit
min_wavelength = self.spectral_subset.selected_obj.lower.to_value(u.Unit(display_unit))
max_wavelength = self.spectral_subset.selected_obj.upper.to_value(u.Unit(display_unit))
self.flux_viewer.update_listener_wls(min_wavelength, max_wavelength, display_unit)

Check warning on line 97 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L94-L97

Added lines #L94 - L97 were not covered by tests
kecnry marked this conversation as resolved.
Show resolved Hide resolved

@observe('volume')
def update_volume_level(self, event):
self.flux_viewer.update_volume_level(event['new'])

Check warning on line 101 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L101

Added line #L101 was not covered by tests

@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)

Check warning on line 107 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L105-L107

Added lines #L105 - L107 were not covered by tests

def build_device_lists(self):
# dedicated function to build the current *output*
# device and index lists
devices = []
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'])
device_indexes.append(index)
return devices, device_indexes

Check warning on line 118 in jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py#L112-L118

Added lines #L112 - L118 were not covered by tests
Loading