Skip to content

Commit

Permalink
Added PointProcess.get_jitter and PointProcess.get_shimmer, made sure…
Browse files Browse the repository at this point in the history
… that py::array_t is accepted in PointProcess constructor and PointProcess.add_points, and some minor fixes to Pitch's PointProcess methods
  • Loading branch information
YannickJadoul authored and hokiedsp committed Oct 4, 2024
1 parent 35970e7 commit 6726281
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 67 deletions.
2 changes: 1 addition & 1 deletion src/parselmouth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ using PraatBindings = Bindings<PraatError,
PraatWarning,
PraatFatal,
ValueInterpolation,
PeakInterpolation,
PeakInterpolation,
WindowShape,
AmplitudeScaling,
SignalOutsideTimeDomain,
Expand Down
58 changes: 28 additions & 30 deletions src/parselmouth/Pitch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,36 +243,34 @@ PRAAT_CLASS_BINDING(Pitch) {

// TODO To PitchTier: depends on PitchTier

def(
"to_point_process",
[](Pitch self, Sound sound, std::string method, bool include_maxima, bool include_minima) {
if (sound) {
if (method == "cc")
return Sound_Pitch_to_PointProcess_cc(sound, self);
else if (method == "peaks")
return Sound_Pitch_to_PointProcess_peaks(sound, self, include_maxima, include_minima);
else
throw std::invalid_argument("Unknown method specified.");
} else {
return Pitch_to_PointProcess(self);
}
},
"sound"_a = nullptr, "method"_a = "cc", "include_maxima"_a = true, "include_minima"_a = false,
TO_POINT_PROCESS_DOCSTRING);

def(
"to_point_process_cc",
[](Pitch self, Sound sound) { return Sound_Pitch_to_PointProcess_cc(sound, self); },
"sound"_a, TO_POINT_PROCESS_CC_DOCSTRING);

def(
"to_point_process_peaks",
[](Pitch self, Sound sound, bool include_maxima, bool include_minima) {
return Sound_Pitch_to_PointProcess_peaks(sound, self, include_maxima, include_minima);
},
"sound"_a, "include_maxima"_a = true, "include_minima"_a = false,
TO_POINT_PROCESS_PEAKS_DOCSTRING);

// TODO Not sure what to do with this, yet
def("to_point_process",
[](Pitch self, Sound sound, std::string method, bool include_maxima, bool include_minima) {
if (sound) {
if (method == "cc")
return Sound_Pitch_to_PointProcess_cc(sound, self);
else if (method == "peaks")
return Sound_Pitch_to_PointProcess_peaks(sound, self, include_maxima, include_minima);
else
throw std::invalid_argument("Unknown method specified.");
} else {
return Pitch_to_PointProcess(self);
}
},
"sound"_a = nullptr, "method"_a = "cc", "include_maxima"_a = true, "include_minima"_a = false,
TO_POINT_PROCESS_DOCSTRING);

// NEW1_Sound_Pitch_to_PointProcess_cc
def("to_point_process_cc",
[](Pitch self, Sound sound) { return Sound_Pitch_to_PointProcess_cc(sound, self); },
"sound"_a.none(false),
TO_POINT_PROCESS_CC_DOCSTRING);

// NEW1_Sound_Pitch_to_PointProcess_peaks
def("to_point_process_peaks",
[](Pitch self, Sound sound, bool include_maxima, bool include_minima) { return Sound_Pitch_to_PointProcess_peaks(sound, self, include_maxima, include_minima); },
"sound"_a.none(false), "include_maxima"_a = true, "include_minima"_a = false,
TO_POINT_PROCESS_PEAKS_DOCSTRING);

def("to_matrix",
&Pitch_to_Matrix);
Expand Down
14 changes: 7 additions & 7 deletions src/parselmouth/Pitch_docstrings.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
namespace parselmouth {

constexpr auto TO_POINT_PROCESS_DOCSTRING =
R"(Create PointProcess from Pitch object.
R"(Create a `PointProcess` from a `Pitch` object.
Returns a new PointProcess instance by interpreting the acoustic
periodicity contour in the `Pitch` object as the frequency of an
Expand Down Expand Up @@ -69,26 +69,26 @@ See Also
)";

constexpr auto TO_POINT_PROCESS_CC_DOCSTRING =
R"(Create PointProcess from Sound and Pitch objects using crosscorrelation.
R"(Create a `PointProcess` using cross-correlation.
Returns a new PointProcess instance, generated from the specified Sound
and Pitch instances using the cross-correlation method. The resulting
Returns a new `PointProcess` instance, generated from the specified `Sound`
and `Pitch` instances using the cross-correlation method. The resulting
instance contains voiced and unvoiced intervals according to ``pitch``
object, and the voiced intervals are further divided into fundamental
periods of voice, identified by cross-correlating the sound samples.
Parameters
----------
sound : parselmouth.Sound
Sound object containing the target sound waveform
Sound object containing the target sound waveform.
See Also
--------
:praat:`Sound & Pitch: To PointProcess (cc)`
)";

