diff --git a/docs/conf.py b/docs/conf.py index 371bb3668..58c045cbb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,6 @@ 'sphinx.ext.coverage', 'sphinx_copybutton', 'nbsphinx', - 'pybind11_docstrings', 'praat_manual'] # Add any paths that contain templates here, relative to this directory. @@ -135,6 +134,9 @@ 'numpy': ('https://numpy.org/doc/stable/', None), 'tgt': ('https://textgridtools.readthedocs.io/en/stable/', None)} +# Napoleon configuration +napoleon_preprocess_types = True + default_role = 'py:obj' nitpicky = True nitpick_ignore = [('py:class', 'pybind11_builtins.pybind11_object'), @@ -143,6 +145,8 @@ ('py:class', 'NonNegative'), ('py:class', 'numpy.float64'), ('py:class', 'numpy.complex128'), + ('py:class', 'positive'), + ('py:class', 'readonly'), ('py:obj', 'List')] diff --git a/docs/pybind11_docstrings.py b/docs/pybind11_docstrings.py deleted file mode 100644 index a335795e2..000000000 --- a/docs/pybind11_docstrings.py +++ /dev/null @@ -1,12 +0,0 @@ -import re - -RE_ARGS_KWARGS = re.compile(r'(? #include +#include #include #include @@ -148,7 +151,7 @@ PRAAT_CLASS_BINDING(Pitch) { "from_time"_a = std::nullopt, "to_time"_a = std::nullopt, "sampling_frequency"_a = 44100.0, "round_to_nearest_zero_crossing"_a = true); def("count_voiced_frames", - &Pitch_countVoicedFrames); + &Pitch_countVoicedFrames); def("get_value_at_time", [](Pitch self, double time, kPitch_unit unit, kVector_valueInterpolation interpolation) { @@ -228,18 +231,47 @@ PRAAT_CLASS_BINDING(Pitch) { &Pitch_interpolate); def("smooth", - args_cast<_, Positive<_>>(Pitch_smooth), - "bandwidth"_a = 10.0); + args_cast<_, Positive<_>>(Pitch_smooth), + "bandwidth"_a = 10.0); def("subtract_linear_fit", &Pitch_subtractLinearFit, - "unit"_a = kPitch_unit::HERTZ); + "unit"_a = kPitch_unit::HERTZ); def("kill_octave_jumps", - &Pitch_killOctaveJumps); + &Pitch_killOctaveJumps); // TODO To PitchTier: depends on PitchTier + // 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); diff --git a/src/parselmouth/Pitch_docstrings.h b/src/parselmouth/Pitch_docstrings.h new file mode 100644 index 000000000..8a6222ebe --- /dev/null +++ b/src/parselmouth/Pitch_docstrings.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2021 Yannick Jadoul and contributors + * + * This file is part of Parselmouth. + * + * Parselmouth is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Parselmouth is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Parselmouth. If not, see + */ + +#pragma once +#ifndef INC_PARSELMOUTH_PITCH_DOCSTRINGS_H +#define INC_PARSELMOUTH_PITCH_DOCSTRINGS_H + +namespace parselmouth { + +constexpr auto TO_POINT_PROCESS_DOCSTRING = + 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 +underlying point process (such as the sequence of glottal closures in +vocal-fold vibration). + +The unvoiced intervals in the ``pitch`` object is transferred to the point +process object, and the voiced intervals are further divided into each +phonation cycles. + +Parameters +---------- +sound : parselmouth.Sound, optional + Sound object containing the target sound waveform. If omitted, + `PointProcess` is created only from the pitch contour. Analyzing the + samples in the `Sound` object improves the accuracy of the resulting + point process. + +method : {"cc", "peaks"}, default "cc" + Specify the Sound-assisted generation method: + + "cc" + Cross-correlation method. The fundamental periods of voice are + identified by cross-correlating the sound samples. + + "peaks" + Peak-picking method. The fundamental periods of voice are + identified by peak-picking the sound samples. Typically, less + accurate than the cross-correlation method. + +include_maxima : bool, default True + True to include the absolute maximum (for ``method="peaks"`` only). + +include_minima : bool, default False + True to include the absolute minimum (for ``method="peaks"`` only). + +See Also +-------- +:praat:`Pitch: To PointProcess` +:praat:`Sound & Pitch: To PointProcess (cc)` +:praat:`Sound & Pitch: To PointProcess (peaks)...` +)"; + +constexpr auto TO_POINT_PROCESS_CC_DOCSTRING = +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 +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. + +See Also +-------- +:praat:`Sound & Pitch: To PointProcess (cc)` +)"; + +constexpr auto TO_POINT_PROCESS_PEAKS_DOCSTRING = +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 +instance contains voiced and unvoiced intervals according to ``pitch`` +object, and the voiced intervals are further divided into fundamental +periods of voice, identified by peak-picking the sound samples. + +The periods that are found in this way are much more variable than those +found by `Pitch.to_point_process_cc()` and therefore less useful for +analysis and subsequent overlap-add synthesis. + +Parameters +---------- +sound : parselmouth.Sound + Sound object containing the target sound waveform. + +See Also +-------- +:praat:`Sound & Pitch: To PointProcess (peaks)...` +)"; + +} // namespace parselmouth + +#endif // INC_PARSELMOUTH_PITCH_DOCSTRINGS_H diff --git a/src/parselmouth/PointProcess.cpp b/src/parselmouth/PointProcess.cpp new file mode 100644 index 000000000..e03db2197 --- /dev/null +++ b/src/parselmouth/PointProcess.cpp @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2021 Yannick Jadoul and contributors + * + * This file is part of Parselmouth. + * + * Parselmouth is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Parselmouth is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Parselmouth. If not, see + */ + +#include "Parselmouth.h" +#include "PointProcess_docstrings.h" + +#include "TimeClassAspects.h" +#include "utils/SignatureCast.h" +#include "utils/pybind11/ImplicitStringToEnumConversion.h" +#include "utils/pybind11/NumericPredicates.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace py = pybind11; +using namespace py::literals; +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); + + doc() = CREATE_CLASS_DOCSTRING; + + // NEW1_PointProcess_createEmpty + def(py::init([](double startTime, double endTime) { + Melder_require (endTime >= startTime, U"Your end time (", endTime, U") should not be less than your start time (", startTime, U")."); + return PointProcess_create(startTime, endTime, 0); + }), + "start_time"_a, "end_time"_a, + CONSTRUCTOR_EMPTY_DOCSTRING); + + def(py::init([](py::array_t times, std::optional startTime, std::optional 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."); + + 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(data, n)); + return result; + }), + "time_points"_a, "start_time"_a = std::nullopt, "end_time"_a = std::nullopt, + CONSTRUCTOR_FILLED_DOCSTRING); + + // NEW1_PointProcess_createPoissonProcess + def_static("create_poisson_process", + args_cast<_, _, Positive<_>>(PointProcess_createPoissonProcess), + "start_time"_a, "end_time"_a, "density"_a, + CREATE_POISSON_PROCESS_DOCSTRING); + + // TODO .values? Maybe not, if the underlying array can change through `add_point`. + + // Make PointProcess class a s sequence-like Python class + def("__getitem__", + [](PointProcess self, long i) { + if (i < 0) i += self->nt; + if (i < 0 || i >= self->nt) + throw py::index_error("PointProcess index out of range"); + return self->t[i + 1]; + }, + "i"_a); + + def("__len__", [](PointProcess self) { return self->nt; }); + + // Iterators come for free with the sequence protocol, but this is (apparently) slightly faster + def("__iter__", + [](PointProcess self) { + return py::make_iterator(&self->t[1], &self->t[self->nt + 1]); + }, + py::keep_alive<0, 1>()); + +/** + * Standard arguments for many of the query methods + */ +#define RANGE_FUNCTION(f) \ + [](PointProcess self, std::optional fromTime, std::optional toTime, double periodFloor, double periodCeiling, Positive maximumPeriodFactor) { \ + return f(self, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), periodFloor, periodCeiling, maximumPeriodFactor); \ + } +#define RANGE_ARGS \ + "from_time"_a = std::nullopt, "to_time"_a = std::nullopt, "period_floor"_a = 0.0001, "period_ceiling"_a = 0.02, "maximum_period_factor"_a = 1.3 +#define SHIMMER_RANGE_FUNCTION(f) \ + [](PointProcess self, Sound sound, std::optional fromTime, std::optional toTime, double periodFloor, double periodCeiling, Positive maximumPeriodFactor, Positive maximumAmplitudeFactor) { \ + return f(self, sound, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), periodFloor, periodCeiling, maximumPeriodFactor, maximumAmplitudeFactor); \ + } +#define SHIMMER_RANGE_ARGS \ + "sound"_a.none(false), RANGE_ARGS, "maximum_amplitude_factor"_a = 1.6 + + // INTEGER_PointProcess_getNumberOfPoints + def("get_number_of_points", + [](PointProcess self) { return self->nt; }, + GET_NUMBER_OF_POINTS_DOCSTRING); + + def_readonly("n_points", + &structPointProcess::nt, + N_POINTS_DOCSTRING); + + // INTEGER_PointProcess_getNumberOfPeriods + def("get_number_of_periods", + RANGE_FUNCTION(PointProcess_getNumberOfPeriods), + RANGE_ARGS, + GET_NUMBER_OF_PERIODS_DOCSTRING); + + // REAL_PointProcess_getMeanPeriod + def("get_mean_period", + RANGE_FUNCTION(PointProcess_getMeanPeriod), + RANGE_ARGS, + GET_MEAN_PERIOD_DOCSTRING); + + // REAL_PointProcess_getStdevPeriod + def("get_stdev_period", + RANGE_FUNCTION(PointProcess_getStdevPeriod), + RANGE_ARGS, + GET_STDEV_PERIOD_DOCSTRING); + + // REAL_PointProcess_getTimeFromIndex + def("get_time_from_index", + [](PointProcess self, integer pointNumber) { + return (pointNumber <= 0 || pointNumber > self->nt) ? undefined : self->t[pointNumber]; + }, + "point_number"_a, + GET_TIME_FROM_INDEX_DOCSTRING); + + // REAL_PointProcess_getJitter_local + def("get_jitter_local", + RANGE_FUNCTION(PointProcess_getJitter_local), + RANGE_ARGS, + GET_JITTER_LOCAL_DOCSTRING); + + // REAL_PointProcess_getJitter_local_absolute + def("get_jitter_local_absolute", + RANGE_FUNCTION(PointProcess_getJitter_local_absolute), + RANGE_ARGS, + GET_JITTER_LOCAL_ABSOLUTE_DOCSTRING); + + // REAL_PointProcess_getJitter_rap + def("get_jitter_rap", + RANGE_FUNCTION(PointProcess_getJitter_rap), + RANGE_ARGS, + GET_JITTER_RAP_DOCSTRING); + + // REAL_PointProcess_getJitter_ppq5 + def("get_jitter_ppq5", + RANGE_FUNCTION(PointProcess_getJitter_ppq5), + RANGE_ARGS, + GET_JITTER_PPQ5_DOCSTRING); + + // REAL_PointProcess_getJitter_ddp + def("get_jitter_ddp", + RANGE_FUNCTION(PointProcess_getJitter_ddp), + RANGE_ARGS, + GET_JITTER_DDP_DOCSTRING); + + def("get_jitter", + [](PointProcess self, JitterMeasurement measurement, std::optional fromTime, std::optional toTime, double periodFloor, double periodCeiling, Positive 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 fromTime, std::optional toTime, double maximumPeriod) { + MelderCountAndFraction out = PointProcess_getCountAndFractionOfVoiceBreaks(self, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), maximumPeriod); + return std::make_tuple(out.count, out.numerator / out.denominator, out.numerator, out.denominator); + }, + "from_time"_a = std::nullopt, "to_time"_a = std::nullopt, "period_ceiling"_a = 0.02, + GET_COUNT_AND_FRACTION_OF_VOICE_BREAKS_DOCSTRING); + + // REAL_Point_Sound_getShimmer_local + def("get_shimmer_local", + SHIMMER_RANGE_FUNCTION(PointProcess_Sound_getShimmer_local), + SHIMMER_RANGE_ARGS, GET_SHIMMER_LOCAL_DOCSTRING); + + // REAL_Point_Sound_getShimmer_local_dB + def("get_shimmer_local_db", + SHIMMER_RANGE_FUNCTION(PointProcess_Sound_getShimmer_local_dB), + SHIMMER_RANGE_ARGS, + GET_SHIMMER_LOCAL_DB_DOCSTRING); + + // REAL_Point_Sound_getShimmer_apq3 + def("get_shimmer_apq3", + SHIMMER_RANGE_FUNCTION(PointProcess_Sound_getShimmer_apq3), + SHIMMER_RANGE_ARGS, + GET_SHIMMER_APQ3_DOCSTRING); + + // REAL_Point_Sound_getShimmer_apq5 + def("get_shimmer_apq5", + SHIMMER_RANGE_FUNCTION(PointProcess_Sound_getShimmer_apq5), + SHIMMER_RANGE_ARGS, + GET_SHIMMER_APQ5_DOCSTRING); + + // REAL_Point_Sound_getShimmer_apq11 + def("get_shimmer_apq11", + SHIMMER_RANGE_FUNCTION(PointProcess_Sound_getShimmer_apq11), + SHIMMER_RANGE_ARGS, + GET_SHIMMER_APQ11_DOCSTRING); + + // REAL_Point_Sound_getShimmer_dda + def("get_shimmer_dda", + SHIMMER_RANGE_FUNCTION(PointProcess_Sound_getShimmer_dda), + SHIMMER_RANGE_ARGS, + GET_SHIMMER_DDA_DOCSTRING); + + def("get_shimmer", + [](PointProcess self, Sound sound, ShimmerMeasurement measurement, std::optional fromTime, std::optional toTime, double periodFloor, double periodCeiling, Positive maximumPeriodFactor, Positive 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, + "time"_a, + GET_LOW_INDEX_DOCSTRING); + + // INTEGER_PointProcess_getHighIndex + def("get_high_index", + PointProcess_getHighIndex, + "time"_a, + GET_HIGH_INDEX_DOCSTRING); + + // INTEGER_PointProcess_getNearestIndex + def("get_nearest_index", + PointProcess_getNearestIndex, + "time"_a, + GET_NEAREST_INDEX_DOCSTRING); + + def("get_window_points", + [](PointProcess self, double tmin, double tmax) { + const MelderIntegerRange points = PointProcess_getWindowPoints(self, tmin, tmax); + return std::make_tuple(points.first, points.last); + }, + "from_time"_a, "to_time"_a, + GET_WINDOW_POINTS_DOCSTRING); + + // REAL_PointProcess_getInterval + def("get_interval", + &PointProcess_getInterval, + "time"_a, + GET_INTERVAL_DOCSTRING); + + // NEW1_PointProcesses_union + def("union", + PointProcesses_union, + "other"_a.none(false), + UNION_DOCSTRING); + + // NEW1_PointProcesses_intersection + def("intersection", + PointProcesses_intersection, + "other"_a.none(false), + INTERSECTION_DOCSTRING); + + // NEW1_PointProcesses_difference + def("difference", + PointProcesses_difference, + "other"_a.none(false), + DIFFERENCE_DOCSTRING); + + // MODIFY_PointProcess_addPoint + def("add_point", + PointProcess_addPoint, + "time"_a, + ADD_POINT_DOCSTRING); + + // MODIFY_PointProcess_addPoints + def("add_points", + // TODO Caster for constVEC? + [](PointProcess self, py::array_t 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); + + // MODIFY_PointProcess_removePoint + def("remove_point", + PointProcess_removePoint, + "point_number"_a, + REMOVE_POINT_DOCSTRING); + + // MODIFY_PointProcess_removePointNear + def("remove_point_near", + PointProcess_removePointNear, + "time"_a, + REMOVE_POINT_NEAR_DOCSTRING); + + // MODIFY_PointProcess_removePoints + def("remove_points", + PointProcess_removePoints, + "from_point_number"_a, "to_point_number"_a, + REMOVE_POINTS_DOCSTRING); + + // MODIFY_PointProcess_removePointsBetween + def("remove_points_between", + PointProcess_removePointsBetween, + "from_time"_a, "to_time"_a, + REMOVE_POINTS_BETWEEN_DOCSTRING); + + // MODIFY_PointProcess_fill + def("fill", + [](PointProcess self, std::optional fromTime, std::optional toTime, Positive period) { + return PointProcess_fill(self, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), period); + }, + "from_time"_a, "to_time"_a, "period"_a = 0.01, + FILL_DOCSTRING); + + // MODIFY_PointProcess_voice + def("voice", + args_cast<_, Positive<_>, Positive<_>>(PointProcess_voice), + "period"_a = 0.01, "maximum_voiced_period"_a = 0.02000000001, + VOICE_DOCSTRING); + + // MODIFY_Point_Sound_transplantDomain + def("transplant_domain", + [](PointProcess self, Sound src) { + self->xmin = src->xmin; + self->xmax = src->xmax; + }, + "sound"_a.none(false), + TRANSPLANT_DOMAIN_DOCSTRING); + + // NEW_PointProcess_to_IntervalTier + + // NEW_PointProcess_to_Matrix + def("to_matrix", + PointProcess_to_Matrix); + + // NEW_PointProcess_to_PitchTier + + // NEW_PointProcess_to_TextGrid + // TODO `std::vector`? + def("to_text_grid", + [](PointProcess self, const std::u32string tierNames, const std::u32string pointTiers) { + return TextGrid_create(self->xmin, self->xmax, tierNames.c_str(), pointTiers.c_str()); + }, + "tier_names"_a, "point_tiers"_a, + TO_TEXT_GRID_DOCSTRING); + + // NEW_PointProcess_to_TextGrid_vuv + def("to_text_grid_vuv", + PointProcess_to_TextGrid_vuv, + "maximum_period"_a = 0.02, "mean_period"_a = 0.01, + TO_TEXT_GRID_VUV_DOCSTRING); + + // NEW_PointProcess_to_TextTier + + // NEW_PointProcess_to_Sound_pulseTrain + def("to_sound_pulse_train", + PointProcess_to_Sound_pulseTrain, + "sampling_frequency"_a = 44100.0, "adaptation_factor"_a = 1.0, "adaptation_time"_a = 0.05, "interpolation_depth"_a = 2000, + TO_SOUND_PULSE_TRAIN_DOCSTRING); + + // NEW_PointProcess_to_Sound_phonation + def("to_sound_phonation", + PointProcess_to_Sound_phonation, + "sampling_frequency"_a = 44100.0, "adaptation_factor"_a = 1.0, "maximum_period"_a = 0.05, "open_phase"_a = 0.7,"collision_phase"_a = 0.03, "power1"_a = 3.0, "power2"_a = 4.0, + TO_SOUND_PHONATION_DOCSTRING); + + // NEW_PointProcess_to_Sound_hum + def("to_sound_hum", + PointProcess_to_Sound_hum, + TO_SOUND_HUM_DOCSTRING); + + // NEW_PointProcess_upto_IntensityTier + // NEW_PointProcess_upto_PitchTier + // NEW_PointProcess_upto_TextTier + // NEW1_PointProcess_Sound_to_AmplitudeTier_period + // NEW1_PointProcess_Sound_to_AmplitudeTier_point + // NEW1_PointProcess_Sound_to_Ltas + // NEW1_PointProcess_Sound_to_Ltas_harmonics +} + +}// namespace parselmouth diff --git a/src/parselmouth/PointProcess_docstrings.h b/src/parselmouth/PointProcess_docstrings.h new file mode 100644 index 000000000..c7d6945c5 --- /dev/null +++ b/src/parselmouth/PointProcess_docstrings.h @@ -0,0 +1,802 @@ +/* + * Copyright (C) 2021 Yannick Jadoul and contributors + * + * This file is part of Parselmouth. + * + * Parselmouth is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Parselmouth is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Parselmouth. If not, see + */ + +#pragma once +#ifndef INC_PARSELMOUTH_POINTPROCESS_DOCSTRINGS_H +#define INC_PARSELMOUTH_POINTPROCESS_DOCSTRINGS_H + +namespace parselmouth { + +constexpr auto CREATE_CLASS_DOCSTRING = R"(Praat PointProcess. + +A sequence object contain a sequence of points :math:`t_i` in time, defined +on a domain [``xmin``, ``xmax``]. The points are sorted in time, i.e., +:math:`t_i+1 > t_i`. + +Attributes +---------- +tmin : float, readonly + Starting time of the analysis domain in seconds +tmax : float, readonly + Ending time of the analysis domain in seconds + +See Also +-------- +:praat:`PointProcess` +)"; + +#define GET_RANGE_PARAMETER_DOCSTRING \ + "from_time : float, optional\n" \ + " The start time of the part of the `PointProcess` to be measured in\n" \ + " seconds. If `None`, all the points from ``tmin`` are included.\n" \ + "\n" \ + "end_time : float, optional\n" \ + " The end time of the part of the `PointProcess` to be measured in\n" \ + " seconds. If `None`, all the points to ``tmax`` are included.\n" \ + "\n" \ + "period_floor : float, default 0.0001\n" \ + " The shortest possible interval to be used in the computation in\n" \ + " seconds. If an interval is shorter than this, it will be ignored (and\n"\ + " the previous and next intervals will not be regarded as consecutive).\n"\ + " This setting will normally be very small.\n" \ + "\n" \ + "period_ceiling : float, default 0.02\n" \ + " The longest possible interval that to be used in the computation in\n" \ + " seconds. If an interval is longer than this, it will be ignored (and\n" \ + " the previous and next intervals will not be regarded as consecutive).\n"\ + " For example, if the minimum frequency of periodicity is 50 Hz, set\n" \ + " this setting to 0.02 seconds; intervals longer than that could be\n" \ + " regarded as voiceless stretches and will be ignored.\n" \ + "\n" \ + "maximum_period_factor : float, positive, default 1.3\n" \ + " The largest possible difference between consecutive intervals to\n" \ + " be used in the computation. If the ratio of the durations of two\n" \ + " consecutive intervals is greater than this, this pair of intervals\n" \ + " will be ignored (each of the intervals could still take part in the\n" \ + " computation in a comparison with its neighbour on the other side).\n" + +#define GET_SHIMMER_RANGE_PARAMETER_DOCSTRING \ + "sound : parselmouth.Sound\n" \ + " Sound object containing the samples to evaluate the amplitude.\n" \ + GET_RANGE_PARAMETER_DOCSTRING \ + "maximum_amplitude_factor : float, positive, default 1.6\n" \ + " Maximum amplitude factor.\n" \ + "\n" \ + "See Also\n" \ + "--------\n" \ + ":praat:`Voice 3. Shimmer`\n" + +constexpr auto CONSTRUCTOR_EMPTY_DOCSTRING = + R"(Create an empty PointProcess. + +Returns an empty PointProcess instance. + +Parameters +---------- +start_time : float + :math:`t_{min}`, the beginning of the time domain, in seconds. +end_time : float + :math:`t_{max}`, the end of the time domain, in seconds. + +See Also +-------- +:praat:`Create empty PointProcess...` +)"; + +constexpr auto CONSTRUCTOR_FILLED_DOCSTRING = + R"(Create a PointProcess filled with time points. + +Returns a new PointProcess instance that contains the time points +specified. + +Parameters +---------- +times : sequence-like of float + A sequence of time points in seconds to be added to the PointProcess. +start_time : float, optional + :math:`t_{min}`, the beginning of the time domain, in seconds. If + `None`, the smallest value from ``times`` is used. +end_time : float, optional + :math:`t_{max}`, the end of the time domain, in seconds. If `None`, + the largest value from ``times`` is used. +)"; + +constexpr auto CREATE_POISSON_PROCESS_DOCSTRING = + R"(Create a PointProcess instance with Poisson-distributed random time points. + +Returns a new PointProcess instance that represents a Poisson process. +A Poisson process is a stationary point process with a fixed density :math:`\lambda`, +which means that there are, on the average, :math:`\lambda` events per second. + +Parameters +---------- +start_time : float, default 0.0 + :math:`t_{min}`, the beginning of the time domain, in seconds. +end_time : float, default 1.0 + :math:`t_{max}`, the end of the time domain, in seconds. +density : float, default 100.0 + The average number of points per second. + +See Also +-------- +:praat:`Create Poisson process...` +)"; + +constexpr auto FROM_PITCH_DOCSTRING = + R"(Create PointProcess from Pitch object. + +Returns a new PointProcess instance which is generated from the specified +Pitch object. The acoustic periodicity contour stored in the Pitch object +is used as the frequency of an underlying point process (such as the +sequence of glottal closures in vocal-fold vibration). + +Parameters +---------- +pitch : parselmouth.Pitch + Pitch object defining the periodicity contour. + +See Also +-------- +:praat:`Pitch: To PointProcess` +)"; + +constexpr auto GET_NUMBER_OF_POINTS_DOCSTRING = + R"(Get the number of time points. + +Returns the total number of time points defined in the `PointProcess` +instance. +)"; + +constexpr auto N_POINTS_DOCSTRING = + R"(The total number of time points defined in the `PointProcess`. +)"; + +constexpr auto GET_NUMBER_OF_PERIODS_DOCSTRING = R"(Get the number of periods. + +Get the number of periods within the specified time range. + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_TIME_FROM_INDEX_DOCSTRING = + R"(Get time associated with the point number. + +Returns a time, specified by the time point number. If the number is not a +valid, it returns None. + +Parameters +---------- +point_number : int + Index (1-based) of the requested time point. +)"; + +constexpr auto GET_JITTER_LOCAL_DOCSTRING = + R"(Get jitter measure (MDVP Jitt). + +Returns the average absolute difference between consecutive periods, +divided by the average period. (MDVP Jitt: 1.040% as a threshold for +pathology). + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING R"( + +See Also +-------- +:praat:`PointProcess: Get jitter (local)...` +)"; + +constexpr auto GET_JITTER_LOCAL_ABSOLUTE_DOCSTRING = + R"(Get absolute jitter measure (MDVP Jita). + +Get the average absolute difference between consecutive periods, in +seconds (MDVP Jita: 83.200 μs as a threshold for pathology). + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING R"( + +See Also +-------- +:praat:`PointProcess: Get jitter (local, absolute)...` +)"; + +constexpr auto GET_JITTER_RAP_DOCSTRING = + R"(Get Relative Average Perturbation measure (MDVP RAP). + +Get the Relative Average Perturbation, the average absolute difference +between a period and the average of it and its two neighbours, divided by +the average period (MDVP: 0.680% as a threshold for pathology). + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING R"( + +See Also +-------- +:praat:`PointProcess: Get jitter (rap)...` +)"; + +constexpr auto GET_JITTER_PPQ5_DOCSTRING = + R"(Get 5-point PPQ measure (MDVP PPQ). + +Get the five-point Period Perturbation Quotient, the average absolute +difference between a period and the average of it and its four closest +neighbours, divided by the average period (MDVP PPQ, and gives 0.840% as a +threshold for pathology). + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING R"( + +See Also +-------- +:praat:`PointProcess: Get jitter (local, absolute)...` +)"; + +constexpr auto GET_JITTER_DDP_DOCSTRING = R"(Get Praat jitter measure. + +Get the average absolute difference between consecutive differences +between consecutive periods, divided by the average period. + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING R"( + +See Also +-------- +:praat:`PointProcess: Get jitter (local, absolute)...` +)"; + +constexpr auto GET_COUNT_AND_FRACTION_OF_VOICE_BREAKS_DOCSTRING = + R"(Get voice break analysis outputs. + +Returns a tuple, containing the outputs of the Praat voice break analysis: + + - the number of voice breaks + - the degree of voice breaks (MDVP DVB) + - the total duration of the voice breaks in seconds + - the duration of the analysed part of the signal in seconds + +Parameters +---------- +from_time : float, optional + The start time of the part of the PointProcess to be measured in + seconds. If `None`, all the points from ``tmin`` are included. + +end_time : float, optional + The end time of the part of the PointProcess to be measured in + seconds. If `None`, all the points to ``tmax`` are included. + +period_ceiling : float, default 0.02 + The longest possible interval that to be used in the computation in + seconds. If an interval is longer than this, it will be ignored (and + the previous and next intervals will not be regarded as consecutive). + For example, if the minimum frequency of periodicity is 50 Hz, set + this setting to 0.02 seconds; intervals longer than that could be + regarded as voiceless stretches and will be ignored. + +See Also +-------- +:praat:`Voice 1. Voice breaks` +)"; + +constexpr auto GET_SHIMMER_LOCAL_DOCSTRING = + R"(Get shimmer measure (MDVP Shim). + +Returns the average absolute difference between the amplitudes of +consecutive periods, divided by the average amplitude (MDVP Shim: 3.810% +as a threshold for pathology). + +Parameters +---------- +)" GET_SHIMMER_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_SHIMMER_LOCAL_DB_DOCSTRING = + R"(Get shimmer measure in dB (MDVP ShdB). + +Returns the average absolute base-10 logarithm of the difference between +the amplitudes of consecutive periods, multiplied by 20 (MDVP ShdB: +0.350 dB as a threshold for pathology). + +Parameters +---------- +)" GET_SHIMMER_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_SHIMMER_APQ3_DOCSTRING = + R"(Get 3-point APQ. + +Returns the three-point Amplitude Perturbation Quotient, the average +absolute difference between the amplitude of a period and the average of +the amplitudes of its neighbours, divided by the average amplitude. + +Parameters +---------- +)" GET_SHIMMER_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_SHIMMER_APQ5_DOCSTRING = + R"(Get 5-point APQ. + +Returns the five-point Amplitude Perturbation Quotient, the average +absolute difference between the amplitude of a period and the average of +the amplitudes of it and its four closest neighbours, divided by the +average amplitude. + +Parameters +---------- +)" GET_SHIMMER_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_SHIMMER_APQ11_DOCSTRING = + R"(Get 11-point APQ (MDVP APQ). + +Returns the 11-point Amplitude Perturbation Quotient, the average absolute +difference between the amplitude of a period and the average of the +amplitudes of it and its ten closest neighbours, divided by the average +amplitude (MDVP APQ: 3.070% as a threshold for pathology). + +Parameters +---------- +)" GET_SHIMMER_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_SHIMMER_DDA_DOCSTRING = + R"(Get Praat shimmer measure. + +Returns the average absolute difference between consecutive differences +between the amplitudes of consecutive periods (three times APQ3). + +Parameters +---------- +)" GET_SHIMMER_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_LOW_INDEX_DOCSTRING = + R"(Get nearest point below. + +Returns the 1-base index of the nearest point before or at the specified +time. If the point process contains no points or the specified time is +before the first point, returns 0. + +Parameters +---------- +time : float + The time from which a point is looked for in seconds. + +See Also +-------- +:praat:`PointProcess: Get low index...` +)"; + +constexpr auto GET_HIGH_INDEX_DOCSTRING = + R"(Get nearest point above. + +Returns the 1-base index of the nearest point at or after the specified +time. If the point process contains no points or the specified time is +after the last point, returns 0. + +Parameters +---------- +time : float + The time from which a point is looked for in seconds. + +See Also +-------- +:praat:`PointProcess: Get high index...` +)"; + +constexpr auto GET_NEAREST_INDEX_DOCSTRING = + R"(Get nearest point. + +Returns the 1-base index of the point nearest to the specified time. If +the point process contains no points or the specified time is before the +first point or after the last point, returns 0. + +Parameters +---------- +time : float + The time from which a point is looked for in seconds. + +See Also +-------- +:praat:`PointProcess: Get nearest index...` +)"; + +constexpr auto GET_WINDOW_POINTS_DOCSTRING = + R"(Get included point range. + +Returns the 1-base starting and ending indices of the time points inside +the specified time range. + +Parameters +---------- +from_time : float + The starting time in seconds. + +to_time : float + The ending time in seconds. + +Returns +------- +tuple of float + (start, end) +)"; + +constexpr auto GET_INTERVAL_DOCSTRING = + R"(Get period duration. + +Returns the duration of the period interval around a specified time. + +Parameters +---------- +time : float + The time from which a point is looked for in seconds + +See Also +-------- +:praat:`PointProcess: Get interval...` +)"; + +constexpr auto GET_MEAN_PERIOD_DOCSTRING = + R"(Get mean period. + +Returns the average period in the specified time range. + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING; + +constexpr auto GET_STDEV_PERIOD_DOCSTRING = + R"(Get standard deviation of periods. + +Returns the standard deviation of the periods in the specified time range. + +Parameters +---------- +)" GET_RANGE_PARAMETER_DOCSTRING; + +constexpr auto UNION_DOCSTRING = + R"(Combine with another time process. + +Returns a new `PointProcess` instance containing all the points of the two +original point processes, sorted by time. + +Parameters +---------- +other : parselmouth.PointProcess + The other PointProcess object to combine with ``self``. + +See Also +-------- +:praat:`PointProcesses: Union` +)"; + +constexpr auto INTERSECTION_DOCSTRING = + R"(Intersect with another time process. + +Returns a new `PointProcess` instance containing only those points that +occur in both ``self`` and ``other`` `PointProcess` objects. + +Parameters +---------- +other : parselmouth.PointProcess + The other PointProcess object to intersect with ``self``. + +See Also +-------- +:praat:`PointProcesses: Intersection` +)"; + +constexpr auto DIFFERENCE_DOCSTRING = + R"(Subtract another time process. + +Returns a new `PointProcess` instance containing only those points of the +``self`` point process that do not occur in the ``other`` point process. + +Parameters +---------- +other : parselmouth.PointProcess + The other `PointProcess` object to subtract from ``self``. + +See Also +-------- +:praat:`PointProcesses: Difference` +)"; + +constexpr auto ADD_POINT_DOCSTRING = + R"(Add time point. + +Add the specified time point. If the point already exists in the point +process, nothing happens. + +Parameters +---------- +time : float + Time point to be added. + +See Also +-------- +:praat:`PointProcess: Add point...` +)"; + +constexpr auto ADD_POINTS_DOCSTRING = + R"(Add time points. + +Add the specified time points. If any of the points already exists in the +point process, nothing happens for that point. + +Parameters +---------- +times : numpy.ndarray of float + Array of time points to be added. +)"; + +constexpr auto REMOVE_POINT_DOCSTRING = + R"(Remove time point. + +Remove the specified time point. (e.g., if ``point_number`` is 3, the third +point is removed) It does nothing if index is less than 1 or greater than +the number of points in the point process. + +Parameters +---------- +point_number : int + 1-based index of time point to remove. + +See Also +-------- +:praat:`PointProcess: Remove point...` +)"; + +constexpr auto REMOVE_POINT_NEAR_DOCSTRING = + R"(Remove nearest time point. + +Remove a time point nearest to the specified time. It does nothing if +there are no points in the point process. + +Parameters +---------- +time : float + Time point to be removed. + +See Also +-------- +:praat:`PointProcess: Remove point near...` +)"; + +constexpr auto REMOVE_POINTS_DOCSTRING = + R"(Remove a range of time points. + +Remove all the time point that originally fell in the range +[from_point_number, to_point_number]. + +Parameters +---------- +from_point_number : int + Starting 1-based time point index. + +to_point_number : int + Ending 1-based time point index. + +See Also +-------- +:praat:`PointProcess: Remove points...` +)"; + +constexpr auto REMOVE_POINTS_BETWEEN_DOCSTRING = + R"(Remove time points in a time range. + +Remove all points that originally fell in the domain [from_time, to_time], +including the edges. + +Parameters +---------- +from_time : float + Starting time in seconds. + +to_time : float + Ending time in seconds. + +See Also +-------- +:praat:`PointProcess: Remove points between...` +)"; + +constexpr auto FILL_DOCSTRING = + R"(Add equispaced time points. + +Add equispaced time points between the specified time range separated by +the specified period. + +Parameters +---------- +from_time : float, optional + Starting time in seconds. + +to_time : float, optional + Ending time in seconds. + +period : float, default 0.01 + Time interval in seconds. +)"; + +constexpr auto VOICE_DOCSTRING = + R"(Add equispaced time points in unvoiced intervals. + +Add equispaced time points separated by the specified period over all +existing periods longer than ``maximum_voiced_period``. + +Parameters +---------- +period : float, default 0.01 + Time interval in seconds. + +maximum_voiced_period : float, default 0.02000000001 + Time period longer than this is considered unvoiced, in seconds. +)"; + +constexpr auto TRANSPLANT_DOMAIN_DOCSTRING = + R"(Copy time domain. + +Copy the time domain of the specified `Sound` object. + +Parameters +---------- +sound : parselmouth.Sound + Source sound object. +)"; + +constexpr auto TO_TEXT_GRID_DOCSTRING = + R"(Convert into a TextGrid. + +PointProcess object is converted to a sound object by genering a pulse at +every point in the point process. This pulse is filtered at the Nyquist +frequency of the resulting Sound by converting it into a sampled sinc +function. + +Parameters +---------- +tier_names : str + A list of the names of the tiers that you want to create, separated by + spaces. + +point_tiers : str + A list of the names of the tiers that you want to be point tiers; the + rest of the tiers will be interval tiers. + +See also +-------- +:praat:`PointProcess: To TextGrid...` +)"; + +constexpr auto TO_TEXT_GRID_VUV_DOCSTRING = + R"(Convert into a Sound with voiced/unvoiced information. + +PointProcess object is converted to a sound object with voiced/unvoiced +information. + +Parameters +---------- +maximum_period : float, default 0.02 + The maximum interval that will be consider part of a larger voiced + interval. + +mean_period : float, default 0.01 + Half of this value will be taken to be the amount to which a voiced + interval will extend beyond its initial and final points. Mean period + should be less than Maximum period, or you may get intervals with + negative durations. + +See also +-------- +:praat:`PointProcess: To TextGrid (vuv)...` +)"; + +constexpr auto TO_SOUND_PULSE_TRAIN_DOCSTRING = + R"(Convert into a Sound with pulses + +PointProcess object is converted to a sound object with a series of pulses, +each generated at every point in the point process. This pulse is filtered +at the Nyquist frequency of the resulting Sound by converting it into a +sampled sinc function. + +Parameters +---------- +sampling_frequency : float, default 44100.0 + The sampling frequency of the resulting `Sound` object. + +adaptation_factor : float, default 1.0 + The factor by which a pulse height will be multiplied if the pulse time + is not within ``adaptation_time`` from the pre-previous pulse, and by + which a pulse height will again be multiplied if the pulse time is not + within ``adaptation_time`` from the previous pulse. This factor is + against abrupt starts of the pulse train after silences, and is 1.0 if + you do want abrupt starts after silences. + +adaptation_time : float, default 0.05 + The minimal period that will be considered a silence. + +interpolation_depth : int, default 2000 + The extent of the :math:`sinc` function to the left and to the right of + the peak. + +See also +-------- +:praat:`PointProcess: To Sound (pulse train)...` +)"; + +constexpr auto TO_SOUND_PHONATION_DOCSTRING = + R"(Convert into a glottal waveform `Sound` object. + +PointProcess object is converted to a sound object containing glottal +waveform at every point in the point process. Its shape depends on the +settings ``power1`` and ``power2`` according to the formula. + +.. math:: U(x) = x^{power1} - x^{power2} + +where :math:`x` is a normalized time that runs from 0 to 1 and :math:`U(x)` +is the normalized glottal flow in arbitrary units (the real unit is +:math:`m^3/s`). + +Parameters +---------- +sampling_frequency : float, default 44100.0 + The sampling frequency of the resulting `Sound` object. + +adaptation_factor : float, default 1.0 + The factor by which a pulse height will be multiplied if the pulse time + is not within Maximum period from the previous pulse, and by which a + pulse height will again be multiplied if the previous pulse time is not + within ``maximum_period`` from the pre-previous pulse. This factor is + against abrupt starts of the pulse train after silences, and is 1.0 if + you do want abrupt starts after silences. + +maximum_period : float, default 0.05 + The minimal period that will be considered a silence in seconds. + +open_phase: float, default 0.7 + Fraction of a period when the glottis is open. + +collision_phase : float, default 0.03 + Decay factor to ease the abrupt collision at closure. + +power1 : float, default 3.0 + First glottal flow shape coefficient. + +power2 : float, default 4.0 + Second glottal flow shape coefficient. + +See also +-------- +:praat:`PointProcess: To Sound (phonation)...` +)"; + +constexpr auto TO_SOUND_HUM_DOCSTRING = + R"(Convert into a Sound with hum sound + +PointProcess object is converted to a sound object with hum sound. A Sound +is created from pulses, followed by filtered by a sequence of second-order +filters that represent five formants. + +See also +-------- +:praat:`PointProcess: To Sound (hum)...` +)"; + +} // namespace parselmouth + +#endif // INC_PARSELMOUTH_POINTPROCESS_DOCSTRINGS_H diff --git a/src/parselmouth/Sound.cpp b/src/parselmouth/Sound.cpp index 7edf5f3b7..ddbe16a12 100644 --- a/src/parselmouth/Sound.cpp +++ b/src/parselmouth/Sound.cpp @@ -21,6 +21,7 @@ #include "Parselmouth.h" #include "TimeClassAspects.h" +#include "Sound_docstrings.h" #include "utils/SignatureCast.h" #include "utils/praat/MelderUtils.h" @@ -37,6 +38,7 @@ #include #include #include +#include #include #include @@ -56,6 +58,20 @@ PraatCollection referencesToPraatCollection(const Container &container) { // TOD return collection; } +struct Channel { + integer getValue(integer nChannels) { + if (value > nChannels) + throw py::value_error(fmt::format("Channel number ({}) is larger than number of available channels ({}).", value, nChannels)); + return value; + } + integer value; + static const Channel LEFT; + static const Channel RIGHT; +}; + +constexpr Channel Channel::LEFT = Channel{1}; +constexpr Channel Channel::RIGHT = Channel{2}; + } // namespace enum class SoundFileFormat { // TODO Nest within Sound? @@ -180,11 +196,43 @@ PRAAT_ENUM_BINDING(ToHarmonicityMethod) { make_implicitly_convertible_from_string(*this); } -PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { +CLASS_BINDING(Channel, Channel) +BINDING_CONSTRUCTOR(Channel, "Channel") +BINDING_INIT(Channel) { + def(py::init([](integer value) { + if (value < 0) + throw py::value_error("Channel number should be positive or zero."); + return Channel{value}; + })); + + def(py::init([](std::string value) { + for (auto &c : value) + c = std::toupper(c); + if (value == "LEFT") + return Channel{Channel::LEFT}; + else if (value == "RIGHT") + return Channel{Channel::RIGHT}; + else + throw py::value_error("Channel string can only be 'left' or 'right'."); + })); + + def("__repr__", [](const Channel &self) { return fmt::format("Channel({})", self.value); }); + + def_readonly("value", &Channel::value); + + attr("LEFT") = Channel::LEFT; + attr("RIGHT") = Channel::RIGHT; + + py::implicitly_convertible(); + py::implicitly_convertible(); +} + +PRAAT_CLASS_BINDING(Sound) { addTimeFrameSampledMixin(*this); NESTED_BINDINGS(ToPitchMethod, - ToHarmonicityMethod) + ToHarmonicityMethod, + Channel) using signature_cast_placeholder::_; @@ -202,7 +250,7 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { auto nx = values.shape(ndim - 1); auto ny = ndim == 2 ? values.shape(0) : 1; if (ndim == 2 && ny > nx) - PyErr_WarnEx(PyExc_RuntimeWarning, ("Number of channels (" + std::to_string(ny) + ") is greater than number of samples (" + std::to_string(nx) + "); note that the shape of the `values` array is interpreted as (n_channels, n_samples).").c_str(), 1); + PyErr_WarnEx(PyExc_RuntimeWarning, fmt::format("Number of channels ({}) is greater than number of samples ({}); note that the shape of the `values` array is interpreted as (n_channels, n_samples).", ny, nx).c_str(), 1); auto result = Sound_create(ny, startTime, startTime + nx / samplingFrequency, nx, 1.0 / samplingFrequency, startTime + 0.5 / samplingFrequency); @@ -322,12 +370,11 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { // TODO Minimum & maximum (Vector?) - def("get_nearest_zero_crossing", // TODO Channel is CHANNEL - [](Sound self, double time, long channel) { - if (channel > self->ny) channel = 1; - return Sound_getNearestZeroCrossing(self, time, channel); + def("get_nearest_zero_crossing", + [](Sound self, double time, Channel channel) { + return Sound_getNearestZeroCrossing (self, time, channel.getValue(self->ny)); }, - "time"_a, "channel"_a = 1); + "time"_a, "channel"_a = Channel::LEFT); // TODO Get mean (Vector?) @@ -409,6 +456,7 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { def("convert_to_stereo", &Sound_convertToStereo); + // NEWMANY_Sound_extractAllChannels def("extract_all_channels", [](Sound self) { std::vector result; @@ -419,25 +467,16 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { return result; }); - def("extract_channel", // TODO Channel POSITIVE? (Actually CHANNEL; >= 1, but does not always have intended result (e.g., Set value at sample...)) - &Sound_extractChannel, - "channel"_a); - - def("extract_channel", // TODO Channel enum type? - [](Sound self, std::string channel) { - std::transform(channel.begin(), channel.end(), channel.begin(), tolower); - if (channel == "left") - return Sound_extractChannel(self, 1); - if (channel == "right") - return Sound_extractChannel(self, 2); - Melder_throw(U"'channel' can only be 'left' or 'right'"); // TODO Melder_throw or throw PraatError ? + def("extract_channel", + [](Sound self, Channel channel) { + return Sound_extractChannel(self, channel.value); // Will check the range of the channel itself. }); def("extract_left_channel", - [](Sound self) { return Sound_extractChannel(self, 1); }); + [](Sound self) { return Sound_extractChannel(self, Channel::LEFT.value); }); def("extract_right_channel", - [](Sound self) { return Sound_extractChannel(self, 2); }); + [](Sound self) { return Sound_extractChannel(self, Channel::RIGHT.value); }); def("extract_part", // TODO Something for std::optional for from and to in Sounds? [](Sound self, std::optional fromTime, std::optional toTime, kSound_windowShape windowShape, Positive relativeWidth, bool preserveTimes) { return Sound_extractPart(self, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), windowShape, relativeWidth, preserveTimes); }, @@ -481,7 +520,7 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { case ToPitchMethod::SHS: return callMethod("to_pitch_shs"); } - return py::none(); // Unreachable + throw py::value_error("Invalid ToPitchMethod value"); }, "method"_a); @@ -527,7 +566,7 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { case ToHarmonicityMethod::GNE: return callMethod("to_harmonicity_gne"); } - return py::none(); // Unreachable + throw py::value_error("Invalid ToPitchMethod value"); }, "method"_a = ToHarmonicityMethod::CC); @@ -599,6 +638,44 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) { }, "number_of_coefficients"_a = 12, "window_length"_a = 0.015, "time_step"_a = 0.005, "firstFilterFrequency"_a = 100.0, "distance_between_filters"_a = 100.0, "maximum_frequency"_a = std::nullopt); + // NEW_Sound_to_PointProcess_extrema + def("to_point_process_extrema", + [](Sound self, Channel channel, bool includeMaxima, bool includeMinima, kVector_peakInterpolation peakInterpolationType) { + return Sound_to_PointProcess_extrema(self, channel.getValue(self->ny), peakInterpolationType, includeMaxima, includeMinima); + }, + "channel"_a = Channel::LEFT, "include_maxima"_a = true, "include_minima"_a = false, + "interpolation"_a = kVector_peakInterpolation::SINC70, + TO_POINT_PROCESS_EXTREMA_DOCSTRING); + + // NEW_Sound_to_PointProcess_periodic_cc + def("to_point_process_periodic", + [](Sound self, float minimumPitch, float maximumPitch) { + if (maximumPitch <= minimumPitch) + Melder_throw(U"Your maximum pitch should be greater than your minimum pitch."); + return Sound_to_PointProcess_periodic_cc(self, minimumPitch, maximumPitch); + }, + "minimum_pitch"_a = 75.0, "maximum_pitch"_a = 600.0, + TO_POINT_PROCESS_PERIODIC_DOCSTRING); + + // NEW_Sound_to_PointProcess_periodic_peaks + def("to_point_process_periodic_peaks", + [](Sound self, float minimumPitch, float maximumPitch, bool includeMaxima, bool includeMinima) { + if (maximumPitch <= minimumPitch) + Melder_throw(U"Your maximum pitch should be greater than your minimum pitch."); + return Sound_to_PointProcess_periodic_peaks(self, minimumPitch, maximumPitch, includeMaxima, includeMinima); + }, + "minimum_pitch"_a = 75.0, "maximum_pitch"_a = 600.0, + "include_maxima"_a = true, "include_minima"_a = false, + TO_POINT_PROCESS_PERIODIC_PEAKS_DOCSTRING); + + // NEW_Sound_to_PointProcess_zeroes + def("to_point_process_zeros", + [](Sound self, Channel channel, bool includeRaisers, bool includeFallers) { + return Sound_to_PointProcess_zeroes(self, channel.getValue(self->ny), includeRaisers, includeFallers); + }, + "channel"_a = Channel::LEFT, "include_raisers"_a = true, + "include_fallers"_a = false, TO_POINT_PROCESS_ZEROS_DOCSTRING); + // TODO For some reason praat_David_init.cpp also still contains Sound functionality // TODO Still a bunch of Sound in praat_LPC_init.cpp } diff --git a/src/parselmouth/Sound_docstrings.h b/src/parselmouth/Sound_docstrings.h index 800d848b2..58ab13f10 100644 --- a/src/parselmouth/Sound_docstrings.h +++ b/src/parselmouth/Sound_docstrings.h @@ -83,6 +83,106 @@ See Also :praat:`Sound files 4. Files that Praat can write` )"; +constexpr auto TO_POINT_PROCESS_EXTREMA_DOCSTRING = +R"(Create PointProcess by peak picking. + +Returns a new `PointProcess` instance by peak-picking the acoustic sample +without pitch estimation. + +Parameters +---------- +channel : {"LEFT", "RIGHT"}, default "LEFT" (first channel) + Sound channel to process. + +include_maxima : bool, default True + True to include the absolute maximum. + +include_minima : bool, default False + True to include the absolute minimum. + +interpolation : {"NONE", "PARABOLIC", "CUBIC", "SINC70", "SINC700"}, + default: "SINC70" + Peak-picking interpolation method. + +See Also +-------- +parselmouth.PointProcess +parselmouth.Sound.to_pitch_ac, parselmouth.Pitch.to_point_process_peaks +)"; + +constexpr auto TO_POINT_PROCESS_PERIODIC_DOCSTRING = + R"(Create PointProcess by cross-correlation. + +Returns a new PointProcess instance using the pitch estimation algorithm in +:func:`~parselmouth.Sound.to_pitch_cc` and the voice cycle detection +algorithm in :func:`~parselmouth.Pitch.to_point_process_cc`. + +Parameters +---------- +minimum_pitch : float, default 75.0 + Minimum fundamental frequency to be considered. + +maximum_pitch : float, default 600.0 + Maximum fundamental frequency to be considered. + +See Also +-------- +:praat:`Sound: To PointProcess (periodic, cc)...` +parselmouth.PointProcess +parselmouth.Sound.to_pitch_cc, parselmouth.Pitch.to_point_process_peaks +)"; + +constexpr auto TO_POINT_PROCESS_PERIODIC_PEAKS_DOCSTRING = + R"(Create a `PointProcess` by peak picking with pitch estimation. + +Returns a new `PointProcess` instance using the pitch estimation algorithm +in `Sound.to_pitch_cc` and the voice cycle detection algorithm in +`Pitch.to_point_process_peaks`. + +Parameters +---------- +minimum_pitch : float, default 75.0 + Minimum fundamental frequency to be considered + +maximum_pitch : float, default 600.0 + Maximum fundamental frequency to be considered + +include_maxima : bool, default True + True to include the absolute maximum + +include_minima : bool, default False + True to include the absolute minimum + +See Also +-------- +:praat:`Sound: To PointProcess (periodic, peaks)...` +parselmouth.PointProcess +parselmouth.Sound.to_pitch_cc, parselmouth.Pitch.to_point_process_peaks +)"; + +constexpr auto TO_POINT_PROCESS_ZEROS_DOCSTRING = + R"(Create a `PointProcess` by zero-crossing detection. + +Returns a new `PointProcess` instance by detecting rising or falling edges +in the sound waveform. Linear interpolation is used to refine the timing +of the crossing. + +Parameters +---------- +channel : {"LEFT", "RIGHT"}, default "LEFT" (first channel) + Sound channel to process + +include_raisers : bool, default True + True to detect the rising edges + +include_fallers : bool, default False + True to detect the falling edges + +See Also +-------- +parselmouth.PointProcess +)"; + } // namespace parselmouth #endif // INC_PARSELMOUTH_SOUND_DOCSTRINGS_H diff --git a/src/parselmouth/Vector.cpp b/src/parselmouth/Vector.cpp index 542d10716..b603d68a7 100644 --- a/src/parselmouth/Vector.cpp +++ b/src/parselmouth/Vector.cpp @@ -42,6 +42,17 @@ PRAAT_ENUM_BINDING(ValueInterpolation) { make_implicitly_convertible_from_string(*this); } +PRAAT_ENUM_BINDING(PeakInterpolation) +{ + value("NONE", kVector_peakInterpolation::NONE); + value("PARABOLIC", kVector_peakInterpolation::PARABOLIC); + value("CUBIC", kVector_peakInterpolation::CUBIC); + value("SINC70", kVector_peakInterpolation::SINC70); + value("SINC700", kVector_peakInterpolation::SINC700); + + make_implicitly_convertible_from_string(*this); +} + PRAAT_CLASS_BINDING(Vector) { using signature_cast_placeholder::_; diff --git a/tests/resource_fixtures.py b/tests/resource_fixtures.py index 82c733b5e..43eb15c1e 100644 --- a/tests/resource_fixtures.py +++ b/tests/resource_fixtures.py @@ -45,17 +45,22 @@ def pitch(sound): yield sound.to_pitch() +@pytest.fixture +def point_process(pitch): + yield pitch.to_point_process() + + @pytest.fixture def spectrogram(sound): yield sound.to_spectrogram() -@combined_fixture('intensity', 'pitch', 'spectrogram', 'sound') +@combined_fixture('sound', 'intensity', 'pitch', 'spectrogram') def sampled(request): yield request.param -@combined_fixture('sampled') +@combined_fixture('sampled', 'point_process') def thing(request): yield request.param diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 731c15e4e..6b2fa6e7a 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -51,6 +51,8 @@ def test_docstring_formatting(): for _, docstring in all_docstrings(parselmouth): assert '\t' not in docstring assert not docstring.startswith("\n") + for line in docstring.splitlines(): + assert not line.endswith(" ") def test_docstring_line_lengths(): diff --git a/tests/test_point_process.py b/tests/test_point_process.py new file mode 100644 index 000000000..3ac71ed2f --- /dev/null +++ b/tests/test_point_process.py @@ -0,0 +1,191 @@ +import pytest +import numpy as np + +import parselmouth +from parselmouth.praat import call + + +def test_create_empty_process(): + assert parselmouth.PointProcess(0, 1) == parselmouth.PointProcess(0.0, 1.0) + assert parselmouth.PointProcess(0, 1) != parselmouth.PointProcess(0, 2) + with pytest.raises(parselmouth.PraatError, match=r'Your end time \(0\) should not be less than your start time \(1\).'): + parselmouth.PointProcess(1.0, 0) + assert parselmouth.PointProcess(end_time=1, start_time=0) == parselmouth.PointProcess(0, 1) + + +def test_create_process_from_array(): + # create a new process with prefilled values + t = [0.1, 0.4, 1.2, 2.4, 0.5] + process = parselmouth.PointProcess(t) + assert process.xmin == min(t) + assert process.xmax == max(t) + assert np.array_equal(process, np.sort(t)) + + # create another process but explicitly set the time domain + process = parselmouth.PointProcess(t, start_time=0.0, end_time=1.0) + assert process.xmin == 0.0 + 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) + assert isinstance(poisson_process, parselmouth.PointProcess) + assert poisson_process != parselmouth.PointProcess.create_poisson_process(0, 1, 100) + + +def test_from_sound(sound): + # tests both constructor and static from_pitch() + sound.to_point_process_extrema("LEFT", True, False, "SINC70") + sound.to_point_process_periodic(75.0, 600.0) + sound.to_point_process_periodic_peaks(75.0, 600.0, True, False) + + +def test_from_pitch(pitch, sound): + # tests both constructor and static from_pitch() + pitch.to_point_process() + pitch.to_point_process(sound) + pitch.to_point_process(sound, method="cc") + pitch.to_point_process(sound, method="peaks") + with pytest.raises(ValueError): + pitch.to_point_process(sound, method="invalid") + + pitch.to_point_process_cc(sound) + pitch.to_point_process_peaks(sound, False, True) + + +def test_points(point_process): + n = call(point_process, "Get number of points") + assert point_process.get_number_of_points() == n + assert point_process.n_points == n + + +def test_periods(point_process): + # TODO PointProcess.get_periods() ? + default_argument_values = (0.0, 0.0, 0.0001, 0.02, 1.3) + assert point_process.get_number_of_periods() == call(point_process, "Get number of periods", *default_argument_values) + assert point_process.get_mean_period() == call(point_process, "Get mean period", *default_argument_values) + assert point_process.get_stdev_period() == call(point_process, "Get stdev period", *default_argument_values) + + +def test_sequence(): + sequence = sorted(np.random.random(10)) + point_process = parselmouth.PointProcess(sequence) + assert len(point_process) == len(sequence) + for i in range(len(sequence)): + assert point_process[i] == sequence[i] + assert set(point_process) == set(sequence) + + +def test_get_time_from_index(point_process): + x = np.array(point_process) # TODO PointProcess.values + index = x.size // 3 + xi = point_process.get_time_from_index(index + 1) + assert x[index] == xi + + +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() == 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) == 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): + assert len(point_process.get_count_and_fraction_of_voice_breaks()) == 4 + + +def test_indexes(point_process): + t, n = np.array(point_process), len(point_process) + i0, i1 = n // 3, (2 * n) // 3 + t0, t1 = (t[i0] + t[i0 - 1]) / 2, (t[i1] + t[i1 + 1]) / 2 + j0, j1 = point_process.get_window_points(t0, t1) + assert i0 + 1 == j0 and i1 + 1 == j1 + + low, high = point_process.get_low_index(0.5), point_process.get_high_index(0.5) + assert point_process.get_nearest_index(0.5) in [low, high] + assert point_process.get_interval(0.5) == point_process[high-1] - point_process[low-1] + + +def test_modifications(point_process): + n = len(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, 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] + point_process.remove_point(1) + assert len(point_process) == n + 9 + assert point_process[0] != p + + n = np.random.randint(1, len(point_process) - 1) + p = point_process[n] + point_process.remove_point_near((point_process[n-1] + 2 * p) / 3) + assert not np.any(np.array(point_process) == p) + + n = len(point_process) + i, j = n // 3, (2 * n) // 3 + remaining = np.delete(point_process, slice(i, j)) + point_process.remove_points(i + 1, j) + assert set(point_process) == set(remaining) + + point_process.remove_points_between(point_process.centre_time, point_process.tmax) + assert not np.any(np.array(point_process) > point_process.centre_time) + + +def test_combinations(): + a = np.arange(0.1, 0.9, 0.2) + b = np.arange(0.1, 0.5, 0.1) + point_process_a = parselmouth.PointProcess(a) + point_process_b = parselmouth.PointProcess(b) + + union = point_process_a.union(point_process_b) + assert np.array_equal(np.array(union), np.union1d(a, b)) + assert union.xmin == min(point_process_a.xmin, point_process_b.xmin) + assert union.xmax == max(point_process_a.xmax, point_process_b.xmax) + + intersection = point_process_a.intersection(point_process_b) + assert np.array_equal(np.array(intersection), np.intersect1d(a, b)) + assert intersection.xmin == max(point_process_a.xmin, point_process_b.xmin) + assert intersection.xmax == min(point_process_a.xmax, point_process_b.xmax) + + difference = point_process_a.difference(point_process_b) + assert np.array_equal(np.array(difference), np.setdiff1d(a, b)) + assert difference.xmin == point_process_a.xmin + assert difference.xmax == point_process_a.xmax + + +def test_voice(point_process): + new_point_process = parselmouth.PointProcess(0.0, 2.0) + new_point_process.fill(0.5, 1.5, 0.01) + assert len(new_point_process) == 100 + assert np.array_equal(np.array(new_point_process), np.arange(0.5, 1.5, 0.01)) + + point_process.voice() + _, voice_breaks_fraction, _, _ = point_process.get_count_and_fraction_of_voice_breaks() + assert voice_breaks_fraction == 0 diff --git a/tests/test_sound.py b/tests/test_sound.py index 83fea4561..ea148d224 100644 --- a/tests/test_sound.py +++ b/tests/test_sound.py @@ -52,7 +52,7 @@ def test_from_numpy_array_stereo(sampling_frequency): sound = parselmouth.Sound(np.vstack((sine_values, cosine_values))[::-1,1::3], sampling_frequency=sampling_frequency) assert np.all(sound.values == [cosine_values[1::3], sine_values[1::3]]) - with pytest.warns(RuntimeWarning, match=r'Number of channels \([0-9]+\) is greater than number of samples \([0-9]+\)'): + with pytest.warns(RuntimeWarning, match=r"Number of channels \([0-9]+\) is greater than number of samples \([0-9]+\)"): parselmouth.Sound(np.vstack((sine_values, cosine_values)).T, sampling_frequency=sampling_frequency) @@ -62,3 +62,27 @@ def test_from_scalar(sampling_frequency): with pytest.raises(ValueError, match="Cannot create Sound from a single 0-dimensional number"): parselmouth.Sound(3.14159, sampling_frequency=sampling_frequency) + +@pytest.mark.filterwarnings('ignore:Number of channels .* is greater than number of samples') +def test_channel_type(): + n_channels = 10 + sound = parselmouth.Sound(np.arange(n_channels)[:,None]) + assert sound.n_channels == n_channels + for i in range(n_channels): + assert np.array_equal(sound.extract_channel(parselmouth.Sound.Channel(i + 1)).values, [[i]]) + assert np.array_equal(sound.extract_channel(i + 1).values, [[i]]) + with pytest.raises(TypeError, match=r"extract_channel\(\): incompatible function arguments"): + sound.extract_channel(-1) + assert np.isnan(sound.get_nearest_zero_crossing(0, channel=1)) + with pytest.raises(ValueError, match=r"Channel number (.*) is larger than number of available channels (.*)\."): + assert sound.get_nearest_zero_crossing(0, channel=n_channels + 1) + assert np.array_equal(sound.extract_channel('LEFT').values, [[0]]) + assert np.array_equal(sound.extract_channel('right').values, [[1]]) + with pytest.raises(TypeError, match=r"extract_channel\(\): incompatible function arguments"): + sound.extract_channel('MIDDLE') + + assert parselmouth.Sound.Channel(42).value == 42 + with pytest.raises(ValueError, match=r"Channel number should be positive or zero\."): + parselmouth.Sound.Channel(-1) + with pytest.raises(ValueError, match=r"Channel string can only be 'left' or 'right'\."): + parselmouth.Sound.Channel('MIDDLE, I said')