diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6dc01242..d5c8882f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,7 @@ set (SFIZZ_SOURCES sfizz/Logger.cpp sfizz/SfzFilter.cpp sfizz/Curve.cpp + sfizz/Wavetables.cpp sfizz/Effects.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp new file mode 100644 index 00000000..c74ee822 --- /dev/null +++ b/src/sfizz/Wavetables.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Wavetables.h" +#include +#include + +namespace sfz { + +void HarmonicProfile::generate( + absl::Span table, double amplitude, double cutoff) const +{ + size_t size = table.size(); + + typedef std::complex cpx; + + // allocate a spectrum of size N/2+1 + // bins are equispaced in frequency, with index N/2 being nyquist + std::unique_ptr spec(new cpx[size / 2 + 1]()); + + kiss_fftr_cfg cfg = kiss_fftr_alloc(size, true, nullptr, nullptr); + if (!cfg) + throw std::bad_alloc(); + + // bins need scaling and phase offset; this IFFT is a sum of cosines + const std::complex k = std::polar(amplitude * 0.5, M_PI / 2); + + // start filling at bin index 1; 1 is fundamental, 0 is DC + for (size_t index = 1; index < size / 2 + 1; ++index) { + if (index * (1.0 / size) > cutoff) + break; + + std::complex harmonic = getHarmonic(index); + spec[index] = k * harmonic; + } + + kiss_fftri(cfg, reinterpret_cast(spec.get()), table.data()); + kiss_fftr_free(cfg); +} + +class SineProfile : public HarmonicProfile { +public: + std::complex getHarmonic(size_t index) const + { + return (index == 1) ? 1.0 : 0.0; + } +}; + +class TriangleProfile : public HarmonicProfile { +public: + std::complex getHarmonic(size_t index) const + { + if ((index & 1) == 0) + return 0.0; + + bool s = (index >> 1) & 1; + return std::polar( + (8 / (M_PI * M_PI)) * (1.0 / (index * index)), + s ? 0.0 : M_PI); + } +}; + +class SawProfile : public HarmonicProfile { +public: + std::complex getHarmonic(size_t index) const + { + if (index < 1) + return 0.0; + + return std::polar( + (2.0 / M_PI) / index, + (index & 1) ? 0.0 : M_PI); + } +}; + +class SquareProfile : public HarmonicProfile { +public: + std::complex getHarmonic(size_t index) const + { + if ((index & 1) == 0) + return 0.0; + + return std::polar((4.0 / M_PI) / index, M_PI); + } +}; + +static const SineProfile sineProfile; +static const TriangleProfile triangleProfile; +static const SawProfile sawProfile; +static const SquareProfile squareProfile; + +const HarmonicProfile& HarmonicProfile::getSine() +{ + return sineProfile; +} + +const HarmonicProfile& HarmonicProfile::getTriangle() +{ + return triangleProfile; +} + +const HarmonicProfile& HarmonicProfile::getSaw() +{ + return sawProfile; +} + +const HarmonicProfile& HarmonicProfile::getSquare() +{ + return squareProfile; +} + +} // namespace sfz diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h new file mode 100644 index 00000000..c150b612 --- /dev/null +++ b/src/sfizz/Wavetables.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +namespace sfz { + +/** + A description of the harmonics of a particular wave form + */ +class HarmonicProfile { +public: + virtual ~HarmonicProfile() {} + + static const HarmonicProfile& getSine(); + static const HarmonicProfile& getTriangle(); + static const HarmonicProfile& getSaw(); + static const HarmonicProfile& getSquare(); + + /** + @brief Get the value at the given index of the frequency spectrum. + + The modulus and the argument of the complex number are equal to the + amplitude and the phase of the harmonic component. + */ + virtual std::complex getHarmonic(size_t index) const = 0; + + /** + @brief Generate a period of the waveform and store it in the table. + + Do not generate harmonics above cutoff, which is expressed as Fc/Fs. + */ + void generate(absl::Span table, double amplitude, double cutoff) const; +}; + +} // namespace sfz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index eb28bb5c..c2d030ee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,4 +60,7 @@ target_link_libraries(filter_apply PRIVATE sfizz::sfizz) add_executable(sfizz_plot_curve PlotCurve.cpp) target_link_libraries(sfizz_plot_curve PRIVATE sfizz::sfizz) +add_executable(sfizz_plot_wavetables PlotWavetables.cpp) +target_link_libraries(sfizz_plot_wavetables PRIVATE sfizz::sfizz) + file(COPY "." DESTINATION ${CMAKE_BINARY_DIR}/tests) diff --git a/tests/PlotWavetables.cpp b/tests/PlotWavetables.cpp new file mode 100644 index 00000000..6037c549 --- /dev/null +++ b/tests/PlotWavetables.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/Wavetables.h" +#include +#include +#include + +static void usage() +{ + std::cerr << "Usage: sfizz_plot_wavetables [-w wave] [-a amplitude] [-c cutoff]\n"; +} + +int main(int argc, char* argv[]) +{ + absl::string_view waveName; + double amplitude = 1.0; + double cutoff = 0.5; + + for (int i = 1; i < argc; ++i) { + absl::string_view arg = argv[i]; + + if (arg == "-w") { + if (i + 1 >= argc) + return usage(), 1; + waveName = argv[++i]; + } else if (arg == "-a") { + if (i + 1 >= argc || !absl::SimpleAtod(argv[++i], &litude)) + return usage(), 1; + } else if (arg == "-c") { + if (i + 1 >= argc || !absl::SimpleAtod(argv[++i], &cutoff)) + return usage(), 1; + } else + return usage(), 1; + } + + const sfz::HarmonicProfile* hp = nullptr; + if (waveName == "sine") + hp = &sfz::HarmonicProfile::getSine(); + else if (waveName == "square") + hp = &sfz::HarmonicProfile::getSquare(); + else if (waveName == "triangle") + hp = &sfz::HarmonicProfile::getTriangle(); + else if (waveName == "saw") + hp = &sfz::HarmonicProfile::getSaw(); + else { + std::cerr << "Unknown wave: " << waveName << '\n'; + return 1; + } + + constexpr size_t tableSize = 2048; + float table[tableSize]; + + hp->generate(table, amplitude, cutoff); + + for (size_t i = 0; i < tableSize; ++i) + std::cout << (i * (1.0 / tableSize)) << ' ' << table[i] << '\n'; + + return 0; +}