constexpr auto TO_POINT_PROCESS_PEAKS_DOCSTRING =
R"(Create PointProcess from Sound and Pitch objects using peak-picking.
R"(Create a `PointProcess` using peak-picking.
Returns a new PointProcess instance, generated from the specified `Sound`
and `Pitch` instances using the peak-picking method. The resulting
Expand All @@ -103,7 +103,7 @@ analysis and subsequent overlap-add synthesis.
Parameters
----------
sound : parselmouth.Sound
Sound object containing the target sound waveform
Sound object containing the target sound waveform.
See Also
--------
Expand Down
110 changes: 99 additions & 11 deletions src/parselmouth/PointProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include <tuple>
#include <vector>

#include <pybind11/numpy.h>
#include <pybind11/stl.h>

namespace py = pybind11;
Expand All @@ -45,7 +46,48 @@ using namespace std::string_literals;

namespace parselmouth {

enum class JitterMeasurement {
LOCAL,
LOCAL_ABSOLUTE,
RAP,
PPQ5,
DDP,
};

enum class ShimmerMeasurement {
LOCAL,
LOCAL_DB,
APQ3,
APQ5,
APQ11,
DDA,
};

PRAAT_ENUM_BINDING(JitterMeasurement) {
value("LOCAL", JitterMeasurement::LOCAL);
value("LOCAL_ABSOLUTE", JitterMeasurement::LOCAL_ABSOLUTE);
value("RAP", JitterMeasurement::RAP);
value("PPQ5", JitterMeasurement::PPQ5);
value("DDP", JitterMeasurement::DDP);

make_implicitly_convertible_from_string(*this);
}

PRAAT_ENUM_BINDING(ShimmerMeasurement) {
value("LOCAL", ShimmerMeasurement::LOCAL);
value("LOCAL_DB", ShimmerMeasurement::LOCAL_DB);
value("APQ3", ShimmerMeasurement::APQ3);
value("APQ5", ShimmerMeasurement::APQ5);
value("APQ11", ShimmerMeasurement::APQ11);
value("DDA", ShimmerMeasurement::DDA);

make_implicitly_convertible_from_string(*this);
}

PRAAT_CLASS_BINDING(PointProcess) {
NESTED_BINDINGS(JitterMeasurement,
ShimmerMeasurement)

using signature_cast_placeholder::_;

addTimeFunctionMixin(*this);
Expand All @@ -60,18 +102,23 @@ PRAAT_CLASS_BINDING(PointProcess) {
"start_time"_a, "end_time"_a,
CONSTRUCTOR_EMPTY_DOCSTRING);

// TODO Use py::array_t
def(py::init([](std::vector<double> times, std::optional<double> startTime, std::optional<double> endTime) {
if (times.empty())
throw py::value_error("Cannot create a PointProcess from an empty list of time points.");
def(py::init([](py::array_t<double, py::array::c_style> times, std::optional<double> startTime, std::optional<double> endTime) {
// TODO Should we `times.squeeze();` ?
if (times.ndim() != 1)
throw py::value_error("Can only create a PointProcess from a one-dimensional array.");
auto n = times.shape(0);
if (n == 0)
throw py::value_error("Cannot create a PointProcess from an empty array of time points.");

double t0 = startTime ? *startTime : *std::min_element(times.cbegin(), times.cend());
double t1 = endTime ? *endTime : *std::max_element(times.cbegin(), times.cend());
auto data = times.data();
auto [it0, it1] = !(startTime && endTime) ? std::minmax_element(data, data + n) : std::pair(data, data);
double t0 = startTime.value_or(*it0);
double t1 = endTime.value_or(*it1);

Melder_require (endTime >= startTime, U"Your end time (", t0, U") should not be less than your start time (", t1, U").");
auto result = PointProcess_create(t0, t1, times.size());

PointProcess_addPoints(result.get(), constVEC(times.data(), times.size()));
PointProcess_addPoints(result.get(), constVEC(data, n));
return result;
}),
"time_points"_a, "start_time"_a = std::nullopt, "end_time"_a = std::nullopt,
Expand Down Expand Up @@ -185,7 +232,24 @@ PRAAT_CLASS_BINDING(PointProcess) {
RANGE_ARGS,
GET_JITTER_DDP_DOCSTRING);

// TODO get_jitter(JitterMeasure) ?
def("get_jitter",
[](PointProcess self, JitterMeasurement measurement, std::optional<double> fromTime, std::optional<double> toTime, double periodFloor, double periodCeiling, Positive<double> maximumPeriodFactor) {
auto call = [&](auto f) { return f(self, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), periodFloor, periodCeiling, maximumPeriodFactor); };
switch (measurement) {
case JitterMeasurement::LOCAL:
return call(PointProcess_getJitter_local);
case JitterMeasurement::LOCAL_ABSOLUTE:
return call(PointProcess_getJitter_local_absolute);
case JitterMeasurement::RAP:
return call(PointProcess_getJitter_rap);
case JitterMeasurement::PPQ5:
return call(PointProcess_getJitter_ppq5);
case JitterMeasurement::DDP:
return call(PointProcess_getJitter_ddp);
}
throw py::value_error("Invalid JitterMeasurement value");
},
"measurement"_a, RANGE_ARGS);

def("get_count_and_fraction_of_voice_breaks",
[](PointProcess self, std::optional<double> fromTime, std::optional<double> toTime, double maximumPeriod) {
Expand Down Expand Up @@ -230,6 +294,27 @@ PRAAT_CLASS_BINDING(PointProcess) {
SHIMMER_RANGE_ARGS,
GET_SHIMMER_DDA_DOCSTRING);

def("get_shimmer",
[](PointProcess self, Sound sound, ShimmerMeasurement measurement, std::optional<double> fromTime, std::optional<double> toTime, double periodFloor, double periodCeiling, Positive<double> maximumPeriodFactor, Positive<double> maximumAmplitudeFactor) {
auto call = [&](auto f) { return f(self, sound, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), periodFloor, periodCeiling, maximumPeriodFactor, maximumAmplitudeFactor); };
switch (measurement) {
case ShimmerMeasurement::LOCAL:
return call(PointProcess_Sound_getShimmer_local);
case ShimmerMeasurement::LOCAL_DB:
return call(PointProcess_Sound_getShimmer_local_dB);
case ShimmerMeasurement::APQ3:
return call(PointProcess_Sound_getShimmer_apq3);
case ShimmerMeasurement::APQ5:
return call(PointProcess_Sound_getShimmer_apq5);
case ShimmerMeasurement::APQ11:
return call(PointProcess_Sound_getShimmer_apq11);
case ShimmerMeasurement::DDA:
return call(PointProcess_Sound_getShimmer_dda);
}
throw py::value_error("Invalid ShimmerMeasurement value");
},
"sound"_a.none(false), "measurement"_a, RANGE_ARGS, "maximum_amplitude_factor"_a = 1.6);

// INTEGER_PointProcess_getLowIndex
def("get_low_index",
PointProcess_getLowIndex,
Expand Down Expand Up @@ -288,9 +373,12 @@ PRAAT_CLASS_BINDING(PointProcess) {

// MODIFY_PointProcess_addPoints
def("add_points",
// TODO py::array_t ? Caster for constVEC?
[](PointProcess self, std::vector<double> times) {
PointProcess_addPoints(self, constVEC(times.data(), times.size()));
// TODO Caster for constVEC?
[](PointProcess self, py::array_t<double, py::array::c_style> times) {
// TODO Should we `times.squeeze();` ?
if (times.ndim() != 1)
throw py::value_error("Expected a one-dimensional array.");
PointProcess_addPoints(self, constVEC(times.data(), times.shape(0)));
},
"times"_a,
ADD_POINTS_DOCSTRING);
Expand Down
12 changes: 6 additions & 6 deletions src/parselmouth/Sound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ PRAAT_CLASS_BINDING(Sound) {
case ToPitchMethod::SHS:
return callMethod("to_pitch_shs");
}
return py::none(); // Unreachable
throw py::value_error("Invalid ToPitchMethod value");
},
"method"_a);

Expand Down Expand Up @@ -542,7 +542,7 @@ PRAAT_CLASS_BINDING(Sound) {
case ToHarmonicityMethod::GNE:
return callMethod("to_harmonicity_gne");
}
return py::none(); // Unreachable
throw py::value_error("Invalid ToPitchMethod value");
},
"method"_a = ToHarmonicityMethod::CC);

Expand Down Expand Up @@ -614,7 +614,7 @@ PRAAT_CLASS_BINDING(Sound) {
},
"number_of_coefficients"_a = 12, "window_length"_a = 0.015, "time_step"_a = 0.005, "firstFilterFreqency"_a = 100.0, "distance_between_filters"_a = 100.0, "maximum_frequency"_a = std::nullopt);

// FORM (NEW_Sound_to_PointProcess_extrema
// NEW_Sound_to_PointProcess_extrema
def(
"to_point_process_extrema",
[](Sound self, Channel channel, bool includeMaxima, bool includeMinima,
Expand All @@ -628,7 +628,7 @@ PRAAT_CLASS_BINDING(Sound) {
"interpolation"_a = kVector_peakInterpolation::SINC70,
TO_POINT_PROCESS_EXTREMA_DOCSTRING);

// FORM (NEW_Sound_to_PointProcess_periodic_cc
// NEW_Sound_to_PointProcess_periodic_cc
def(
"to_point_process_periodic",
[](Sound self, float minimumPitch, float maximumPitch) {
Expand All @@ -641,7 +641,7 @@ PRAAT_CLASS_BINDING(Sound) {
"minimum_pitch"_a = 75.0, "maximum_pitch"_a = 600.0,
TO_POINT_PROCESS_PERIODIC_DOCSTRING);

// FORM (NEW_Sound_to_PointProcess_periodic_peaks
// NEW_Sound_to_PointProcess_periodic_peaks
def(
"to_point_process_periodic_peaks",
[](Sound self, float minimumPitch, float maximumPitch, bool includeMaxima,
Expand All @@ -656,7 +656,7 @@ PRAAT_CLASS_BINDING(Sound) {
"include_maxima"_a = true, "include_minima"_a = false,
TO_POINT_PROCESS_PERIODIC_PEAKS_DOCSTRING);

// FORM (NEW_Sound_to_PointProcess_zeroes
// NEW_Sound_to_PointProcess_zeroes
def(
"to_point_process_zeros",
[](Sound self, Channel ch, bool includeRaisers, bool includeFallers) {
Expand Down
31 changes: 19 additions & 12 deletions tests/test_point_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ def test_create_process_from_array():
assert process.xmax == 1.0
assert np.array_equal(process, np.sort(t))

process = parselmouth.PointProcess(np.array(t))
assert np.array_equal(process, np.sort(t))

process = parselmouth.PointProcess(np.array(t)[::2])
assert np.array_equal(process, np.sort(t[::2]))


def test_create_poisson_process():
poisson_process = parselmouth.PointProcess.create_poisson_process(0, 1, 100)
Expand Down Expand Up @@ -88,23 +94,23 @@ def test_get_jitters(point_process):
def call_jitter(which):
return call(point_process, f"Get jitter ({which})", 0, 0, 0.0001, 0.02, 1.3)

assert point_process.get_jitter_local() == call_jitter("local")
assert point_process.get_jitter_local_absolute() == call_jitter("local, absolute")
assert point_process.get_jitter_rap() == call_jitter("rap")
assert point_process.get_jitter_ppq5() == call_jitter("ppq5")
assert point_process.get_jitter_ddp() == call_jitter("ddp")
assert point_process.get_jitter_local() == point_process.get_jitter('LOCAL') == call_jitter("local")
assert point_process.get_jitter_local_absolute() == point_process.get_jitter('LOCAL_ABSOLUTE') == call_jitter("local, absolute")
assert point_process.get_jitter_rap() == point_process.get_jitter('RAP') == call_jitter("rap")
assert point_process.get_jitter_ppq5() == point_process.get_jitter('PPQ5') == call_jitter("ppq5")
assert point_process.get_jitter_ddp() == point_process.get_jitter('DDP') == call_jitter("ddp")


def test_get_shimmers(sound, point_process):
def call_shimmer(which):
return call([point_process, sound], f"Get shimmer ({which})", 0, 0, 0.0001, 0.02, 1.3, 1.6)

assert point_process.get_shimmer_local(sound) == call_shimmer("local")
assert point_process.get_shimmer_local_db(sound) == call_shimmer("local_dB")
assert point_process.get_shimmer_apq3(sound) == call_shimmer("apq3")
assert point_process.get_shimmer_apq5(sound) == call_shimmer("apq5")
assert point_process.get_shimmer_apq11(sound) == call_shimmer("apq11")
assert point_process.get_shimmer_dda(sound) == call_shimmer("dda")
assert point_process.get_shimmer_local(sound) == point_process.get_shimmer(sound, 'LOCAL') == call_shimmer("local")
assert point_process.get_shimmer_local_db(sound) == point_process.get_shimmer(sound, 'LOCAL_DB') == call_shimmer("local_dB")
assert point_process.get_shimmer_apq3(sound) == point_process.get_shimmer(sound, 'APQ3') == call_shimmer("apq3")
assert point_process.get_shimmer_apq5(sound) == point_process.get_shimmer(sound, 'APQ5') == call_shimmer("apq5")
assert point_process.get_shimmer_apq11(sound) == point_process.get_shimmer(sound, 'APQ11') == call_shimmer("apq11")
assert point_process.get_shimmer_dda(sound) == point_process.get_shimmer(sound, 'DDA') == call_shimmer("dda")


def test_get_count_and_fraction_of_voice_breaks(point_process):
Expand All @@ -128,7 +134,8 @@ def test_modifications(point_process):
point_process.add_point(np.random.uniform(point_process.tmin, point_process.tmax))
assert len(point_process) == n + 1

point_process.add_points(np.random.uniform(point_process.tmin, point_process.tmax, 9))
point_process.add_points(np.random.uniform(point_process.tmin, point_process.tmax, 7))
point_process.add_points(np.random.uniform(point_process.tmin, point_process.tmax, 6)[::3])
assert len(point_process) == n + 10

p = point_process[0]
Expand Down

0 comments on commit 6726281

Please sign in to comment.