diff --git a/PitchTracker/Helper.h b/PitchTracker/Helper.h new file mode 100644 index 00000000..f0416375 --- /dev/null +++ b/PitchTracker/Helper.h @@ -0,0 +1,61 @@ +#pragma once +#include "pitch.hh" +#include "PitchWrapper.h" + +#pragma managed(push, off) + +#include +#include + +float* short2FloatArray(short* in, size_t len){ + const float maxShort = 32768.0; //maximum abs value of a short + + float* result = static_cast(malloc(len * sizeof(float))); + float* curOut = result; + for(int i=0; i(malloc(len * sizeof(double))); + double* curOut = result; + for(int i=0; iFreq = tone.freq; + result->DB = tone.db; + result->Stabledb = tone.stabledb; + result->Age = static_cast(tone.age); + result->NoteExact = 12.0 * std::log(tone.freq / 127.09) / std::log(2.0) + 27.5 - 16; // 16 = C2 + result->Note = static_cast(std::round(result->NoteExact)); + return result; + } + } +} \ No newline at end of file diff --git a/PitchTracker/License.txt b/PitchTracker/License.txt new file mode 100644 index 00000000..0db6541a --- /dev/null +++ b/PitchTracker/License.txt @@ -0,0 +1,2 @@ +Some code is taken from Performous: https://github.com/performous/performous/tree/master/game +From 25.03.2014 \ No newline at end of file diff --git a/PitchTracker/Performous.txt b/PitchTracker/Performous.txt new file mode 100644 index 00000000..5861375b --- /dev/null +++ b/PitchTracker/Performous.txt @@ -0,0 +1,346 @@ +GNU GPLv2 or later. Some parts of the project may have more liberal licenses. +See Authors.txt for information on copyright holders and licenses of specific +parts. Fonts are licensed under SIL OFL. + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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 2 of the License, or + (at your option) any later version. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + diff --git a/PitchTracker/PitchTracker.vcxproj b/PitchTracker/PitchTracker.vcxproj index 73e5ca52..e17ebdcd 100644 --- a/PitchTracker/PitchTracker.vcxproj +++ b/PitchTracker/PitchTracker.vcxproj @@ -167,10 +167,35 @@ + + + + true + true + true + true + false + false + false + false + false + false + Default + Default + + + + + + + + + + diff --git a/PitchTracker/PitchTracker.vcxproj.filters b/PitchTracker/PitchTracker.vcxproj.filters index aa287032..f122cd3f 100644 --- a/PitchTracker/PitchTracker.vcxproj.filters +++ b/PitchTracker/PitchTracker.vcxproj.filters @@ -18,10 +18,42 @@ Quelldateien + + Quelldateien + + + Quelldateien + + + Quelldateien + Headerdateien + + Headerdateien + + + Headerdateien + + + Headerdateien + + + Headerdateien + + + Headerdateien + + + + + Headerdateien + + + Headerdateien + \ No newline at end of file diff --git a/PitchTracker/PitchWrapper.cpp b/PitchTracker/PitchWrapper.cpp new file mode 100644 index 00000000..6c18714e --- /dev/null +++ b/PitchTracker/PitchWrapper.cpp @@ -0,0 +1,92 @@ +#include "PitchWrapper.h" +#pragma managed(push, off) +#include "pitchTracker.h" +#pragma managed(pop) + +#include "Helper.h" + +namespace Native{ + namespace PitchTracking{ + + //CAnalyzer + + void CAnalyzer::Input(array^ data){ + if(data->Length == 0) + return; + pin_ptr dataPtr = &data[0]; + float* dataPtr2 = dataPtr; + m_analyzer->input(dataPtr2, dataPtr2 + data->Length); + } + + void CAnalyzer::Input(array^ data){ + if(data->Length == 0) + return; + pin_ptr dataPtr = &data[0]; + float* dataFloat = short2FloatArray(dataPtr, data->Length); + m_analyzer->input(dataFloat, dataFloat + data->Length); + freeFloatArray(dataFloat); + } + + void CAnalyzer::Process(){ + m_analyzer->process(); + } + + double CAnalyzer::GetPeak(){ + return m_analyzer->getPeak(); + } + + array^ CAnalyzer::GetTones(){ + Analyzer::tones_t umTones = m_analyzer->getTones(); + array^ tones = gcnew array(static_cast(umTones.size())); + int i = 0; + for(auto it=umTones.begin(); it!=umTones.end(); it++, i++){ + tones[i]=ConvertToneToManaged(*it); + } + return tones; + } + + CTone^ CAnalyzer::FindTone(){ + return FindTone(65.0); + } + + CTone^ CAnalyzer::FindTone(double minFreq){ + return FindTone(minFreq, 1000.0); + } + + CTone^ CAnalyzer::FindTone(double minFreq, double maxFreq){ + const Tone* tone = m_analyzer->findTone(minFreq, maxFreq); + return (tone == NULL) ? nullptr : ConvertToneToManaged(*tone); + } + + void CAnalyzer::Output(array^ data, float rate){ + pin_ptr dataPtr = &data[0]; + m_analyzer->output(dataPtr, dataPtr + data->Length, rate); + } + + String^ CAnalyzer::GetId(){ + return m_id; + } + + + //CPitchTracker + + void CPitchTracker::Init(double baseToneFrequency, int minHalfTone, int maxHalfTone){ + PitchTracker::Init(baseToneFrequency, minHalfTone, maxHalfTone); + } + + void CPitchTracker::DeInit(){ + PitchTracker::DeInit(); + } + + int CPitchTracker::GetTone(array^ samples, array^ weights){ + if(samples->Length == 0) + return -1; + pin_ptr dataPtr = &samples[0]; + pin_ptr weightsPtr = &weights[0]; + double* dataDouble = short2DoubleArray(dataPtr, samples->Length, false); + int result = PitchTracker::GetTone(dataDouble, samples->Length, weightsPtr, true); + freeDoubleArray(dataDouble); + return result; + } + } +} \ No newline at end of file diff --git a/PitchTracker/PitchWrapper.h b/PitchTracker/PitchWrapper.h new file mode 100644 index 00000000..617c24f3 --- /dev/null +++ b/PitchTracker/PitchWrapper.h @@ -0,0 +1,62 @@ +#pragma once + +#pragma managed(push, off) +#include "pitch.hh" +#pragma managed(pop) + +using namespace System; +using namespace System::Collections::Generic; + +namespace Native{ + namespace PitchTracking{ + public ref class CTone { + public: + static const int MAXHARM = 48; ///< The maximum number of harmonics tracked + static const int MINAGE = 2; ///< The minimum age required for a tone to be output + double Freq; ///< Frequency (Hz) + double DB; ///< Level (dB) + double Stabledb; ///< Stable level, useful for graphics rendering + int Age; ///< How many times the tone has been detected in row + double NoteExact; + int Note; + /// Less-than compare by levels (instead of frequencies like operator< does) + static bool dbCompare(CTone l, CTone r) { return l.DB < r.DB; } + }; + + public ref class CAnalyzer { + public: + CAnalyzer(double rate, String^ id) { + m_analyzer = new Analyzer(rate,""); + m_id = id; + } + CAnalyzer(double rate, String^ id, unsigned step) { + m_analyzer = new Analyzer(rate,"",step); + m_id = id; + } + ~CAnalyzer() { this->!CAnalyzer(); } + !CAnalyzer() { delete m_analyzer; } + + void Input(array^ data); + void Input(array^ data); + void Process(); + double GetPeak(); + array^ GetTones(); + CTone^ FindTone(); + CTone^ FindTone(double minFreq); + CTone^ FindTone(double minFreq, double maxFreq); + void Output(array^ data, float rate); + String^ GetId(); + + private: + Analyzer* m_analyzer; + String^ m_id; + }; + + public ref class CPitchTracker abstract sealed{ + public: + static void Init(double baseToneFrequency, int minHalfTone, int maxHalfTone); + static void DeInit(); + static int GetTone(array^ samples, array^ weights); + }; + } +} \ No newline at end of file diff --git a/PitchTracker/compatibility.cpp b/PitchTracker/compatibility.cpp new file mode 100644 index 00000000..207b750d --- /dev/null +++ b/PitchTracker/compatibility.cpp @@ -0,0 +1,30 @@ +#include "compatibility.h" + +#include + +#ifdef _MSC_VER +#if(_MSC_VER < 1800) // < VS2013 +namespace std +{ + float round(float d) + { + return static_cast(floor(d + 0.5)); + } + + double round(double d) + { + return floor(d + 0.5); + } + + long double round(long double d) + { + return floor(d + 0.5); + } + +} + +double remainder(double num, double denom){ + return num - std::round(num/denom) * denom; +} +#endif +#endif \ No newline at end of file diff --git a/PitchTracker/compatibility.h b/PitchTracker/compatibility.h new file mode 100644 index 00000000..ebdf8a88 --- /dev/null +++ b/PitchTracker/compatibility.h @@ -0,0 +1,17 @@ +//Contains some definitions to make the code work with compilers that do not support all feautures + +#pragma once + +#ifdef _MSC_VER + #define constexpr const + #if(_MSC_VER < 1800) // < VS2013 + namespace std + { + float round(float d); + double round(double d); + long double round(long double d); + } + + double remainder(double num, double denom); + #endif +#endif \ No newline at end of file diff --git a/PitchTracker/libda/fft.hpp b/PitchTracker/libda/fft.hpp new file mode 100644 index 00000000..1611c2fb --- /dev/null +++ b/PitchTracker/libda/fft.hpp @@ -0,0 +1,84 @@ +#pragma once + +/** + * @file fft.hpp FFT and related facilities. + */ + +#include "sample.hpp" +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.141592653589793 +#endif + +#define M_TAU (2.0 * M_PI) + +namespace da { + + // With g++ optimization -fcx-limited-range should be used for 5x performance boost. + + // Based on the description of Volodymyr Myrnyy in + // http://www.dspdesignline.com/showArticle.jhtml?printableArticle=true&articleId=199903272 + template struct DanielsonLanczos { + static void apply(std::complex* data) { + constexpr std::size_t N = 1 << P; + constexpr std::size_t M = N / 2; + // Compute even and odd halves + DanielsonLanczos

