-
Notifications
You must be signed in to change notification settings - Fork 25
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 #91
Merged
Sonify #91
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
#!/usr/bin/env python | ||
# CREATED:2015-12-12 18:20:37 by Brian McFee <brian.mcfee@nyu.edu> | ||
r''' | ||
Sonification | ||
============ | ||
|
||
.. autosummary:: | ||
:toctree: generated/ | ||
|
||
sonify | ||
''' | ||
|
||
from itertools import product | ||
from collections import OrderedDict | ||
import six | ||
import numpy as np | ||
import mir_eval.sonify | ||
from mir_eval.util import filter_kwargs | ||
from .eval import coerce_annotation, hierarchy_flatten | ||
from .exceptions import NamespaceError | ||
|
||
__all__ = ['sonify'] | ||
|
||
|
||
def mkclick(freq, sr=22050, duration=0.1): | ||
'''Generate a click sample. | ||
|
||
This replicates functionality from mir_eval.sonify.clicks, | ||
but exposes the target frequency and duration. | ||
''' | ||
|
||
times = np.arange(int(sr * duration)) | ||
click = np.sin(2 * np.pi * times * freq / float(sr)) | ||
click *= np.exp(- times / (1e-2 * sr)) | ||
|
||
return click | ||
|
||
|
||
def clicks(annotation, sr=22050, length=None, **kwargs): | ||
'''Sonify events with clicks. | ||
|
||
This uses mir_eval.sonify.clicks, and is appropriate for instantaneous | ||
events such as beats or segment boundaries. | ||
''' | ||
|
||
interval, _ = annotation.data.to_interval_values() | ||
|
||
return filter_kwargs(mir_eval.sonify.clicks, interval[:, 0], | ||
fs=sr, length=length, **kwargs) | ||
|
||
|
||
def downbeat(annotation, sr=22050, length=None, **kwargs): | ||
'''Sonify beats and downbeats together. | ||
''' | ||
|
||
beat_click = mkclick(440 * 2, sr=sr) | ||
downbeat_click = mkclick(440 * 3, sr=sr) | ||
|
||
intervals, values = annotation.data.to_interval_values() | ||
|
||
beats, downbeats = [], [] | ||
|
||
for time, value in zip(intervals[:, 0], values): | ||
if value['position'] == 1: | ||
downbeats.append(time) | ||
else: | ||
beats.append(time) | ||
|
||
if length is None: | ||
length = int(sr * np.max(intervals)) + len(beat_click) + 1 | ||
|
||
y = filter_kwargs(mir_eval.sonify.clicks, | ||
np.asarray(beats), | ||
fs=sr, length=length, click=beat_click) | ||
|
||
y += filter_kwargs(mir_eval.sonify.clicks, | ||
np.asarray(downbeats), | ||
fs=sr, length=length, click=downbeat_click) | ||
|
||
return y | ||
|
||
|
||
def multi_segment(annotation, sr=22050, length=None, **kwargs): | ||
'''Sonify multi-level segmentations''' | ||
|
||
# Pentatonic scale, because why not | ||
PENT = [1, 32./27, 4./3, 3./2, 16./9] | ||
DURATION = 0.1 | ||
|
||
h_int, _ = hierarchy_flatten(annotation) | ||
|
||
if length is None: | ||
length = int(sr * (max(np.max(_) for _ in h_int) + 1. / DURATION) + 1) | ||
|
||
y = 0.0 | ||
for ints, (oc, scale) in zip(h_int, product(range(3, 3 + len(h_int)), | ||
PENT)): | ||
click = mkclick(440.0 * scale * oc, sr=sr, duration=DURATION) | ||
y = y + filter_kwargs(mir_eval.sonify.clicks, | ||
np.unique(ints), | ||
fs=sr, length=length, | ||
click=click) | ||
return y | ||
|
||
|
||
def chord(annotation, sr=22050, length=None, **kwargs): | ||
'''Sonify chords | ||
|
||
This uses mir_eval.sonify.chords. | ||
''' | ||
|
||
intervals, chords = annotation.data.to_interval_values() | ||
|
||
return filter_kwargs(mir_eval.sonify.chords, | ||
chords, intervals, | ||
fs=sr, length=length, | ||
**kwargs) | ||
|
||
|
||
def pitch_contour(annotation, sr=22050, length=None, **kwargs): | ||
'''Sonify pitch contours. | ||
|
||
This uses mir_eval.sonify.pitch_contour, and should only be applied | ||
to pitch annotations using the pitch_contour namespace. | ||
|
||
Each contour is sonified independently, and the resulting waveforms | ||
are summed together. | ||
''' | ||
|
||
times, values = annotation.data.to_interval_values() | ||
|
||
indices = np.unique([v['index'] for v in values]) | ||
|
||
y_out = 0.0 | ||
for ix in indices: | ||
rows = annotation.data.value.apply(lambda x: x['index'] == ix).nonzero()[0] | ||
|
||
freqs = np.asarray([values[r]['frequency'] for r in rows]) | ||
unv = ~np.asarray([values[r]['voiced'] for r in rows]) | ||
freqs[unv] *= -1 | ||
|
||
y_out = y_out + filter_kwargs(mir_eval.sonify.pitch_contour, | ||
times[rows, 0], | ||
freqs, | ||
fs=sr, | ||
length=length, | ||
**kwargs) | ||
if length is None: | ||
length = len(y_out) | ||
|
||
return y_out | ||
|
||
|
||
def piano_roll(annotation, sr=22050, length=None, **kwargs): | ||
'''Sonify a piano-roll | ||
|
||
This uses mir_eval.sonify.time_frequency, and is appropriate | ||
for sparse transcription data, e.g., annotations in the `note_midi` | ||
namespace. | ||
''' | ||
|
||
intervals, pitches = annotation.data.to_interval_values() | ||
|
||
# Construct the pitchogram | ||
pitch_map = {f: idx for idx, f in enumerate(np.unique(pitches))} | ||
|
||
gram = np.zeros((len(pitch_map), len(intervals))) | ||
|
||
for col, f in enumerate(pitches): | ||
gram[pitch_map[f], col] = 1 | ||
|
||
return filter_kwargs(mir_eval.sonify.time_frequency, | ||
gram, pitches, intervals, | ||
sr, length=length, **kwargs) | ||
|
||
|
||
SONIFY_MAPPING = OrderedDict() | ||
SONIFY_MAPPING['beat_position'] = downbeat | ||
SONIFY_MAPPING['beat'] = clicks | ||
SONIFY_MAPPING['multi_segment'] = multi_segment | ||
SONIFY_MAPPING['segment_open'] = clicks | ||
SONIFY_MAPPING['onset'] = clicks | ||
SONIFY_MAPPING['chord'] = chord | ||
SONIFY_MAPPING['note_hz'] = piano_roll | ||
SONIFY_MAPPING['pitch_contour'] = pitch_contour | ||
|
||
|
||
def sonify(annotation, sr=22050, duration=None, **kwargs): | ||
'''Sonify a jams annotation through mir_eval | ||
|
||
Parameters | ||
---------- | ||
annotation : jams.Annotation | ||
The annotation to sonify | ||
|
||
sr = : positive number | ||
The sampling rate of the output waveform | ||
|
||
duration : float (optional) | ||
Optional length (in seconds) of the output waveform | ||
|
||
kwargs | ||
Additional keyword arguments to mir_eval.sonify functions | ||
|
||
Returns | ||
------- | ||
y_sonified : np.ndarray | ||
The waveform of the sonified annotation | ||
|
||
Raises | ||
------ | ||
NamespaceError | ||
If the annotation has an un-sonifiable namespace | ||
''' | ||
|
||
length = None | ||
|
||
if duration is None: | ||
duration = annotation.duration | ||
|
||
if duration is not None: | ||
length = int(duration * sr) | ||
|
||
for namespace, func in six.iteritems(SONIFY_MAPPING): | ||
try: | ||
ann = coerce_annotation(annotation, namespace) | ||
return func(ann, sr=sr, length=length, **kwargs) | ||
except NamespaceError: | ||
pass | ||
|
||
raise NamespaceError('Unable to sonify annotation of namespace="{:s}"' | ||
.format(annotation.namespace)) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're choosing the sonification function by checking whether the annotation can be converted to each supported namespace and if so you sonify it? What if an annotation can be converted to two different namespaces, both of which have supported sonifications? Wouldn't this call both sonification functions?
Wouldn't it be cleaner to just check if the annotation namespace is a key in
SONIFY_MAPPING
and if so call the function it maps to?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know of any such namespaces that don't already have a native mapping. Am I missing something?
No -- it returns on the first successful conversion. I think this is a sane convention.
We'd need to explicitly code each namespace into a data structure, and I'd rather not do that.
For most namespace types, this isn't a huge deal, but for something like
segment_*
there are many namespaces (and potentially more in the future). Trying to do something programmatic to handle this case would eventually boil down to auto-conversion anyway.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a specific example, just thinking of a hypothetical future case where you want to have different sonification functions for two annotations that can be converted into each other (imagine you decided to write a new/different function for
pitch_midi
and don't wantpitch_hz
annotations to be sonified using it and vise versa). Maybe this is a contrived case...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If two namespaces can be converted to each other, then they must be "equivalent" in a pretty strong sense. It's hard to imagine why we would have different sonification methods there.
However, I think I can imagine a slightly more realistic case in which conversion only works in one way. We currently have a 'click' sonifier that applies to all event-based data, for example, beats. We also have two beat namespaces, one with metrical position and one without. This conversion can go in only one direction:
beat_position -> beat
.Now, imagine that we want to have a sonifier that plays different clicks on the downbeat. This would only apply to the
beat_position
namespace, but that can also be coerced into abeat
namespace.Proposed solution: use an ordered dict to store the mappings, and put the most specific namespaces first. The conversion logic stays the same, but sonification is always done with the most specific applicable method. If you want to sonify using an alternate method, you can always auto-convert the annotation before sonifying.
How does this sound?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could work, but the logic could get increasingly unwieldy as we add sonifications/namespaces/conversions. For example, in the future we might want to add destructive one-way conversions such as going from pitch_midi to onsets. The sonifications are dramatically different, and since onsets might come first in the ordered dictionary, it could get messy?
I actually don't think that a direct dictionary mapping from each namespace-to-sonification would be that bad... we don't have that many namespaces and it's not a quantity that's likely to grow very fast. And anyone adding a new namespace can make a PR that just adds the new namespace to the mapping dictionary, and since every namespace is mapped independently we don't have to worry about some new namespace or new conversion breaking the logic of the ordered dict. I actually think it's the simplest solution in the long run, but well, open to rebuttals as always.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!resolved