From 69218758b6d8830b78b7df76e83e23ae866bd241 Mon Sep 17 00:00:00 2001 From: "Charles K. Neimog" Date: Sat, 6 Jul 2024 21:14:19 -0300 Subject: [PATCH] add pybind11 bridge --- .github/workflows/c-cpp.yml | 19 +-- .gitignore | 3 + CMakeLists.txt | 65 ++++++++-- README.md | 11 +- Sources/OScofo.hpp | 40 ++++++ Sources/OScofo/OScofo.cpp | 108 ++++++++++++++++- Sources/OScofo/log.hpp | 4 +- Sources/OScofo/mdp.cpp | 67 +++++----- Sources/OScofo/mdp.hpp | 15 ++- Sources/OScofo/mir.cpp | 17 +-- Sources/OScofo/mir.hpp | 1 + Sources/OScofo/score.cpp | 30 +++-- Sources/OScofo/score.hpp | 11 +- Sources/OScofo/states.hpp | 1 + Sources/PureData/o.scofo~.cpp | 195 +++++++++++++----------------- Sources/Python/OScofo/__init__.py | 7 ++ Sources/Python/PyOScofo.cpp | 35 ++++++ Tests/Tests.py | 44 +++++++ Tests/oscofo-test.pd | 32 ++--- setup.py | 32 +++++ 20 files changed, 510 insertions(+), 227 deletions(-) create mode 100644 Sources/Python/OScofo/__init__.py create mode 100644 Sources/Python/PyOScofo.cpp create mode 100644 Tests/Tests.py create mode 100644 setup.py diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 944e2cf..321f3b1 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -26,17 +26,19 @@ jobs: arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" arch -x86_64 /usr/local/bin/brew install pd arch -x86_64 /usr/local/bin/brew install boost + arch -x86_64 /usr/local/bin/brew install pybind11 - name: Install PureData and Deps arm64 Mac if: ${{ matrix.arch == 'arm64' }} run: | brew install pd brew install boost + brew install pybind11 - name: Build Object for Arm if: ${{ matrix.arch == 'arm64' }} run: | export CPLUS_INCLUDE_PATH="$CPLUS_INCLUDE_PATH:/opt/homebrew/include/" export LDFLAGS="-L/opt/homebrew/lib" - cmake . -B build -DCMAKE_OSX_ARCHITECTURES=arm64 -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ + cmake . -B build -DCMAKE_OSX_ARCHITECTURES=arm64 -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ -DBUILD_ALL=ON cmake --build build -j $(sysctl -n hw.logicalcpu) make install -C build - name: Build Object for x86_64 @@ -44,7 +46,7 @@ jobs: run: | export CPLUS_INCLUDE_PATH="$CPLUS_INCLUDE_PATH:/usr/local/include/" export LDFLAGS="-L/usr/local/lib" - cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DPDLIBDIR=./ + cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DPDLIBDIR=./ -DBUILD_ALL=ON cmake --build build -j $(sysctl -n hw.logicalcpu) make install -C build - name: Upload Object @@ -71,7 +73,7 @@ jobs: with: msystem: mingw64 update: false - install: make mingw-w64-x86_64-gcc mingw64/mingw-w64-x86_64-cmake mingw-w64-x86_64-boost + install: make mingw-w64-x86_64-gcc mingw64/mingw-w64-x86_64-cmake mingw-w64-x86_64-boost mingw-w64-x86_64-pybind11 - name: Install winget uses: Cyberboss/install-winget@v1 - name: Install PureData Float 32 @@ -85,14 +87,14 @@ jobs: - name: Configure and build Visual Studio if: matrix.compiler == 'msvc' run: | - cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ + cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ -DBUILD_ALL=ON cmake --build build cmake --install build - name: Configure and build Mingw shell: msys2 {0} if: matrix.compiler == 'mingw' run: | - cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ + cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ -DBUILD_ALL=ON cmake --build build cmake --install build - name: Upload @@ -117,6 +119,7 @@ jobs: sudo add-apt-repository ppa:pure-data/pure-data -y sudo apt install puredata -y sudo apt install libboost-all-dev + sudo apt install python3-pybind11 -y - name: Install aarch64 gcc if: matrix.arch == 'aarch64' run: | @@ -130,19 +133,19 @@ jobs: - name: Build Object if: matrix.arch == 'amd64' run: | - cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ + cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DPDLIBDIR=./ -DBUILD_ALL=ON cmake --build build -- -j$(nproc) make install -C build - name: Build Object if: matrix.arch == 'aarch64' run: | - cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DCMAKE_SYSTEM_PROCESSOR=aarch64 -DPDLIBDIR=./ + cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DCMAKE_SYSTEM_PROCESSOR=aarch64 -DPDLIBDIR=./ -DBUILD_ALL=ON cmake --build build -- -j$(nproc) make install -C build - name: Build Object if: matrix.arch == 'arm' run: | - cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DCMAKE_SYSTEM_PROCESSOR=arm -DPDLIBDIR=./ + cmake . -B build -DPD_FLOATSIZE=${{ matrix.precision }} -DCMAKE_SYSTEM_PROCESSOR=arm -DPDLIBDIR=./ -DBUILD_ALL=ON cmake --build build -- -j$(nproc) make install -C build - name: Upload Object diff --git a/.gitignore b/.gitignore index c3f17ea..9c688d9 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ Resources/Research/__pycache__ Tests/.mscbackup Tests/__pycache__ .cmake-format.yaml +dist +poetry.lock +Sources/Python/OScofo.egg-info diff --git a/CMakeLists.txt b/CMakeLists.txt index ae78c46..da68a23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,12 +2,21 @@ cmake_minimum_required(VERSION 3.20) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +project(OScofo LANGUAGES CXX) + set(PDCMAKE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Resources/pd.cmake/" CACHE PATH "Path to pd.cmake") include(${PDCMAKE_DIR}/pd.cmake) -project(o.scofo~) +option(BUILD_PY_MODULE "Build Python Module" OFF) +option(BUILD_PD_OBJECT "Build PureData Object" OFF) +option(BUILD_ALL "Build Python Module and PureData Object" OFF) + +if(BUILD_ALL) + set(BUILD_PY_MODULE ON) + set(BUILD_PD_OBJECT ON) +endif() # ╭──────────────────────────────────────╮ # │ FFTW3 │ @@ -28,18 +37,52 @@ set_target_properties(fftw3 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_D set_target_properties(fftw3 PROPERTIES POSITION_INDEPENDENT_CODE ON) # ╭──────────────────────────────────────╮ -# │ Library │ +# │ OScofo │ # ╰──────────────────────────────────────╯ -file(GLOB SCOFO_SRC "${CMAKE_CURRENT_SOURCE_DIR}/Sources/*.cpp") -pd_add_external(o.scofo~ "${SCOFO_SRC}" TARGET oscofo) -target_include_directories(oscofo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/Libraries/fftw-3.3.10/api") -target_link_libraries(oscofo PUBLIC fftw3) -target_compile_options(oscofo PRIVATE -static-libstdc++ -static-libgcc -Wl,-allow-multiple-definition) -target_link_options(oscofo PRIVATE -static-libstdc++ -static-libgcc -Wl,-allow-multiple-definition) +list(APPEND SCofoSrc Sources/OScofo/mdp.cpp) +list(APPEND SCofoSrc Sources/OScofo/mir.cpp) +list(APPEND SCofoSrc Sources/OScofo/score.cpp) +list(APPEND SCofoSrc Sources/OScofo/OScofo.cpp) +add_library(OScofo STATIC ${SCofoSrc}) +target_link_libraries(OScofo PUBLIC fftw3) +set_target_properties(OScofo PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_include_directories(OScofo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/Libraries/fftw-3.3.10/api") +# ╭──────────────────────────────────────╮ +# │ Python Module │ +# ╰──────────────────────────────────────╯ +if(BUILD_PY_MODULE) + find_package(Python REQUIRED COMPONENTS Interpreter Development) + find_package(pybind11 REQUIRED) + include_directories(${PYTHON_INCLUDE_DIRS}) + include_directories(${pybind11_INCLUDE_DIR}) + pybind11_add_module(_OScofo Sources/Python/PyOScofo.cpp) + target_link_libraries(_OScofo PRIVATE OScofo ${PYTHON_LIBRARIES}) + set_target_properties( + _OScofo + PROPERTIES CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO) + if(NOT APPLE) + target_compile_options(_OScofo PRIVATE -static-libstdc++ -static-libgcc -Wl,-allow-multiple-definition) + target_link_options(_OScofo PRIVATE -static-libstdc++ -static-libgcc -Wl,-allow-multiple-definition) + endif() + install(TARGETS _OScofo DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/Sources/Python/OScofo/) +endif() # ╭──────────────────────────────────────╮ -# │ Data Files │ +# │ PureData Object │ # ╰──────────────────────────────────────╯ -pd_add_datafile(oscofo "${CMAKE_CURRENT_SOURCE_DIR}/README.md") -pd_add_datafile(oscofo "${CMAKE_CURRENT_SOURCE_DIR}/Resources/o.scofo~-help.pd") +if(BUILD_PD_OBJECT) + project(o.scofo~) + pd_add_external(o.scofo~ Sources/PureData/o.scofo~.cpp TARGET PdOScofo) + target_include_directories(PdOScofo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/Libraries/fftw-3.3.10/api") + if(NOT APPLE) + target_compile_options(PdOScofo PRIVATE -static-libstdc++ -static-libgcc -Wl,-allow-multiple-definition) + target_link_options(PdOScofo PRIVATE -static-libstdc++ -static-libgcc -Wl,-allow-multiple-definition) + endif() + target_link_libraries(PdOScofo PRIVATE OScofo) + + pd_add_datafile(pdOScofo "${CMAKE_CURRENT_SOURCE_DIR}/README.md") + pd_add_datafile(pdOScofo "${CMAKE_CURRENT_SOURCE_DIR}/Resources/o.scofo~-help.pd") +endif() diff --git a/README.md b/README.md index 4e77cb2..df06326 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ -# o.scofo~ +# OScofo -**O**pen **SCO**re **FO**llower (o.scofo~) is a PureData object designed for contemporary music applications. This project aims to encourage collaboration among researchers and musicians for contemporary music. Here's what you need to know: +**O**pen **SCO**re **FO**llower (OScofo) is a PureData object designed for contemporary music applications. This project aims to encourage collaboration among researchers and musicians for contemporary music. Here's what you need to know: ## Goal -The goal of *o.scofo~* is to provide a simple, accessible tool for real-time score following. By keeping the software lightweight, it can be easily run on the web using the [pd4web](https://charlesneimog.github.io/pd4web/) platform, now the we can use PureData on Web Browsers. So finally we have something that can be run for rehearsals one click distance (without externals, OSs incompatibility, etc). +The goal of *OScofo* is to provide a simple, accessible tool for real-time score following. By keeping the software lightweight, it can be easily run on the web using the [pd4web](https://charlesneimog.github.io/pd4web/) platform, now the we can use PureData on Web Browsers. So finally we have something that can be run for rehearsals one click distance (without externals, OSs incompatibility, etc). ## Collaboration and Contribution -I invite researchers and developers to contribute to the *o.scofo~* project. By sharing the source code, I am trying to provide access to the theories and mathematical formulas that drive the software. This transparency is inspired by the amazing tools like IEM, SPARTA, and of course, PureData. My main aim is artistic, maybe your research can help me and a lot of others composers. +I invite researchers and developers to contribute to the *OScofo* project. By sharing the source code, I am trying to provide access to the theories and mathematical formulas that drive the software. This transparency is inspired by the amazing tools like IEM, SPARTA, and of course, PureData. My main aim is artistic, maybe your research can help me and a lot of others composers. ## Technical Foundations -*o.scofo~* uses several concepts developed by many researches: +*OScofo* uses several concepts developed by many researches: * **Score Language**: Based on the `scofo` (by Miller Puckette) and `antescofo~` (by Arshia Cont) language. * **Pitch Comparison**: Utilizes the Kullback-Leibler (KL) Divergence method for pitch comparison as presented by Arshia Cont in 2008 and 2010. * **Rhythm Synchronization**: Integrates theories of rhythm synchronization developed by Edward Large and Mari Riess Jones (1999) and Edward Large and Caroline Palmer (2002), as presented for Cont (2010). * **Descriptors**: Employs descriptors from Willian Brent's timbreIDLib project for analyzing and identifying timbral characteristics. + diff --git a/Sources/OScofo.hpp b/Sources/OScofo.hpp index 17e10f5..c2b400f 100644 --- a/Sources/OScofo.hpp +++ b/Sources/OScofo.hpp @@ -4,3 +4,43 @@ #include "OScofo/mir.hpp" #include "OScofo/score.hpp" #include "OScofo/states.hpp" + +#define OSCOFO_VERSION_MAJOR 0 +#define OSCOFO_VERSION_MINOR 1 +#define OSCOFO_VERSION_PATCH 0 + +class OScofo { + public: + OScofo(float Sr, float WindowSize, float HopSize); + + // Set Functions + void SetPitchTemplateSigma(double Sigma); + void SetHarmonics(int Harmonics); + void SetTimeAccumFactor(double TimeAccumFactor); + void SetTimeCouplingStrength(double TimeCouplingStrength); + void SetdBTreshold(double dB); + void SetTunning(double Tunning); + void SetCurrentEvent(int Event); + + // Get Functions + float GetLiveBPM(); + int GetEventIndex(); + + // Main Functions + bool ParseScore(std::string ScorePath); + bool ProcessBlock(std::vector &AudioBuffer); + + // Helpers Functions + bool ScoreIsLoaded(); + std::string GetError(); + + private: + OScofoMDP m_MDP; + OScofoMIR m_MIR; + OScofoScore m_Score; + + States m_States; + Description m_Desc; + + std::string m_Error; +}; diff --git a/Sources/OScofo/OScofo.cpp b/Sources/OScofo/OScofo.cpp index 17e10f5..35a990a 100644 --- a/Sources/OScofo/OScofo.cpp +++ b/Sources/OScofo/OScofo.cpp @@ -1,6 +1,104 @@ -#pragma once +#include "../OScofo.hpp" -#include "OScofo/mdp.hpp" -#include "OScofo/mir.hpp" -#include "OScofo/score.hpp" -#include "OScofo/states.hpp" +// ───────────────────────────────────── +OScofo::OScofo(float Sr, float WindowSize, float HopSize) + : m_MDP(Sr, WindowSize, HopSize), m_MIR(Sr, WindowSize, HopSize) { + m_States = States(); + m_Desc = Description(); +} + +// ╭─────────────────────────────────────╮ +// │ Set Functions │ +// ╰─────────────────────────────────────╯ +void OScofo::SetPitchTemplateSigma(double Sigma) { + m_MDP.SetPitchTemplateSigma(Sigma); + m_MDP.UpdatePitchTemplate(); +} + +// ───────────────────────────────────── +void OScofo::SetHarmonics(int Harmonics) { + m_MDP.SetHarmonics(Harmonics); + m_MDP.UpdatePitchTemplate(); +} + +// ───────────────────────────────────── +void OScofo::SetTimeCouplingStrength(double TimeCouplingStrength) { + m_MDP.SetTimeCouplingStrength(TimeCouplingStrength); +} + +// ───────────────────────────────────── +void OScofo::SetTimeAccumFactor(double TimeAccumFactor) { + m_MDP.SetTimeAccumFactor(TimeAccumFactor); +} + +// ───────────────────────────────────── +void OScofo::SetdBTreshold(double dB) { + m_MDP.SetdBTreshold(dB); +} + +// ───────────────────────────────────── +void OScofo::SetTunning(double Tunning) { + m_MDP.SetTunning(Tunning); + m_Score.SetTunning(Tunning); +} + +// ───────────────────────────────────── +void OScofo::SetCurrentEvent(int Event) { + m_MDP.SetCurrentEvent(Event); +} + +// ╭─────────────────────────────────────╮ +// │ Get Functions │ +// ╰─────────────────────────────────────╯ +int OScofo::GetEventIndex() { + return 0; // TODO: Implement yet +} + +// ───────────────────────────────────── +std::string OScofo::GetError() { + return m_Error; +} + +// ───────────────────────────────────── +float OScofo::GetLiveBPM() { + return m_MDP.GetLiveBPM(); +} + +// ╭─────────────────────────────────────╮ +// │ Helpers Functions │ +// ╰─────────────────────────────────────╯ +bool OScofo::ScoreIsLoaded() { + return m_Score.ScoreIsLoaded(); +} + +// ╭─────────────────────────────────────╮ +// │ Main Functions │ +// ╰─────────────────────────────────────╯ +bool OScofo::ParseScore(std::string ScorePath) { + LOGE() << "OScofo::ParseScore"; + m_Score.Parse(m_States, ScorePath); + LOGE() << "Score has " << m_States.size() << " states"; + + for (int i = 0; i < m_States.size(); i++) { + if (!m_States[i].Valid) { + m_Error = std::string("Error on line ") + std::to_string(m_States[i].Line) + ": " + + m_States[i].Error; + return false; + } + } + m_MDP.SetScoreStates(m_States); + m_MDP.UpdatePhaseValues(); + return true; +} + +// ───────────────────────────────────── +bool OScofo::ProcessBlock(std::vector &AudioBuffer) { + if (!m_Score.ScoreIsLoaded()) { + return true; + } + + m_MIR.GetDescription(AudioBuffer, m_Desc); + // m_MDP.GetEvent(m_Desc); + + return true; +} diff --git a/Sources/OScofo/log.hpp b/Sources/OScofo/log.hpp index 924e505..9bab110 100644 --- a/Sources/OScofo/log.hpp +++ b/Sources/OScofo/log.hpp @@ -3,9 +3,8 @@ #include #include -#define SCOFO_DEBUG true +#define SCOFO_DEBUG false -#if SCOFO_DEBUG class LogStream { public: template LogStream &operator<<(const T &value) { @@ -24,6 +23,7 @@ class LogStream { std::ostringstream buffer; }; +#if SCOFO_DEBUG #define LOGE() LogStream() #else #define LOGE() \ diff --git a/Sources/OScofo/mdp.cpp b/Sources/OScofo/mdp.cpp index 29188f8..802b4d7 100644 --- a/Sources/OScofo/mdp.cpp +++ b/Sources/OScofo/mdp.cpp @@ -20,14 +20,11 @@ OScofoMDP::OScofoMDP(float Sr, float WindowSize, float HopSize) { SetTunning(440); } -// ───────────────────────────────────── -OScofoMDP::~OScofoMDP() { -} - // ───────────────────────────────────── void OScofoMDP::SetScoreStates(States States) { m_States.clear(); m_States = States; + UpdatePitchTemplate(); } // ───────────────────────────────────── void OScofoMDP::UpdatePitchTemplate() { @@ -102,7 +99,7 @@ void OScofoMDP::SetBPM(double BPM) { } // ───────────────────────────────────── -void OScofoMDP::SetTreshold(double dB) { +void OScofoMDP::SetdBTreshold(double dB) { m_dBTreshold = dB; } @@ -122,7 +119,7 @@ int OScofoMDP::GetTunning() { } // ───────────────────────────────────── -void OScofoMDP::SetEvent(int Event) { +void OScofoMDP::SetCurrentEvent(int Event) { m_CurrentEvent = Event; } @@ -312,7 +309,7 @@ double OScofoMDP::GetReward(State &PossibleState, Description &Desc) { // TODO: Add Attack Envelope double PitchSimilarity = GetPitchSimilarity(PossibleState, Desc); - double TimeSimilarity = GetTimeSimilarity(PossibleState, Desc) * TimeWeight; + // double TimeSimilarity = GetTimeSimilarity(PossibleState, Desc) * TimeWeight; double Reward = (PitchSimilarity * PitchWeight); //+ (TimeSimilarity * (m_SyncStr / 2)); @@ -321,14 +318,11 @@ double OScofoMDP::GetReward(State &PossibleState, Description &Desc) { // ───────────────────────────────────── double OScofoMDP::GetBestEvent(Description &Desc) { - LOGE() << "OScofoMDP::GetBestEvent"; if (m_CurrentEvent == -1) { m_BPM = m_States[0].BPMExpected; } - // TODO: Make this user defined - double LookAhead = 2; // Look 5 seconds in future - + double LookAhead = 2; // TODO: Look 5 seconds in future int BestGuess, i = m_CurrentEvent; double BestReward = -1; double EventLookAhead = 0; @@ -356,11 +350,11 @@ double OScofoMDP::GetBestEvent(Description &Desc) { } // ───────────────────────────────────── -int OScofoMDP::GetEvent(std::vector AudioIn, Description &Desc) { +int OScofoMDP::GetEvent(Description &Desc) { double BlockDur = 1 / m_Sr; m_TimeInThisEvent += BlockDur * m_HopSize; - if (Desc.PassTreshold) { + if (!Desc.PassTreshold) { double BlockDur = 1 / m_Sr; LOGE() << "End GetEvent"; return m_CurrentEvent; @@ -368,28 +362,33 @@ int OScofoMDP::GetEvent(std::vector AudioIn, Description &Desc) { // Get the best event to describe the current state double Event = GetBestEvent(Desc); - if (Event != m_CurrentEvent && Event == 0) { - LOGE(); - m_CurrentEvent = Event; - double PsiK = 60 / m_States[0].BPMExpected; - m_LastPsiN = PsiK; - m_PsiN = PsiK; - m_PsiN1 = PsiK; - m_States[0].OnsetObserved = 0; - m_BPM = m_States[0].BPMExpected; - m_Tn = 0; - m_LastTn = 0; - m_TimeInThisEvent = 0; - } else if (Event != m_CurrentEvent && Event != 0) { - LOGE() << "<== Event: " << Event << " ==>"; - m_CurrentEvent = Event; - m_LastTn = m_Tn; - m_Tn += m_TimeInThisEvent; - GetBPM(); - m_TimeInThisEvent = 0; - LOGE(); + if (Event != m_CurrentEvent) { + printf("Event: %f\n", Event); } - LOGE() << "Get Event Finish"; + // if (Event != m_CurrentEvent && Event == 0) { + // LOGE(); + // m_CurrentEvent = Event; + // double PsiK = 60 / m_States[0].BPMExpected; + // m_LastPsiN = PsiK; + // m_PsiN = PsiK; + // m_PsiN1 = PsiK; + // m_States[0].OnsetObserved = 0; + // m_BPM = m_States[0].BPMExpected; + // m_Tn = 0; + // m_LastTn = 0; + // m_TimeInThisEvent = 0; + // } else if (Event != m_CurrentEvent && Event != 0) { + // LOGE() << "<== Event: " << Event << " ==>"; + // m_CurrentEvent = Event; + // m_LastTn = m_Tn; + // m_Tn += m_TimeInThisEvent; + // GetBPM(); + // m_TimeInThisEvent = 0; + // LOGE(); + // } + // + // LOGE() << "Get Event Finish"; + m_CurrentEvent = Event; return m_CurrentEvent; } diff --git a/Sources/OScofo/mdp.hpp b/Sources/OScofo/mdp.hpp index 2a2a925..7ebea78 100644 --- a/Sources/OScofo/mdp.hpp +++ b/Sources/OScofo/mdp.hpp @@ -3,9 +3,12 @@ #include #include -#include "mir.hpp" #include "states.hpp" +#ifndef TWO_PI +#define TWO_PI (2 * M_PI) +#endif + using PitchTemplateArray = std::vector; // ╭─────────────────────────────────────╮ @@ -14,7 +17,7 @@ using PitchTemplateArray = std::vector; class OScofoMDP { public: OScofoMDP(float Sr, float WindowSize, float HopSize); - ~OScofoMDP(); + // ~OScofoMDP(); // Init Functions void SetScoreStates(States States); @@ -27,7 +30,7 @@ class OScofoMDP { void SetBPM(double Bpm); double GetLiveBPM(); void ResetLiveBpm(); - void SetTreshold(double dB); + void SetdBTreshold(double dB); // Get Functions int GetTunning(); @@ -41,11 +44,11 @@ class OScofoMDP { void ClearStates(); int GetStatesSize(); - int GetEvent(std::vector AudioIn, Description &Desc); + int GetEvent(Description &Desc); // Set Variables void SetTunning(double Tunning); - void SetEvent(int Event); + void SetCurrentEvent(int Event); void SetTimeAccumFactor(double f); void SetTimeCouplingStrength(double f); @@ -56,7 +59,7 @@ class OScofoMDP { double m_HopSize; double m_Harmonics = 10; double m_PitchTemplateHigherBin = 0; - double m_dBTreshold = -70; + double m_dBTreshold = -55; // Events double m_Tunning = 440; diff --git a/Sources/OScofo/mir.cpp b/Sources/OScofo/mir.cpp index 6a62e94..ad009f9 100644 --- a/Sources/OScofo/mir.cpp +++ b/Sources/OScofo/mir.cpp @@ -1,6 +1,6 @@ #include "mir.hpp" -#include +#include #include // ╭─────────────────────────────────────╮ @@ -14,8 +14,10 @@ OScofoMIR::OScofoMIR(float Sr, float WindowSize, float HopSize) { m_Sr = Sr; int WindowHalf = WindowSize / 2; - m_FFTOut = new fftw_complex[WindowHalf]; - m_FFTPlan = fftw_plan_dft_r2c_1d(m_WindowSize, nullptr, nullptr, FFTW_ESTIMATE); + m_FFTIn = (double *)fftw_alloc_real(WindowSize); + m_FFTOut = (fftw_complex *)fftw_alloc_complex(WindowHalf + 1); + m_FFTPlan = fftw_plan_dft_r2c_1d(m_WindowSize, m_FFTIn, m_FFTOut, FFTW_MEASURE); + if (m_FFTPlan == nullptr) { LOGE() << "OScofoMIR::OScofoMIR fftw_plan_dft_r2c_1d failed"; } @@ -25,8 +27,9 @@ OScofoMIR::OScofoMIR(float Sr, float WindowSize, float HopSize) { // ───────────────────────────────────── OScofoMIR::~OScofoMIR() { - delete[] m_FFTOut; fftw_destroy_plan(m_FFTPlan); + fftw_free(m_FFTIn); + fftw_free(m_FFTOut); } // ╭─────────────────────────────────────╮ @@ -34,7 +37,7 @@ OScofoMIR::~OScofoMIR() { // ╰─────────────────────────────────────╯ // ───────────────────────────────────── double OScofoMIR::Mtof(double note, double tunning) { - return tunning * pow(2.0, (note - 69) / 12.0); + return tunning * std::pow(2.0, (note - 69) / 12.0); } // ───────────────────────────────────── @@ -80,8 +83,8 @@ void OScofoMIR::GetFFTDescriptions(const std::vector &In, Description &D Desc.NormSpectralPower.resize(NHalf); } - std::vector inCopy = In; - fftw_execute_dft_r2c(m_FFTPlan, inCopy.data(), m_FFTOut); + std::copy(In.begin(), In.end(), m_FFTIn); + fftw_execute(m_FFTPlan); double Real, Imag; Desc.MaxAmp = 0; diff --git a/Sources/OScofo/mir.hpp b/Sources/OScofo/mir.hpp index 5dfab14..13eb3a9 100644 --- a/Sources/OScofo/mir.hpp +++ b/Sources/OScofo/mir.hpp @@ -36,6 +36,7 @@ class OScofoMIR { double Freq2Bin(double freq, double n, double Sr); // FFT + double *m_FFTIn; fftw_complex *m_FFTOut; fftw_plan m_FFTPlan; void GetFFTDescriptions(const std::vector &In, Description &Desc); diff --git a/Sources/OScofo/score.cpp b/Sources/OScofo/score.cpp index 88bc652..b3a3d7a 100644 --- a/Sources/OScofo/score.cpp +++ b/Sources/OScofo/score.cpp @@ -15,6 +15,13 @@ // TODO: ADD EXTENDED TECHNIQUES @pizz, @multiphonic, // clang-format on +void OScofoScore::SetTunning(double Tunning) { + m_Tunning = Tunning; +} +bool OScofoScore::ScoreIsLoaded() { + return m_ScoreLoaded; +} + // ╭─────────────────────────────────────╮ // │ Utils │ // ╰─────────────────────────────────────╯ @@ -78,8 +85,8 @@ int OScofoScore::Name2Midi(std::string note) { } // ───────────────────────────────────── -State OScofoScore::AddNote(State State, std::vector Tokens, double BPM, - int lineCount) { +State OScofoScore::AddNote(State &State, std::vector Tokens, double BPM, + int LineCount) { double Midi; if (BPM == -1) { State.Valid = false; @@ -88,7 +95,7 @@ State OScofoScore::AddNote(State State, std::vector Tokens, double } if (Tokens.size() < 3) { State.Valid = false; - State.Error = "Invalid note on line " + std::to_string(lineCount); + State.Error = "Invalid note on line " + std::to_string(LineCount); return State; } @@ -131,14 +138,13 @@ State OScofoScore::AddNote(State State, std::vector Tokens, double // ╭─────────────────────────────────────╮ // │ Parse File of the Score │ // ╰─────────────────────────────────────╯ -void OScofoScore::Parse(States &States, const char *ScoreFile) { +void OScofoScore::Parse(States &States, std::string ScoreFile) { LOGE() << "start OScofoScore::Parse"; - States.clear(); + States.clear(); // Clear the vector before parsing - m_ScoreLoaded = false; + // Open the score file for reading std::ifstream File(ScoreFile); - if (!File) { State State; State.Valid = false; @@ -170,9 +176,12 @@ void OScofoScore::Parse(States &States, const char *ScoreFile) { State State; State.Type = NOTE; State.Id = States.size(); + State.Line = LineCount; State = AddNote(State, Tokens, BPM, LineCount); if (!State.Valid) { - return; + State.Error = "Error on line " + std::to_string(LineCount) + ": " + State.Error; + States.push_back(State); + continue; } if (Event != 0) { State.OnsetExpected = LastOnset + PreviousDuration * (60 / BPM); // in Seconds @@ -181,7 +190,6 @@ void OScofoScore::Parse(States &States, const char *ScoreFile) { } Event++; States.push_back(State); - PreviousDuration = State.Duration; LastOnset = State.OnsetExpected; @@ -189,8 +197,8 @@ void OScofoScore::Parse(States &States, const char *ScoreFile) { BPM = std::stof(Tokens[1]); } } + // Optionally, set m_ScoreLoaded to true if necessary m_ScoreLoaded = true; - LOGE() << "end OScofoScore::Parse | There is " << States.size() << " events"; - return; + LOGE() << "end OScofoScore::Parse | There are " << States.size() << " events"; } diff --git a/Sources/OScofo/score.hpp b/Sources/OScofo/score.hpp index 2c471c2..c78350d 100644 --- a/Sources/OScofo/score.hpp +++ b/Sources/OScofo/score.hpp @@ -13,14 +13,13 @@ class OScofoScore { public: int Name2Midi(std::string note); - void Parse(States &States, const char *ScoreFile); - bool ScoreLoaded() { - return m_ScoreLoaded; - } + void Parse(States &States, std::string ScoreFile); + void SetTunning(double Tunning); + bool ScoreIsLoaded(); private: - State AddNote(State State, std::vector tokens, double bpm, int lineCount); - double FollowBpm(std::vector tokens, int lineCount); + State AddNote(State &State, std::vector Tokens, double BPM, int LineCount); + double FollowBpm(std::vector Tokens, int LineCount); double m_LastOnset = 0; double m_LastPhase = 0; bool m_ScoreLoaded = false; diff --git a/Sources/OScofo/states.hpp b/Sources/OScofo/states.hpp index 2e2059b..7c168ba 100644 --- a/Sources/OScofo/states.hpp +++ b/Sources/OScofo/states.hpp @@ -24,6 +24,7 @@ struct State { // Error Handling bool Valid; + int Line; std::string Error; }; using States = std::vector; diff --git a/Sources/PureData/o.scofo~.cpp b/Sources/PureData/o.scofo~.cpp index ae5ef1a..abaf106 100644 --- a/Sources/PureData/o.scofo~.cpp +++ b/Sources/PureData/o.scofo~.cpp @@ -9,20 +9,13 @@ static t_class *OScofoObj; // ───────────────────────────────────── -class OScofo { +class PdOScofo { public: t_object xObj; t_sample Sample; std::vector inBuffer; t_clock *Clock; - - OScofoScore &Score; - OScofoMDP &MDP; - OScofoMIR &MIR; - - // - States &ScoreStates; - Description &Desc; + OScofo *OpenScofo; int Event; std::string PatchDir; @@ -48,59 +41,59 @@ class OScofo { t_outlet *Debug; }; -// ───────────────────────────────────── -static void GerenateAnalTemplate(OScofo *x, t_symbol *s, t_float Sr, t_float Fund, t_float H) { - LOGE() << "PureData GerenateAnalTemplate"; - - double *FFTIn; - fftw_complex *FFTOut; - fftw_plan FFTPlan; - x->MDP.m_PitchTemplate.clear(); - - FFTIn = (double *)fftw_malloc(sizeof(double) * x->WindowSize); - FFTOut = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * (x->WindowSize / 2 + 1)); - FFTPlan = fftw_plan_dft_r2c_1d(x->WindowSize, nullptr, nullptr, FFTW_ESTIMATE); - - t_garray *Array; - int VecSize; - t_word *Vec; - - if (!(Array = (t_garray *)pd_findbyclass(s, garray_class))) { - pd_error(x, "[follower~] Array %s not found.", s->s_name); - return; - } else if (!garray_getfloatwords(Array, &VecSize, &Vec)) { - pd_error(x, "[follower~] Bad template for tabwrite '%s'.", s->s_name); - return; - } - - // get middle of the array - int middle = VecSize / 2; - std::vector AudioChunk(x->WindowSize, 0); - for (int i = 0; i < x->WindowSize; i++) { - AudioChunk[i] = Vec[middle + i].w_float; - } - fftw_execute_dft_r2c(FFTPlan, AudioChunk.data(), FFTOut); - - std::vector SpectralPower(x->WindowSize / 2 + 1, 0); - for (int i = 0; i < x->WindowSize / 2 + 1; i++) { - double real = FFTOut[i][0]; - double imag = FFTOut[i][1]; - SpectralPower[i] = ((real * real + imag * imag) / x->WindowSize) / x->WindowSize; - } - - for (int i = 0; i < H; i++) { - double pitchHz = Fund * (i + 1); - int bin = round(pitchHz / (Sr / x->WindowSize)); - x->MDP.m_PitchTemplate.push_back(SpectralPower[bin - 1]); - x->MDP.m_PitchTemplate.push_back(SpectralPower[bin]); - x->MDP.m_PitchTemplate.push_back(SpectralPower[bin + 1]); - } - post("[follower~] Template loaded"); - LOGE() << "PureData end GerenateAnalTemplate"; -} +// // ───────────────────────────────────── +// static void GerenateAnalTemplate(PdOScofo *x, t_symbol *s, t_float Sr, t_float Fund, t_float H) { +// LOGE() << "PureData GerenateAnalTemplate"; +// +// double *FFTIn; +// fftw_complex *FFTOut; +// fftw_plan FFTPlan; +// x->OpenScofo.m_PitchTemplate.clear(); +// +// FFTIn = (double *)fftw_malloc(sizeof(double) * x->WindowSize); +// FFTOut = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * (x->WindowSize / 2 + 1)); +// FFTPlan = fftw_plan_dft_r2c_1d(x->WindowSize, nullptr, nullptr, FFTW_ESTIMATE); +// +// t_garray *Array; +// int VecSize; +// t_word *Vec; +// +// if (!(Array = (t_garray *)pd_findbyclass(s, garray_class))) { +// pd_error(x, "[follower~] Array %s not found.", s->s_name); +// return; +// } else if (!garray_getfloatwords(Array, &VecSize, &Vec)) { +// pd_error(x, "[follower~] Bad template for tabwrite '%s'.", s->s_name); +// return; +// } +// +// // get middle of the array +// int middle = VecSize / 2; +// std::vector AudioChunk(x->WindowSize, 0); +// for (int i = 0; i < x->WindowSize; i++) { +// AudioChunk[i] = Vec[middle + i].w_float; +// } +// fftw_execute_dft_r2c(FFTPlan, AudioChunk.data(), FFTOut); +// +// std::vector SpectralPower(x->WindowSize / 2 + 1, 0); +// for (int i = 0; i < x->WindowSize / 2 + 1; i++) { +// double real = FFTOut[i][0]; +// double imag = FFTOut[i][1]; +// SpectralPower[i] = ((real * real + imag * imag) / x->WindowSize) / x->WindowSize; +// } +// +// for (int i = 0; i < H; i++) { +// double pitchHz = Fund * (i + 1); +// int bin = round(pitchHz / (Sr / x->WindowSize)); +// x->OpenScofo.m_PitchTemplate.push_back(SpectralPower[bin - 1]); +// x->OpenScofo.m_PitchTemplate.push_back(SpectralPower[bin]); +// x->OpenScofo.m_PitchTemplate.push_back(SpectralPower[bin + 1]); +// } +// post("[follower~] Template loaded"); +// LOGE() << "PureData end GerenateAnalTemplate"; +// } // ───────────────────────────────────── -static void Set(OScofo *x, t_symbol *s, int argc, t_atom *argv) { +static void Set(PdOScofo *x, t_symbol *s, int argc, t_atom *argv) { LOGE() << "PureData Set Methoda"; if (argv[0].a_type != A_SYMBOL) { pd_error(x, "[follower~] First argument of set method must be a symbol"); @@ -108,32 +101,31 @@ static void Set(OScofo *x, t_symbol *s, int argc, t_atom *argv) { } std::string method = atom_getsymbol(argv)->s_name; if (method == "sigma") { - x->MDP.SetPitchTemplateSigma(atom_getfloat(argv + 1)); + x->OpenScofo->SetPitchTemplateSigma(atom_getfloat(argv + 1)); post("[follower~] Sigma set to %f", atom_getfloat(argv + 1)); } else if (method == "harmonics") { - x->MDP.SetHarmonics(atom_getint(argv + 1)); - x->MDP.UpdatePitchTemplate(); + x->OpenScofo->SetHarmonics(atom_getint(argv + 1)); post("[follower~] Using pitch template with %d harmonics", atom_getint(argv + 1)); } else if (method == "threshold") { double dB = atom_getfloat(argv + 1); - x->MIR.SetTreshold(dB); + x->OpenScofo->SetdBTreshold(dB); post("[follower~] Treshold set to %f", atom_getfloat(argv + 1)); } else if (method == "time") { std::string submethod = atom_getsymbol(argv + 1)->s_name; if (submethod == "accum") { - x->MDP.SetTimeAccumFactor(atom_getfloat(argv + 2)); + x->OpenScofo->SetTimeAccumFactor(atom_getfloat(argv + 2)); printf("[follower~] Time accumulation set to %.4f\n", atom_getfloat(argv + 2)); } else if (submethod == "coupling") { - x->MDP.SetTimeCouplingStrength(atom_getfloat(argv + 2)); + x->OpenScofo->SetTimeCouplingStrength(atom_getfloat(argv + 2)); printf("[follower~] Time coupling set to %.4f\n", atom_getfloat(argv + 2)); } } else if (method == "tunning") { - x->MDP.SetTunning(atom_getfloat(argv + 1)); + x->OpenScofo->SetTunning(atom_getfloat(argv + 1)); } else if (method == "event") { int f = atom_getint(argv + 1); x->CurrentEvent = f; - x->MDP.SetEvent(f); + x->OpenScofo->SetCurrentEvent(f); } else { pd_error(x, "[follower~] Unknown method"); } @@ -141,24 +133,21 @@ static void Set(OScofo *x, t_symbol *s, int argc, t_atom *argv) { } // ───────────────────────────────────── -static void Start(OScofo *x) { +static void Start(PdOScofo *x) { LOGE() << "PureData Start Method"; - if (!x->ScoreLoaded) { + if (!x->OpenScofo->ScoreIsLoaded()) { pd_error(nullptr, "[o.scofo~] Score not loaded"); + return; } - x->CurrentEvent = -1; - x->MDP.SetEvent(x->CurrentEvent); - x->MDP.SetScoreStates(x->ScoreStates); - x->MDP.UpdatePhaseValues(); - - outlet_float(x->Tempo, x->ScoreStates[0].BPMExpected); + x->OpenScofo->SetCurrentEvent(x->CurrentEvent); + outlet_float(x->Tempo, x->OpenScofo->GetLiveBPM()); outlet_float(x->EventIndex, 0); LOGE() << "PureData end Start Method"; } // ───────────────────────────────────── -static void Score(OScofo *x, t_symbol *s) { +static void Score(PdOScofo *x, t_symbol *s) { LOGE() << "PureData Score Method"; x->ScoreLoaded = false; std::string CompletePath = x->PatchDir; @@ -169,22 +158,17 @@ static void Score(OScofo *x, t_symbol *s) { pd_error(nullptr, "[o.scofo~] Score file not found"); return; } - - x->Score.Parse(x->ScoreStates, CompletePath.c_str()); - x->MDP.SetScoreStates(x->ScoreStates); - x->MDP.UpdatePitchTemplate(); - x->MDP.UpdatePhaseValues(); - + x->OpenScofo->ParseScore(CompletePath.c_str()); x->ScoreLoaded = true; post("[o.scofo~] Score loaded"); LOGE() << "PureData end Score Method"; } // ───────────────────────────────────── -static void ClockTick(OScofo *x) { +static void ClockTick(PdOScofo *x) { LOGE() << "PureData ClockTick"; if (x->Event != 0) { - outlet_float(x->Tempo, x->MDP.GetLiveBPM()); + outlet_float(x->Tempo, x->OpenScofo->GetLiveBPM()); outlet_float(x->EventIndex, x->Event); } LOGE() << "PureData ClockTick end"; @@ -192,11 +176,11 @@ static void ClockTick(OScofo *x) { // ───────────────────────────────────── static t_int *DspPerform(t_int *w) { - OScofo *x = (OScofo *)(w[1]); + PdOScofo *x = (PdOScofo *)(w[1]); t_sample *in = (t_sample *)(w[2]); int n = static_cast(w[3]); - if (!x->Score.ScoreLoaded()) { + if (!x->OpenScofo->ScoreIsLoaded()) { return (w + 4); } @@ -212,13 +196,18 @@ static t_int *DspPerform(t_int *w) { return (w + 4); } + x->BlockIndex = 0; + + // Windowing for (int i = 0; i < x->WindowSize; i++) { x->inBuffer[i] *= 0.5 * (1.0 - cos(2.0 * M_PI * i / (x->WindowSize - 1))); } - x->BlockIndex = 0; - x->MIR.GetDescription(x->inBuffer, x->Desc); - int Event = x->MDP.GetEvent(x->inBuffer, x->Desc) + 1; + bool ok = x->OpenScofo->ProcessBlock(x->inBuffer); + if (!ok) { + return (w + 4); + } + int Event = x->OpenScofo->GetEventIndex(); if (Event == 0) { return (w + 4); } @@ -226,12 +215,11 @@ static t_int *DspPerform(t_int *w) { x->Event = Event; clock_delay(x->Clock, 0); } - return (w + 4); } // ───────────────────────────────────── -static void AddDsp(OScofo *x, t_signal **sp) { +static void AddDsp(PdOScofo *x, t_signal **sp) { LOGE() << "AddDsp"; x->BlockSize = sp[0]->s_n; x->BlockIndex = 0; @@ -244,7 +232,7 @@ static void AddDsp(OScofo *x, t_signal **sp) { static void *NewOScofo(t_symbol *s, int argc, t_atom *argv) { LOGE() << "NewOScofo"; - OScofo *x = (OScofo *)pd_new(OScofoObj); + PdOScofo *x = (PdOScofo *)pd_new(OScofoObj); x->EventIndex = outlet_new(&x->xObj, &s_float); x->Tempo = outlet_new(&x->xObj, &s_float); x->WindowSize = 4096.0f; @@ -279,23 +267,16 @@ static void *NewOScofo(t_symbol *s, int argc, t_atom *argv) { x->Clock = clock_new(x, (t_method)ClockTick); x->Event = -1; - - x->Score = OScofoScore(); - x->MIR = OScofoMIR(x->Sr, x->WindowSize, x->HopSize); - x->MDP = OScofoMDP(x->Sr, x->WindowSize, x->HopSize); - x->Desc = Description(); + x->OpenScofo = new OScofo(x->Sr, x->WindowSize, x->HopSize); LOGE() << "Returning NewOScofo"; return x; } // ───────────────────────────────────── -static void *FreeOScofo(OScofo *x) { +static void *FreeOScofo(PdOScofo *x) { LOGE() << "Start Free of NewOScofo"; - // delete x->Score; - // delete x->MIR; - // delete x->MDP; - // delete x->Desc; + delete x->OpenScofo; LOGE() << "End Free of NewOScofo"; return nullptr; } @@ -305,15 +286,9 @@ extern "C" void setup_o0x2escofo_tilde(void) { OScofoObj = class_new(gensym("o.scofo~"), (t_newmethod)NewOScofo, (t_method)FreeOScofo, sizeof(OScofo), CLASS_DEFAULT, A_GIMME, 0); - CLASS_MAINSIGNALIN(OScofoObj, OScofo, Sample); + CLASS_MAINSIGNALIN(OScofoObj, PdOScofo, Sample); class_addmethod(OScofoObj, (t_method)AddDsp, gensym("dsp"), A_CANT, 0); class_addmethod(OScofoObj, (t_method)Score, gensym("score"), A_SYMBOL, 0); class_addmethod(OScofoObj, (t_method)Start, gensym("start"), A_NULL, 0); - - // Set Configurations class_addmethod(OScofoObj, (t_method)Set, gensym("set"), A_GIMME, 0); - - // template - class_addmethod(OScofoObj, (t_method)GerenateAnalTemplate, gensym("template"), A_SYMBOL, - A_FLOAT, A_FLOAT, A_FLOAT, 0); } diff --git a/Sources/Python/OScofo/__init__.py b/Sources/Python/OScofo/__init__.py new file mode 100644 index 0000000..4230b7e --- /dev/null +++ b/Sources/Python/OScofo/__init__.py @@ -0,0 +1,7 @@ +import os +import sys + +current_file_directory = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(current_file_directory) + +from _OScofo import OScofo diff --git a/Sources/Python/PyOScofo.cpp b/Sources/Python/PyOScofo.cpp new file mode 100644 index 0000000..b8a1987 --- /dev/null +++ b/Sources/Python/PyOScofo.cpp @@ -0,0 +1,35 @@ +#include + +#include "../OScofo.hpp" // Assuming this header file defines your OScofo class +#include +#include + +namespace py = pybind11; + +PYBIND11_MODULE(_OScofo, m) { + py::class_(m, "OScofo") + .def(py::init()) + .def("ParseScore", &OScofo::ParseScore) + .def("GetLiveBPM", &OScofo::GetLiveBPM) + .def("SetPitchTemplateSigma", &OScofo::SetPitchTemplateSigma) + .def("SetHarmonics", &OScofo::SetHarmonics) + .def("SetTimeAccumFactor", &OScofo::SetTimeAccumFactor) + .def("SetTimeCouplingStrength", &OScofo::SetTimeCouplingStrength) + .def("SetdBTreshold", &OScofo::SetdBTreshold) + .def("SetTunning", &OScofo::SetTunning) + .def("SetCurrentEvent", &OScofo::SetCurrentEvent) + .def("ScoreIsLoaded", &OScofo::ScoreIsLoaded) + .def("GetEventIndex", &OScofo::GetEventIndex) + .def("GetError", &OScofo::GetError) + .def("ProcessBlock", [](OScofo &self, py::array_t Audio) { + py::buffer_info bufInfo = Audio.request(); + if (bufInfo.ndim != 1) { + throw std::runtime_error("Input array must be 1-dimensional"); + } + std::vector CppAudio(static_cast(bufInfo.ptr), + static_cast(bufInfo.ptr) + bufInfo.shape[0]); + + bool result = self.ProcessBlock(CppAudio); + return result; + }); +} diff --git a/Tests/Tests.py b/Tests/Tests.py new file mode 100644 index 0000000..0942c64 --- /dev/null +++ b/Tests/Tests.py @@ -0,0 +1,44 @@ +import os +import unittest + +import numpy +import soundfile as sf + +from OScofo import OScofo + + +class ProcessBlock(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.sr = 48000 + cls.windowSize = 4096 + cls.blockSize = 1024 + cls.OScofo = OScofo(48000, 4096, 1024) + cls.root = os.path.dirname(os.path.abspath(__file__)) + cls.data, cls.fs = sf.read(cls.root + "/Test0.wav") + # check dimensions of the data + if len(cls.data.shape) > 1: + cls.data = cls.data[:, 0] + + def test_LoadScore(self): + self.OScofo.ParseScore(self.root + "/Test0.txt") + print("Score loaded") + + def test_ProcessBlock(self): + # get numpy dimensions of the data + event = -1 + onset = 0 + self.OScofo.SetdBTreshold(-50) + for i in range(0, len(self.data), self.blockSize): + audioBlock = self.data[i:i + self.blockSize] + onset += 1 / (self.sr / self.blockSize) + self.OScofo.ProcessBlock(audioBlock) + if self.OScofo.GetEventIndex() != event: + event = self.OScofo.GetEventIndex() + print("Event is {} | Onset {}".format(event, onset)) + # print("Index is {}".format(i)) + # print("Processing done") + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/oscofo-test.pd b/Tests/oscofo-test.pd index d97363e..8adc629 100644 --- a/Tests/oscofo-test.pd +++ b/Tests/oscofo-test.pd @@ -2,7 +2,7 @@ #X declare -lib timbreIDLib; #X obj -186 -1 bng 60 250 50 0 empty again empty 17 7 0 10 #e4e4e4 #000000 #373737; #X obj -67 64 f; -#X obj -9 350 nbx 13 26 -1e+37 1e+37 0 0 empty empty empty 0 -8 0 10 #e4e4e4 #373737 #373737 0 256; +#X obj -12 350 nbx 13 26 -1e+37 1e+37 0 0 empty empty empty 0 -8 0 10 #e4e4e4 #373737 #373737 0 256; #X obj -186 350 nbx 13 26 -1e+37 1e+37 0 0 empty empty empty 0 -8 0 10 #e4e4e4 #373737 #373737 0 256; #X obj -186 175 tabplay~ audio; #X obj -67 175 soundfiler; @@ -50,32 +50,25 @@ #X obj -117 -92 bng 42 250 50 0 empty empty empty 17 7 0 10 #e4e4e4 #000000 #373737; #X obj 11 -26 nbx 6 34 -1e+37 1e+37 0 0 empty empty empty 0 -8 0 10 #e4e4e4 #373737 #373737 0 256; #X obj -38 24 f 1; -#X obj 47 81 nbx 4 18 -1e+37 1e+37 0 0 empty empty empty 0 -8 0 10 #e4e4e4 #373737 #373737 0 256; +#X obj 47 91 nbx 4 18 -1e+37 1e+37 0 0 empty empty empty 0 -8 0 10 #e4e4e4 #373737 #373737 0 256; #X obj 250 -92 declare -lib timbreIDLib; -#X obj 276 217 nbx 17 30 -1e+37 1e+37 0 0 empty empty empty 0 -8 0 10 #e4e4e4 #373737 #373737 0 256; -#X obj 276 188 specFlatness~ 1024; -#X obj 330 88 bng 25 250 50 0 empty empty empty 17 7 0 10 #e4e4e4 #000000 #373737; -#X obj 366 8 metro 300; -#X obj 365 -52 tgl 25 0 empty empty empty 17 7 0 10 #e4e4e4 #000000 #373737 0 1; -#X obj 276 256 print; -#X obj 62 254 adc~ 1; #X obj -186 311 o.scofo~, f 33; +#X msg 138 91 0; #X connect 0 0 15 0; #X connect 1 0 7 0; #X connect 2 0 10 1; #X connect 3 0 11 0; #X connect 4 0 6 0; #X connect 4 0 6 1; -#X connect 4 0 29 0; -#X connect 4 0 35 0; +#X connect 4 0 28 0; #X connect 7 0 16 0; #X connect 7 1 20 0; -#X connect 8 0 35 0; -#X connect 9 0 35 0; +#X connect 8 0 28 0; +#X connect 9 0 28 0; #X connect 10 0 14 0; #X connect 11 0 10 0; #X connect 11 1 14 1; -#X connect 13 0 35 0; +#X connect 13 0 28 0; #X connect 15 0 4 0; #X connect 15 1 13 0; #X connect 16 0 5 0; @@ -89,11 +82,6 @@ #X connect 24 0 25 1; #X connect 25 0 1 1; #X connect 25 0 26 0; -#X connect 28 0 33 0; -#X connect 29 0 28 0; -#X connect 30 0 29 0; -#X connect 31 0 30 0; -#X connect 32 0 31 0; -#X connect 34 0 35 0; -#X connect 35 0 3 0; -#X connect 35 1 2 0; +#X connect 28 0 3 0; +#X connect 28 1 2 0; +#X connect 29 0 20 0; diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2c53afe --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +import os +import shutil + +from setuptools import find_packages, setup + +setup( + name="OScofo", + version="0.1.0", # Replace with your versioning + author="Charles K. Neimog", + author_email="your.email@example.com", # Replace with your email + description="", + long_description=open('README.md').read(), + long_description_content_type='text/markdown', + url="https://github.com/charlesneimog/OScofo", + packages=find_packages(where='Sources/Python'), + package_dir={'': 'Sources/Python'}, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.10', + install_requires=[ + 'pybind11', + ], + package_data={ + 'OScofo': ['*.so'], + }, + include_package_data=True, + zip_safe=False, +) +