().apply(data); + DanielsonLanczos

().apply(data + M); + // Combine the results + std::complex w(1.0); + for (std::size_t i = 0; i < M; ++i) { + const std::complex temp = data[i + M] * w; + data[M + i] = data[i] - temp; + data[i] += temp; + w *= std::polar(1.0, -M_TAU / N); + } + } + }; + + template struct DanielsonLanczos<0, T> { static void apply(std::complex*) {} }; + + /** Perform FFT on data. **/ + template void fft(std::complex* data) { + // Perform bit-reversal sorting of sample data. + constexpr std::size_t N = 1 << P; + std::size_t j = 0; + for (std::size_t i = 0; i < N; ++i) { + if (i < j) std::swap(data[i], data[j]); + std::size_t m = N / 2; + while (m > 1 && m <= j) { j -= m; m >>= 1; } + j += m; + } + // Do the actual calculation + DanielsonLanczos::apply(data); + } + + /** Perform FFT on data from floating point iterator, windowing the input. **/ + template std::vector > fft(InIt begin, Window window) { + std::vector > data(1 << P); + // Perform bit-reversal sorting of sample data. + constexpr std::size_t N = 1 << P; + std::size_t j = 0; + for (std::size_t i = 0; i < N; ++i) { + data[j] = *begin++ * window[i]; + std::size_t m = N / 2; + while (m > 1 && m <= j) { j -= m; m >>= 1; } + j += m; + } + // Do the actual calculation + DanielsonLanczos::apply(&data[0]); + return data; + } + + template void ifft(std::complex* data) { + constexpr std::size_t N = 1 << P; + for (std::size_t i = 0; i < N; ++i) data[i] = std::conj(data[i]); // Invert phase so that we can use FFT to do IFFT + fft

