Skip to content

Commit

Permalink
implemented pitch contour sonification.
Browse files Browse the repository at this point in the history
  • Loading branch information
bmcfee committed May 26, 2016
1 parent f858df3 commit 042f9a2
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 1 deletion.
60 changes: 60 additions & 0 deletions mir_eval/sonify.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import numpy as np
from numpy.lib.stride_tricks import as_strided
from scipy.interpolate import interp1d

from . import util
from . import chord

Expand Down Expand Up @@ -140,6 +142,64 @@ def _fast_synthesize(frequency):
return output


def pitch_contour(times, frequencies, fs, function=np.sin, length=None,
kind='linear'):
'''Sonify a pitch contour.
Parameters
----------
times : np.ndarray
time indices for each frequency measurement, in seconds
frequencies : np.ndarray
frequency measurements, in Hz.
Non-positive measurements will be interpreted as un-voiced samples.
fs : int
desired sampling rate of the output signal
function : function
function to use to synthesize notes, should be 2pi-periodic
length : int
desired number of samples in the output signal,
defaults to ``max(times)*fs``
kind : str
Interpolation mode for the frequency estimator.
See: ``scipy.interpolate.interp1d`` for valid settings.
Returns
-------
output : np.ndarray
synthesized version of the pitch contour
'''

fs = float(fs)

if length is None:
length = int(times.max() * fs)

# Squash the negative frequencies.
# wave(0) = 0, so clipping here will un-voice the corresponding instants
frequencies = np.maximum(frequencies, 0.0)

# Build a frequency interpolator
f_interp = interp1d(times * fs, frequencies, kind=kind,
fill_value=0.0, bounds_error=False, copy=False)

# Estimate frequency at sample points
f_est = f_interp(np.arange(length))

# Sonify the waveform
x = function(2 * np.pi * np.cumsum(f_est) / fs)

# Clamp the extrapolated values
x[:int(times.min() * fs)] = 0
x[int(times.max() * fs):] = 0
return x


def chroma(chromagram, times, fs, **kwargs):
"""Reverse synthesis of a chromagram (semitone matrix)
Expand Down
36 changes: 35 additions & 1 deletion tests/test_sonify.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import mir_eval
import numpy as np

import scipy

def test_clicks():
# Test output length for a variety of parameter settings
Expand Down Expand Up @@ -53,3 +53,37 @@ def test_chords():
['C', 'C:maj', 'D:min7', 'E:min', 'C#', 'C', 'C', 'C', 'C', 'C'],
intervals, fs, length=fs*11)
assert len(signal) == 11*fs


def test_pitch_contour():

# Generate some random pitch
fs = 8000
times = np.linspace(0, 5, num=5 * fs, endpoint=True)

freqs = 440.0 * 2.0**(16 * scipy.ndimage.gaussian_filter1d(np.random.randn(len(times)),
sigma=256))

# negate a bunch of sequences
idx = np.unique(np.random.randint(0, high=len(times), size=32))
for s, t in zip(idx[::2], idx[1::2]):
freqs[s:t] *= -1

# Test with inferring duration
x = mir_eval.sonify.pitch_contour(times, freqs, fs)
assert len(x) == fs * 5

# Test with an explicit duration
# This forces the interpolator to go off the end of the sampling grid,
# which should put zeros at the end of the signal
x = mir_eval.sonify.pitch_contour(times, freqs, fs, length=fs * 7)
assert len(x) == fs * 7
assert not np.any(x[-fs * 2:]), x[-fs * 2:]

# Test with an explicit duration and a fixed offset
# This forces the interpolator to go off the beginning of the sampling grid,
# which should put zeros at the beginning of the signal
x = mir_eval.sonify.pitch_contour(times + 5.0, freqs, fs, length=fs * 7)
assert len(x) == fs * 7
assert not np.any(x[:fs * 5]), x[:fs * 5]

0 comments on commit 042f9a2

Please sign in to comment.