Skip to content

Commit

Permalink
Add Sound.Channel utility class
Browse files Browse the repository at this point in the history
  • Loading branch information
YannickJadoul authored and tikuma-lsuhsc committed Oct 3, 2024
1 parent 16e71fa commit f121d88
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 47 deletions.
4 changes: 2 additions & 2 deletions src/parselmouth/PointProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,10 @@ PRAAT_CLASS_BINDING(PointProcess) {

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); };
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);
return call(PointProcess_getJitter_local);
case JitterMeasurement::LOCAL_ABSOLUTE:
return call(PointProcess_getJitter_local_absolute);
case JitterMeasurement::RAP:
Expand Down
78 changes: 34 additions & 44 deletions src/parselmouth/Sound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,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?
Expand Down Expand Up @@ -330,7 +344,7 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) {
if (channel > self->ny) channel = 1;
return Sound_getNearestZeroCrossing(self, time, channel);
},
"time"_a, "channel"_a = 1);
"time"_a, "channel"_a = Channel::LEFT);

// TODO Get mean (Vector?)

Expand Down Expand Up @@ -412,6 +426,7 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) {
def("convert_to_stereo",
&Sound_convertToStereo);

// NEWMANY_Sound_extractAllChannels
def("extract_all_channels",
[](Sound self) {
std::vector<autoSound> result;
Expand All @@ -422,25 +437,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<double> for from and to in Sounds?
[](Sound self, std::optional<double> fromTime, std::optional<double> toTime, kSound_windowShape windowShape, Positive<double> relativeWidth, bool preserveTimes) { return Sound_extractPart(self, fromTime.value_or(self->xmin), toTime.value_or(self->xmax), windowShape, relativeWidth, preserveTimes); },
Expand Down Expand Up @@ -603,55 +609,39 @@ PRAAT_CLASS_BINDING(Sound, SOUND_DOCSTRING) {
"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);

// NEW_Sound_to_PointProcess_extrema
def(
"to_point_process_extrema",
[](Sound self, Channel channel, bool includeMaxima, bool includeMinima,
kVector_peakInterpolation peakInterpolationType) {
int ch = static_cast<int>(channel);
return Sound_to_PointProcess_extrema(self, ch > self->ny ? 1 : ch,
peakInterpolationType,
includeMaxima, includeMinima);
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",
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);
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) {
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);
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 ch, bool includeRaisers, bool includeFallers) {
int channel = static_cast<int>(ch);
return Sound_to_PointProcess_zeroes(self,
channel > self->ny ? 1 : channel,
includeRaisers, includeFallers);
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);
Expand Down
26 changes: 25 additions & 1 deletion tests/test_sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -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')

0 comments on commit f121d88

Please sign in to comment.