(data); + constexpr std::complex scale(1.0/N, 0.0); + for (std::size_t i = 0; i < N; ++i) data[i] = scale * std::conj(data[i]); // Invert back, and apply IFFT scaling + } + +} diff --git a/PitchTracker/libda/sample.hpp b/PitchTracker/libda/sample.hpp new file mode 100644 index 00000000..a09bbcb6 --- /dev/null +++ b/PitchTracker/libda/sample.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include "../compatibility.h" + +/** + * @file sample.hpp Sample format definition and format conversions. + */ + +namespace da { + + using std::round; + + // Should be a floating-point type + typedef float sample_t; + + // A helper function for clamping a value to a certain range + template T clamp(T val, T min, T max) { + if (val < min) val = min; + if (val > max) val = max; + return val; + } + + constexpr sample_t max_s16(32767), min_s16 = -max_s16 - sample_t(1); + constexpr sample_t max_s24(8388607), min_s24 = -max_s24 - sample_t(1); + constexpr sample_t max_s32(2147483647), min_s32= -max_s32 - sample_t(1); + constexpr double tau(6.2831853071795864769252867665590057683943387987502116); ///< One full circle + constexpr double pi = tau / sample_t(2); ///< Half circle + constexpr double eps(1e-10); ///< Tiny positive value + + /// Mathematical sinc function + template static inline Float msinc(Float x) { return std::abs(x) < Float(eps) ? Float(1) : std::sin(x) / x; } + + /// Normalized (signal processing) sinc function + template static inline Float sinc(Float x) { return msinc(Float(pi) * x); } + + /// Lanczos kernel of size A + template static inline Float lanc(Float x) { + return std::abs(x) < A ? sinc(x) * sinc(x / A) : Float(); + } + + // The following conversions provide lossless conversions between floats + // and integers. Be sure to use only these conversions or otherwise the + // conversions may not be lossless, due to different scaling factors being + // used by different libraries. + + // The negative minimum integer value produces sample_t value slightly + // more negative than -1.0 but this is necessary in order to prevent + // clipping in the float-to-int conversions. Now amplitude 1.0 in floating + // point produces -32767 .. 32767 symmetrical non-clipping range in s16. + + static inline sample_t conv_from_s16(int s) { return s / max_s16; } + static inline sample_t conv_from_s24(int s) { return s / max_s24; } + static inline sample_t conv_from_s32(int s) { return s / max_s32; } + // The rounding is strictly not necessary, but it greatly improves + // the error tolerance if any floating point calculations are done. + // The ugly static_casts are required to avoid warnings in MSVC. + static inline int conv_to_s16(sample_t s) { return clamp(static_cast(round(s * max_s16)), static_cast(min_s16), static_cast(max_s16)); } + static inline int conv_to_s24(sample_t s) { return clamp(static_cast(round(s * max_s24)), static_cast(min_s24), static_cast(max_s24)); } + static inline int conv_to_s32(sample_t s) { return static_cast(clamp(round(s * max_s32), min_s32, max_s32 )); } + // Non-rounding non-clamping versions are provided for very low end devices (still lossless) + static inline int conv_to_s16_fast(sample_t s) { return static_cast(s * max_s16); } + static inline int conv_to_s24_fast(sample_t s) { return static_cast(s * max_s24); } + static inline int conv_to_s32_fast(sample_t s) { return static_cast(s * max_s32); } + + template class step_iterator: public std::iterator { + ValueType* m_pos; + std::ptrdiff_t m_step; + public: + step_iterator(ValueType* pos, std::ptrdiff_t step): m_pos(pos), m_step(step) {} + ValueType& operator*() { return *m_pos; } + step_iterator operator+(std::ptrdiff_t rhs) { return step_iterator(m_pos + m_step * rhs, m_step); } + step_iterator& operator++() { m_pos += m_step; return *this; } + step_iterator operator++(int) { step_iterator ret = *this; ++*this; return ret; } + bool operator!=(step_iterator const& rhs) const { return m_pos != rhs.m_pos; } + std::ptrdiff_t operator-(step_iterator const& rhs) const { return (m_pos - rhs.m_pos) / m_step; } + // TODO: more operators + }; + + typedef step_iterator sample_iterator; + typedef step_iterator sample_const_iterator; +} diff --git a/PitchTracker/pitch.cc b/PitchTracker/pitch.cc new file mode 100644 index 00000000..d4b9d9a0 --- /dev/null +++ b/PitchTracker/pitch.cc @@ -0,0 +1,233 @@ +#include "pitch.hh" + +#include "util.hh" +#include "libda/fft.hpp" +#include +#include +#include + +// Limit the range to avoid noise and useless computation +static const double FFT_MINFREQ = 45.0; +static const double FFT_MAXFREQ = 5000.0; + +Tone::Tone(): + freq(0.0), + db(-getInf()), + stabledb(-getInf()), + age() +{ + for (auto& h: harmonics) h = -getInf(); +} + +void Tone::print() const { + if (age < Tone::MINAGE) return; + std::cout << std::fixed << std::setprecision(1) << freq << " Hz, age " << age << ", " << db << " dB:"; + for (std::size_t i = 0; i < 8; ++i) std::cout << " " << harmonics[i]; + std::cout << std::endl; +} + +bool Tone::operator==(double f) const { + return std::abs(freq / f - 1.0) < 0.05; +} + +Analyzer::Analyzer(double rate, std::string id, std::size_t step): + m_step(step), + m_resampleFactor(1.0), + m_resamplePos(), + m_rate(rate), + m_id(id), + m_window(FFT_N), + m_fftLastPhase(FFT_N / 2), + m_peak(0.0), + m_oldfreq(0.0) +{ + if (m_step > FFT_N) throw std::logic_error("Analyzer step is larger that FFT_N (ideally it should be less than a fourth of FFT_N)."); + // Hamming window + for (size_t i=0; i < FFT_N; i++) { + m_window[i] = static_cast(0.53836 - 0.46164 * std::cos(2.0 * M_PI * i / (FFT_N - 1))); + } +} + +void Analyzer::output(float* begin, float* end, double rate) { + constexpr unsigned a = 2; + const unsigned size = m_passthrough.size(); + const unsigned out = static_cast((end - begin) / 2 /* stereo */); + if (out == 0) return; + const unsigned in = static_cast(m_resampleFactor * (m_rate / rate) * out + 2 * a /* lanczos kernel */ + 5 /* safety margin for rounding errors */); + float pcm[m_passthrough.capacity]; + m_passthrough.read(pcm, pcm + in + 4); + for (unsigned i = 0; i < out; ++i) { + double s = 0.0; + unsigned k = static_cast(m_resamplePos); + double x = m_resamplePos - k; + // Lanczos sampling of input at m_resamplePos + for (unsigned j = 0; j <= 2 * a; ++j) s += pcm[k + j] * da::lanc(x - j + a); + s *= 5.0; + begin[i * 2] += (float) s; + begin[i * 2 + 1] += (float) s; + m_resamplePos += m_resampleFactor; + } + unsigned num = static_cast(m_resamplePos); + m_resamplePos -= num; + if (size > 3000) { + // Reset + m_passthrough.pop(m_passthrough.size() - 700); + m_resampleFactor = 1.0; + } else { + m_passthrough.pop(num); + m_resampleFactor = 0.99 * m_resampleFactor + 0.01 * (size > 700 ? 1.02 : 0.98); + } +} + + +namespace { + struct Peak { + double freq; + double db; + bool harm[Tone::MAXHARM]; + Peak(double _freq = 0.0, double _db = -getInf()): + freq(_freq), db(_db) + { + for (auto& h: harm) h = false; + } + void clear() { + freq = 0.0; + db = -getInf(); + } + }; + + Peak& match(std::vector& peaks, std::size_t pos) { + std::size_t best = pos; + if (peaks[pos - 1].db > peaks[best].db) best = pos - 1; + if (peaks[pos + 1].db > peaks[best].db) best = pos + 1; + return peaks[best]; + } +} + +bool Analyzer::calcFFT() { + float pcm[FFT_N]; + // Read FFT_N samples, move forward by m_step samples + if (!m_buf.read(pcm, pcm + FFT_N)) return false; + m_buf.pop(static_cast(m_step)); + // Peak level calculation of the most recent m_step samples (the rest is overlap) + for (float const* ptr = pcm + FFT_N - m_step; ptr != pcm + FFT_N; ++ptr) { + float s = *ptr; + float p = s * s; + if (p > m_peak) m_peak = p; else m_peak *= 0.999; + } + // Calculate FFT + m_fft = da::fft(pcm, m_window); + return true; +} + +void Analyzer::calcTones() { + // Precalculated constants + const double freqPerBin = m_rate / FFT_N; + const double phaseStep = 2.0 * M_PI * m_step / FFT_N; + const double normCoeff = 1.0 / FFT_N; + const double minMagnitude = pow(10, -100.0 / 20.0) / normCoeff; // -100 dB + // Limit frequency range of processing + const size_t kMin = std::max(size_t(1), size_t(FFT_MINFREQ / freqPerBin)); + const size_t kMax = std::min(FFT_N / 2, size_t(FFT_MAXFREQ / freqPerBin)); + std::vector peaks(kMax + 1); // One extra to simplify loops + for (size_t k = 1; k <= kMax; ++k) { + double magnitude = std::abs(m_fft[k]); + double phase = std::arg(m_fft[k]); + // process phase difference + double delta = phase - m_fftLastPhase[k]; + m_fftLastPhase[k] = static_cast(phase); + delta -= k * phaseStep; // subtract expected phase difference + delta = remainder(delta, 2.0 * M_PI); // map delta phase into +/- M_PI interval + delta /= phaseStep; // calculate diff from bin center frequency + double freq = (k + delta) * freqPerBin; // calculate the true frequency + if (freq > 1.0 && magnitude > minMagnitude) { + peaks[k].freq = freq; + peaks[k].db = 20.0 * log10(normCoeff * magnitude); + } + } + // Prefilter peaks + double prevdb = peaks[0].db; + for (size_t k = 1; k < kMax; ++k) { + double db = peaks[k].db; + if (db > prevdb) peaks[k - 1].clear(); + if (db < prevdb) peaks[k].clear(); + prevdb = db; + } + // Find the tones (collections of harmonics) from the array of peaks + tones_t tones; + for (size_t k = kMax - 1; k >= kMin; --k) { + if (peaks[k].db < -70.0) continue; + // Find the best divider for getting the fundamental from peaks[k] + std::size_t bestDiv = 1; + int bestScore = 0; + for (std::size_t div = 2; div <= Tone::MAXHARM && k / div > 1; ++div) { + double freq = peaks[k].freq / div; // Fundamental + int score = 0; + for (std::size_t n = 1; n < div && n < 8; ++n) { + Peak& p = match(peaks, k * n / div); + --score; + if (p.db < -90.0 || std::abs(p.freq / n / freq - 1.0) > .03) continue; + if (n == 1) score += 4; // Extra for fundamental + score += 2; + } + if (score > bestScore) { + bestScore = score; + bestDiv = div; + } + } + // Construct a Tone by combining the fundamental frequency (freq) and all harmonics + Tone t; + std::size_t count = 0; + double freq = peaks[k].freq / bestDiv; + t.db = peaks[k].db; + for (std::size_t n = 1; n <= bestDiv; ++n) { + // Find the peak for n'th harmonic + Peak& p = match(peaks, k * n / bestDiv); + if (std::abs(p.freq / n / freq - 1.0) > .03) continue; // Does it match the fundamental freq? + if (p.db > t.db - 10.0) { + t.db = std::max(t.db, p.db); + ++count; + t.freq += p.freq / n; + } + t.harmonics[n - 1] = p.db; + p.clear(); + } + t.freq /= count; + // If the tone seems strong enough, add it (-3 dB compensation for each harmonic) + if (t.db > -50.0 - 3.0 * count) { + t.stabledb = t.db; + tones.push_back(t); + } + } + mergeWithOld(tones); + m_tones.swap(tones); +} + +void Analyzer::mergeWithOld(tones_t& tones) const { + tones.sort(); + auto it = tones.begin(); + // Iterate over old tones + for (auto const& old: m_tones) { + // Try to find a matching new tone + while (it != tones.end() && *it < old) ++it; + // If match found + if (it != tones.end() && *it == old) { + // Merge the old tone into the new tone + it->age = old.age + 1; + it->stabledb = 0.8 * old.stabledb + 0.2 * it->db; + it->freq = 0.5 * old.freq + 0.5 * it->freq; + } else if (old.db > -80.0) { + // Insert a decayed version of the old tone into new tones + Tone& t = *tones.insert(it, old); + t.db -= 5.0; + t.stabledb -= 0.1; + } + } +} + +void Analyzer::process() { + // Try calculating FFT and calculate tones until no more data in input buffer + while (calcFFT()) calcTones(); +} + + diff --git a/PitchTracker/pitch.hh b/PitchTracker/pitch.hh new file mode 100644 index 00000000..bb543be6 --- /dev/null +++ b/PitchTracker/pitch.hh @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "compatibility.h" + +/// struct to represent tones +struct Tone { + static const std::size_t MAXHARM = 48; ///< The maximum number of harmonics tracked + static const std::size_t MINAGE = 2; ///< The minimum age required for a tone to be output + double freq; ///< Frequency (Hz) + double db; ///< Level (dB) + double stabledb; ///< Stable level, useful for graphics rendering + double harmonics[MAXHARM]; ///< Harmonics' levels + std::size_t age; ///< How many times the tone has been detected in row + Tone(); + void print() const; ///< Prints Tone to std::cout + bool operator==(double f) const; ///< Compare for rough frequency match + /// Less-than compare by levels (instead of frequencies like operator< does) + static bool dbCompare(Tone const& l, Tone const& r) { return l.db < r.db; } +}; + +static inline bool operator==(Tone const& lhs, Tone const& rhs) { return lhs == rhs.freq; } +static inline bool operator!=(Tone const& lhs, Tone const& rhs) { return !(lhs == rhs); } +static inline bool operator<=(Tone const& lhs, Tone const& rhs) { return lhs.freq < rhs.freq || lhs == rhs; } +static inline bool operator>=(Tone const& lhs, Tone const& rhs) { return lhs.freq > rhs.freq || lhs == rhs; } +static inline bool operator<(Tone const& lhs, Tone const& rhs) { return lhs.freq < rhs.freq && lhs != rhs; } +static inline bool operator>(Tone const& lhs, Tone const& rhs) { return lhs.freq > rhs.freq && lhs != rhs; } + +static const unsigned FFT_P = 10; +static const std::size_t FFT_N = 1 << FFT_P; + +/// Lock-free ring buffer. Discards oldest data on overflow (not strictly thread-safe). +template class RingBuffer { +public: + constexpr static size_t capacity = SIZE; + RingBuffer(): m_read(), m_write() {} ///< Initialize empty buffer + template void insert(InIt begin, InIt end) { + unsigned r = static_cast(m_read); // The read position + unsigned w = static_cast(m_write); // The write position + bool overflow = false; + while (begin != end) { + m_buf[w] = *begin++; // Copy sample + w = modulo(w + 1); // Update cursor + if (w == r) overflow = true; + } + m_write = w; + if (overflow) m_read = modulo(w + 1); // Reset read pointer on overflow + } + /// Read data from current position if there is enough data to fill the range (otherwise return false). Does not move read pointer. + template bool read(OutIt begin, OutIt end) { + unsigned r = static_cast(m_read); + if (modulo(static_cast(m_write) - r) <= end - begin) return false; // Not enough audio available + while (begin != end) *begin++ = m_buf[r++ % SIZE]; // Copy audio to output iterator + return true; + } + void pop(unsigned n) { m_read = modulo(static_cast(m_read + n)); } ///< Move reading pointer forward. + unsigned size() const { return modulo(static_cast(m_write - m_read)); } +private: + static unsigned modulo(unsigned idx) { return (SIZE + idx) % SIZE; } ///< Modulo operation with proper rounding (handles slightly "negative" idx as well) + float m_buf[SIZE]; + volatile size_t m_read, m_write; ///< The indices of the next read/write operations. read == write implies that buffer is empty. +}; + +/// analyzer class + /** class to analyze input audio and transform it into useable data + */ +class Analyzer { +public: + /// fast fourier transform vector + typedef std::vector > fft_t; + /// list of tones + typedef std::list tones_t; + /// constructor + Analyzer(double rate, std::string id, std::size_t step = 200); + /** Add input data to buffer. This is thread-safe (against other functions). **/ + template void input(InIt begin, InIt end) { + m_buf.insert(begin, end); + m_passthrough.insert(begin, end); + } + /** Call this to process all data input so far. **/ + void process(); + /** Get the raw FFT. **/ + fft_t const& getFFT() const { return m_fft; } + /** Get the peak level in dB (negative value, 0.0 = clipping). **/ + double getPeak() const { return 10.0 * log10(m_peak); } + /** Get a list of all tones detected. **/ + tones_t const& getTones() const { return m_tones; } + /** Find a tone within the singing range; prefers strong tones around 200-400 Hz. **/ + Tone const* findTone(double minfreq = 65.0, double maxfreq = 1000.0) const { + if (m_tones.empty()) { m_oldfreq = 0.0; return NULL; } + double db = std::max_element(m_tones.begin(), m_tones.end(), Tone::dbCompare)->db; + Tone const* best = NULL; + double bestscore = 0; + for (tones_t::const_iterator it = m_tones.begin(); it != m_tones.end(); ++it) { + if (it->db < db - 20.0 || it->freq < minfreq || it->age < Tone::MINAGE) continue; + if (it->freq > maxfreq) break; + double score = it->db - std::max(180.0, std::abs(it->freq - 300.0)) / 10.0; + if (m_oldfreq != 0.0 && std::fabs(it->freq/m_oldfreq - 1.0) < 0.05) score += 10.0; + if (best && bestscore > score) break; + best = &*it; + bestscore = score; + } + m_oldfreq = (best ? best->freq : 0.0); + return best; + } + /** Give data away for mic pass-through */ + void output(float* begin, float* end, double rate); + /** Returns the id (color name) of the mic */ + std::string const& getId() const { return m_id; } + +private: + const std::size_t m_step; + RingBuffer<2 * FFT_N> m_buf; // Twice the FFT size should give enough room for sliding window and for engine delays + RingBuffer<4096> m_passthrough; + double m_resampleFactor; + double m_resamplePos; + double m_rate; + std::string m_id; + std::vector m_window; + fft_t m_fft; + std::vector m_fftLastPhase; + double m_peak; + tones_t m_tones; + mutable double m_oldfreq; + bool calcFFT(); + void calcTones(); + void mergeWithOld(tones_t& tones) const; +}; diff --git a/PitchTracker/pitchTracker.cpp b/PitchTracker/pitchTracker.cpp index 31e82059..4d472ee2 100644 --- a/PitchTracker/pitchTracker.cpp +++ b/PitchTracker/pitchTracker.cpp @@ -19,97 +19,102 @@ #include #include -void doLog(const char* msg,...){ - va_list argptr = NULL; - va_start(argptr, msg); +namespace PitchTracker{ - char buffer[200]; - vsprintf_s(buffer,msg, argptr); - if(Log) Log(buffer); - va_end(argptr); -} + static void doLog(const char* msg,...){ + va_list argptr = NULL; + va_start(argptr, msg); -void SetLogCallback(LogCallback Callback){ - Log=Callback; -} - -double *SamplesPerPeriodPerTone = NULL; -int MinHalfTone = 0, MaxHalfTone = 38; -const double HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave) - -void Init(double baseToneFrequency, int minHalfTone, int maxHalfTone){ - MinHalfTone = minHalfTone; - MaxHalfTone = maxHalfTone; - DeInit(); - if(maxHalfTone <= 0) - return; - SamplesPerPeriodPerTone = (double*) malloc((MaxHalfTone + 1) * sizeof(double)); - //Init Array to avoid costly calculations - for (int toneIndex = 0; toneIndex <= MaxHalfTone; toneIndex++) - { - double freq = baseToneFrequency * pow(HalftoneBase, toneIndex); - SamplesPerPeriodPerTone[toneIndex] = 44100.0 / freq; // samples in one period - } -} - -void DeInit(){ - if(SamplesPerPeriodPerTone){ - free(SamplesPerPeriodPerTone); - SamplesPerPeriodPerTone=NULL; - } -} - -static double _AnalyzeToneFunc(double *samples, int sampleCt, int toneIndex){ - double samplesPerPeriodD = SamplesPerPeriodPerTone[toneIndex]; // samples in one period - int samplesPerPeriod = (int)samplesPerPeriodD; - double fHigh = samplesPerPeriodD - samplesPerPeriod; - double fLow = 1.0 - fHigh; - - double accumDist = 0; // accumulated distances - - // compare correlating samples - int sampleIndex = 0; // index of sample to analyze - // Start value= index of sample one period ahead - for (int correlatingSampleIndex = sampleIndex + samplesPerPeriod; correlatingSampleIndex + 1 < sampleCt; correlatingSampleIndex++, sampleIndex++) - { - // calc distance to corresponding sample in next period (lower means more distant) - double dist = samples[sampleIndex] * (samples[correlatingSampleIndex] * fLow + samples[correlatingSampleIndex + 1] * fHigh); - accumDist += dist; + char buffer[200]; + vsprintf_s(buffer,msg, argptr); + if(Log) Log(buffer); + va_end(argptr); } - //Using _AnalysisBufLen here makes it return correct values among all analyzed frequencies - double scaleValue = 32767.0 * 32767.0 * sampleCt; // Divide also by Int16.MaxValue^2 - return accumDist / scaleValue; -} - -int GetTone(double *samples, int sampleCt, float *weights){ - double maxVolume = 0; - for(int i = 0; i < sampleCt; i++){ - double vol = abs(samples[i]); - if(vol > maxVolume) - maxVolume = vol; + void SetLogCallback(LogCallback Callback){ + Log=Callback; } - double maxWeight = -1; - double minWeight = 1; - int maxTone = -1; - for (int toneIndex = MinHalfTone; toneIndex <= MaxHalfTone; toneIndex++) - { - double curWeight = _AnalyzeToneFunc(samples, sampleCt, toneIndex) / maxVolume * 32767.0; - if (curWeight > maxWeight) + double *SamplesPerPeriodPerTone = NULL; + int MinHalfTone = 0, MaxHalfTone = 38; + const double HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave) + + void Init(double baseToneFrequency, int minHalfTone, int maxHalfTone){ + MinHalfTone = minHalfTone; + MaxHalfTone = maxHalfTone; + DeInit(); + if(maxHalfTone <= 0) + return; + SamplesPerPeriodPerTone = (double*) malloc((MaxHalfTone + 1) * sizeof(double)); + //Init Array to avoid costly calculations + for (int toneIndex = 0; toneIndex <= MaxHalfTone; toneIndex++) { - maxWeight = curWeight; - maxTone = toneIndex; + double freq = baseToneFrequency * pow(HalftoneBase, toneIndex); + SamplesPerPeriodPerTone[toneIndex] = 44100.0 / freq; // samples in one period + } + } + + void DeInit(){ + if(SamplesPerPeriodPerTone){ + free(SamplesPerPeriodPerTone); + SamplesPerPeriodPerTone=NULL; + } + } + + static double _AnalyzeToneFunc(double *samples, int sampleCt, int toneIndex){ + double samplesPerPeriodD = SamplesPerPeriodPerTone[toneIndex]; // samples in one period + int samplesPerPeriod = (int)samplesPerPeriodD; + double fHigh = samplesPerPeriodD - samplesPerPeriod; + double fLow = 1.0 - fHigh; + + double accumDist = 0; // accumulated distances + + // compare correlating samples + int sampleIndex = 0; // index of sample to analyze + // Start value= index of sample one period ahead + for (int correlatingSampleIndex = sampleIndex + samplesPerPeriod; correlatingSampleIndex + 1 < sampleCt; correlatingSampleIndex++, sampleIndex++) + { + // calc distance to corresponding sample in next period (lower means more distant) + double dist = samples[sampleIndex] * (samples[correlatingSampleIndex] * fLow + samples[correlatingSampleIndex + 1] * fHigh); + accumDist += dist; } - if (curWeight < minWeight) - minWeight = curWeight; - - weights[toneIndex - MinHalfTone] = (float) curWeight; + //Using _AnalysisBufLen here makes it return correct values among all analyzed frequencies + double scaleValue = sampleCt; // Divide also by Int16.MaxValue^2 + return accumDist / scaleValue; } - if(maxWeight - minWeight > 0.01){ - return maxTone; - }else return -1; -} + int GetTone(double *samples, int sampleCt, float *weights, bool scale){ + double maxVolume = 0; + for(int i = 0; i < sampleCt; i++){ + double vol = abs(samples[i]); + if(vol > maxVolume) + maxVolume = vol; + } + double maxWeight = -1; + double minWeight = 1; + int maxTone = -1; + for (int toneIndex = MinHalfTone; toneIndex <= MaxHalfTone; toneIndex++) + { + double curWeight = _AnalyzeToneFunc(samples, sampleCt, toneIndex) / maxVolume; + if(scale) + curWeight /= 32768.0; //maximum abs value of a short, would actually need to scale by MaxValue^2 but we already scaled by maxVolume + if (curWeight > maxWeight) + { + maxWeight = curWeight; + maxTone = toneIndex; + } + + if (curWeight < minWeight) + minWeight = curWeight; + + weights[toneIndex - MinHalfTone] = (float) curWeight; + } + + if(maxWeight - minWeight > 0.01){ + return maxTone; + }else return -1; + } + +} \ No newline at end of file diff --git a/PitchTracker/pitchTracker.h b/PitchTracker/pitchTracker.h index 99b477b0..961c436c 100644 --- a/PitchTracker/pitchTracker.h +++ b/PitchTracker/pitchTracker.h @@ -1,17 +1,11 @@ #pragma once -#ifndef dywapitchtrack__H -#define dywapitchtrack__H - -#define DllExport extern "C" __declspec(dllexport) - -DllExport void Init(double baseToneFrequency, int minHalfTone, int maxHalfTone); -DllExport void DeInit(); -DllExport int GetTone(double *samples, int sampleCt, float *weights); - -typedef void (__stdcall * LogCallback)(const char* text); -static LogCallback Log; -DllExport void SetLogCallback(LogCallback Callback); - -#endif +namespace PitchTracker{ + void Init(double baseToneFrequency, int minHalfTone, int maxHalfTone); + void DeInit(); + int GetTone(double *samples, int sampleCt, float *weights, bool scale = false); + typedef void (__stdcall * LogCallback)(const char* text); + static LogCallback Log; + void SetLogCallback(LogCallback Callback); +} diff --git a/PitchTracker/util.hh b/PitchTracker/util.hh new file mode 100644 index 00000000..f07753d4 --- /dev/null +++ b/PitchTracker/util.hh @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +/** Limit val to range [min, max] **/ +template T clamp(T val, T min = 0, T max = 1) { + if (min > max) throw std::logic_error("min > max"); + if (val < min) return min; + if (val > max) return max; + return val; +} + +template struct MinMax { + Numeric min, max; + explicit MinMax(Numeric min = std::numeric_limits::min(), Numeric max = std::numeric_limits::max()): min(min), max(max) {} + bool matches(Numeric val) const { return val >= min && val <= max; } +}; + +/** A convenient way for getting NaNs **/ +static inline double getNaN() { return std::numeric_limits::quiet_NaN(); } + +/** A convenient way for getting infs **/ +static inline double getInf() { return std::numeric_limits::infinity(); } + +static inline bool isPow2(unsigned int val) { + if (val == 0) return false; + if ((val & (val-1)) == 0) return true; // From Wikipedia: Power_of_two + return false; +} + +static inline unsigned int nextPow2(unsigned int val) { + unsigned int ret = 1; + while (ret < val) ret *= 2; + return ret; +} + +static inline unsigned int prevPow2(unsigned int val) { + unsigned int ret = 1; + while ((ret*2) < val) ret *= 2; + return ret; +} + diff --git a/Vocaluxe/Lib/Sound/Record/CBuffer.cs b/Vocaluxe/Lib/Sound/Record/CBuffer.cs index 9c668741..46c3c59a 100644 --- a/Vocaluxe/Lib/Sound/Record/CBuffer.cs +++ b/Vocaluxe/Lib/Sound/Record/CBuffer.cs @@ -23,8 +23,7 @@ using System; using System.IO; using System.Diagnostics; #if USE_NATIVE_DETECTION -using System.Runtime.InteropServices; - +using Native.PitchTracking; #endif #if TEST_PITCH using System.Windows.Forms; @@ -34,26 +33,6 @@ using VocaluxeLib; namespace Vocaluxe.Lib.Sound.Record { -#if USE_NATIVE_DETECTION - static class CPitchTracker - { - [DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern void Init(double baseToneFrequency, int minHalfTone, int maxHalfTone); - - [DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern void DeInit(); - - [DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern int GetTone(double[] samples, int sampleCt, float[] weights); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void LogCallback(string message); - - [DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern void SetLogCallback(LogCallback c); - } -#endif - class CBuffer : IDisposable { private static int _InitCount; @@ -67,7 +46,7 @@ namespace Vocaluxe.Lib.Sound.Record private const int _AnalysisBufLen = 4096; private const int _AnalysisByteBufLen = _AnalysisBufLen * 2; - private readonly double[] _AnalysisBuffer = new double[_AnalysisBufLen]; + private readonly short[] _AnalysisBuffer = new short[_AnalysisBufLen]; private readonly byte[] _AnalysisByteBuffer = new byte[_AnalysisByteBufLen]; //tmp buffer (stream->tmpBuffer->(Int16)AnalysisBuffer) private readonly float[] _TmpWeights = new float[NumHalfTones]; //tmp buffer for weights (gets copied to ToneWeight when checked) @@ -211,8 +190,7 @@ namespace Vocaluxe.Lib.Sound.Record _NewSamples = false; } - for (int i = 0; i < _AnalysisBufLen; i++) - _AnalysisBuffer[i] = BitConverter.ToInt16(_AnalysisByteBuffer, i * 2); + Buffer.BlockCopy(_AnalysisByteBuffer, 0, _AnalysisBuffer, 0, _AnalysisByteBufLen); try { @@ -229,22 +207,22 @@ namespace Vocaluxe.Lib.Sound.Record private void _FindMaxVolume() { - double maxVolume = 0; + int maxVolume = 0; for (int i = 0; i < _AnalysisBufLen / 4; i++) { - double volume = _AnalysisBuffer[i]; + int volume = _AnalysisBuffer[i]; if (volume < 0) volume = -volume; if (volume > maxVolume) maxVolume = volume; } - _MaxVolume = maxVolume / Int16.MaxValue; + _MaxVolume = (double)maxVolume / Int16.MaxValue; } private void _AnalyzeTones() { #if USE_NATIVE_DETECTION - int maxTone = CPitchTracker.GetTone(_AnalysisBuffer, _AnalysisBufLen, _TmpWeights); + int maxTone = CPitchTracker.GetTone(_AnalysisBuffer, _TmpWeights); if (maxTone >= 0) #else // prepare to analyze @@ -305,7 +283,6 @@ namespace Vocaluxe.Lib.Sound.Record // calc distance to corresponding sample in next period (lower means more distant) double dist = _AnalysisBuffer[sampleIndex] * (_AnalysisBuffer[correlatingSampleIndex] * fLow + _AnalysisBuffer[correlatingSampleIndex + 1] * fHigh); - //double dist = _AnalysisBuffer[sampleIndex] * _AnalysisBuffer[correlatingSampleIndex]; accumDist += dist; } diff --git a/Vocaluxe/Vocaluxe.csproj b/Vocaluxe/Vocaluxe.csproj index fcf7e6f6..fd3f9ab0 100644 --- a/Vocaluxe/Vocaluxe.csproj +++ b/Vocaluxe/Vocaluxe.csproj @@ -199,6 +199,10 @@ ..\Output\OpenTK.GLControl.dll + + ..\Output\x64\PitchTracker.dll + False + False ..\Output\PortAudioSharp.dll