Merge remote-tracking branch 'remotes/flamefire/RecordImprovements' into develop

This commit is contained in:
Florian Küpper committed 2014-04-26 19:00:11 +02:00
commit a3fca21b32
35 files changed
+4156 -462

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
#include "Helper.h"
#include <stdlib.h>
#include <cmath>
float* short2FloatArray(short* in, size_t len){
const float maxShort = 32767.0f;
float* result = static_cast<float*>(malloc(len * sizeof(float)));
float* curOut = result;
for(size_t i=0; i<len; i++){
*curOut = *in / maxShort;
curOut++;
in++;
}
return result;
}
double* short2DoubleArray(short* in, size_t len, bool scale){
const double maxShort = 32767.0;
double* result = static_cast<double*>(malloc(len * sizeof(double)));
double* curOut = result;
for(size_t i=0; i<len; i++){
*curOut = *in;
if(scale)
*curOut /= maxShort;
curOut++;
in++;
}
return result;
}
void freeFloatArray(float* floats){
free(floats);
}
void freeDoubleArray(double* floats){
free(floats);
}
double FreqToNote(double freq){
return 12.0 * std::log(freq / 127.09) / std::log(2.0) + 27.5 - 16; // 16 = C2
}
+5 -42
View File
@@ -1,47 +1,10 @@
#pragma once
#include "PitchWrapper.h"
#include <stdlib.h>
#include <cmath>
float* short2FloatArray(short* in, size_t len){
const float maxShort = 32768.0; //maximum abs value of a short
float* result = static_cast<float*>(malloc(len * sizeof(float)));
float* curOut = result;
for(size_t i=0; i<len; i++){
*curOut = *in / maxShort;
curOut++;
in++;
}
return result;
}
double* short2DoubleArray(short* in, size_t len, bool scale = true){
const double maxShort = 32768.0; //maximum abs value of a short
double* result = static_cast<double*>(malloc(len * sizeof(double)));
double* curOut = result;
for(size_t i=0; i<len; i++){
*curOut = *in;
if(scale)
*curOut /= maxShort;
curOut++;
in++;
}
return result;
}
void freeFloatArray(float* floats){
free(floats);
}
void freeDoubleArray(double* floats){
free(floats);
}
double FreqToNote(double freq){
return 12.0 * std::log(freq / 127.09) / std::log(2.0) + 27.5 - 16; // 16 = C2
}
float* short2FloatArray(short* in, size_t len);
double* short2DoubleArray(short* in, size_t len, bool scale = true);
void freeFloatArray(float* floats);
void freeDoubleArray(double* floats);
double FreqToNote(double freq);
/*
namespace Native{
+7
View File
@@ -135,6 +135,7 @@
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<FloatingPointModel>Fast</FloatingPointModel>
<OmitFramePointers>false</OmitFramePointers>
<OpenMPSupport>true</OpenMPSupport>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -157,6 +158,7 @@
<SDLCheck>true</SDLCheck>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<FloatingPointModel>Fast</FloatingPointModel>
<OpenMPSupport>true</OpenMPSupport>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -170,12 +172,17 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="compatibility.cpp" />
<ClCompile Include="dywapitchtrack\dywapitchtrack.cpp" />
<ClCompile Include="dywapitchtrack\ptDyWa.cpp" />
<ClCompile Include="Helper.cpp" />
<ClCompile Include="performous\pitch.cc" />
<ClCompile Include="ptAKF.cpp" />
<ClCompile Include="PitchWrapper.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="compatibility.h" />
<ClInclude Include="dywapitchtrack\dywapitchtrack.h" />
<ClInclude Include="dywapitchtrack\ptDyWa.h" />
<ClInclude Include="Helper.h" />
<ClInclude Include="performous\libda\fft.hpp" />
<ClInclude Include="performous\libda\sample.hpp" />
+21
View File
@@ -22,6 +22,12 @@
<Filter Include="Headerdateien\performous\libda">
<UniqueIdentifier>{d71782a0-47fc-4fe1-8569-0a0ce8fe9b45}</UniqueIdentifier>
</Filter>
<Filter Include="Quelldateien\dywapitchtrack">
<UniqueIdentifier>{e8eab624-6cc3-42db-93d9-575aaeff06f7}</UniqueIdentifier>
</Filter>
<Filter Include="Headerdateien\dywapitchtrack">
<UniqueIdentifier>{2a09cee1-51b7-4c24-aa9f-32bec53c3a36}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="PitchWrapper.cpp">
@@ -36,6 +42,15 @@
<ClCompile Include="ptAKF.cpp">
<Filter>Quelldateien</Filter>
</ClCompile>
<ClCompile Include="dywapitchtrack\ptDyWa.cpp">
<Filter>Quelldateien\dywapitchtrack</Filter>
</ClCompile>
<ClCompile Include="dywapitchtrack\dywapitchtrack.cpp">
<Filter>Quelldateien\dywapitchtrack</Filter>
</ClCompile>
<ClCompile Include="Helper.cpp">
<Filter>Quelldateien</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="compatibility.h">
@@ -56,6 +71,12 @@
<ClInclude Include="ptAKF.h">
<Filter>Headerdateien</Filter>
</ClInclude>
<ClInclude Include="dywapitchtrack\dywapitchtrack.h">
<Filter>Headerdateien\dywapitchtrack</Filter>
</ClInclude>
<ClInclude Include="dywapitchtrack\ptDyWa.h">
<Filter>Headerdateien\dywapitchtrack</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="performous\pitch.hh">
+63 -12
View File
@@ -39,17 +39,17 @@ void Analyzer_Process(Analyzer* analyzer){
analyzer->process();
}
double Analyzer_GetPeak(Analyzer* analyzer){
float Analyzer_GetPeak(Analyzer* analyzer){
if(!analyzer)
return -999;
return analyzer->getPeak();
return static_cast<float>(analyzer->getPeakRaw());
}
double Analyzer_FindNote(Analyzer* analyzer, double minFreq, double maxFreq){
if(!analyzer)
return -1;
const Tone* tone = analyzer->findTone(minFreq, maxFreq);
return (tone == NULL) ? -1 : FreqToNote(tone->freq);
return (tone == NULL) ? -1 : FreqToNote(tone->freq);
}
bool Analyzer_OutputFloat(Analyzer* analyzer, float* data, int sampleCt, float rate){
@@ -58,32 +58,83 @@ bool Analyzer_OutputFloat(Analyzer* analyzer, float* data, int sampleCt, float r
return analyzer->output(data, data + sampleCt, rate);
}
DllExport PtAKF* PtAKF_Create(){
return new PtAKF();
PtAKF* PtAKF_Create(unsigned step){
return new PtAKF(step);
}
DllExport void PtAKF_Free(PtAKF* analyzer){
void PtAKF_Free(PtAKF* analyzer){
if(analyzer)
delete analyzer;
}
DllExport int PtAKF_GetNumHalfTones(){
int PtAKF_GetNumHalfTones(){
return PtAKF::GetNumHalfTones();
}
DllExport void PtAKF_InputByte(PtAKF* analyzer, char* data, int sampleCt){
if(sampleCt <= 0 || !analyzer)
void PtAKF_SetVolumeThreshold(PtAKF* analyzer, float threshold){
if(!analyzer)
return;
short* dataShort = static_cast<short*>(static_cast<void*>(data));
analyzer->input(dataShort, dataShort + sampleCt);
analyzer->SetVolumeThreshold(threshold);
}
DllExport int PtAKF_GetNote(PtAKF* analyzer, double* maxVolume, float* weights){
float PtAKF_GetVolumeThreshold(PtAKF* analyzer){
if(!analyzer)
return 0.f;
return analyzer->GetVolumeThreshold();
}
void PtAKF_InputByte(PtAKF* analyzer, char* data, int sampleCt){
if(sampleCt <= 0 || !analyzer)
return;
//short* dataShort = static_cast<short*>(static_cast<void*>(data));
float* dataFloat = short2FloatArray(static_cast<short*>(static_cast<void*>(data)), sampleCt);
analyzer->input(dataFloat, dataFloat + sampleCt);
freeFloatArray(dataFloat);
//analyzer->input(dataShort, dataShort + sampleCt);
}
int PtAKF_GetNote(PtAKF* analyzer, float* maxVolume, float* weights){
if(!analyzer)
return -1;
return analyzer->GetNote(maxVolume, weights);
}
PtDyWa* PtDyWa_Create(unsigned step){
return new PtDyWa(step);
}
void PtDyWa_Free(PtDyWa* analyzer){
if(analyzer)
delete analyzer;
}
void PtDyWa_SetVolumeTreshold(PtDyWa* analyzer, float threshold){
if(!analyzer)
return;
analyzer->SetVolumeThreshold(threshold);
}
float PtDyWa_GetVolumeThreshold(PtDyWa* analyzer){
if(!analyzer)
return 0.f;
return analyzer->GetVolumeThreshold();
}
void PtDyWa_InputByte(PtDyWa* analyzer, char* data, int sampleCt){
if(sampleCt <= 0 || !analyzer)
return;
float* dataFloat = short2FloatArray(static_cast<short*>(static_cast<void*>(data)), sampleCt);
analyzer->input(dataFloat, dataFloat + sampleCt);
freeFloatArray(dataFloat);
}
double PtDyWa_FindNote(PtDyWa* analyzer, float* maxVolume){
if(!analyzer)
return -1;
return analyzer->FindNote(maxVolume);
}
/*
namespace Native{
namespace PitchTracking{
+13 -3
View File
@@ -2,6 +2,7 @@
#include "performous/pitch.hh"
#include "ptAKF.h"
#include "dywapitchtrack/ptDyWa.h"
#define DllExport extern "C" __declspec(dllexport)
@@ -11,15 +12,24 @@ DllExport void Analyzer_InputFloat(Analyzer* analyzer, float* data, int sampleCt
DllExport void Analyzer_InputShort(Analyzer* analyzer, short* data, int sampleCt);
DllExport void Analyzer_InputByte(Analyzer* analyzer, char* data, int sampleCt);
DllExport void Analyzer_Process(Analyzer* analyzer);
DllExport double Analyzer_GetPeak(Analyzer* analyzer);
DllExport float Analyzer_GetPeak(Analyzer* analyzer);
DllExport double Analyzer_FindNote(Analyzer* analyzer, double minFreq, double maxFreq);
DllExport bool Analyzer_OutputFloat(Analyzer* analyzer, float* data, int sampleCt, float rate);
DllExport PtAKF* PtAKF_Create();
DllExport PtAKF* PtAKF_Create(unsigned step);
DllExport void PtAKF_Free(PtAKF* analyzer);
DllExport int PtAKF_GetNumHalfTones();
DllExport void PtAKF_SetVolumeThreshold(PtAKF* analyzer, float threshold);
DllExport float PtAKF_GetVolumeThreshold(PtAKF* analyzer);
DllExport void PtAKF_InputByte(PtAKF* analyzer, char* data, int sampleCt);
DllExport int PtAKF_GetNote(PtAKF* analyzer, double* maxVolume, float* weights);
DllExport int PtAKF_GetNote(PtAKF* analyzer, float* maxVolume, float* weights);
DllExport PtDyWa* PtDyWa_Create(unsigned step);
DllExport void PtDyWa_Free(PtDyWa* analyzer);
DllExport void PtDyWa_SetVolumeTreshold(PtDyWa* analyzer,float threshold);
DllExport float PtDyWa_GetVolumeThreshold(PtDyWa* analyzer);
DllExport void PtDyWa_InputByte(PtDyWa* analyzer, char* data, int sampleCt);
DllExport double PtDyWa_FindNote(PtDyWa* analyzer, float* maxVolume);
/*
public ref class CTone {
Binary file not shown.
@@ -0,0 +1,29 @@
dywapitchtrack pitch tracking library
Documentation
The dywapitchtrack library computes the pitch of an audio stream in real time. The pitch is the main frequency of the waveform (the 'note' being played or sung). It is expressed as a float in Hz.
Unlike the human ear, pitch detection is difficult to achieve for computers. Many algorithms have been designed and experimented, but there is no 'best' algorithm. They all depend on the context and the tradeoffs acceptable in terms of speed and latency. The context includes the quality and 'cleanness' of the audio : obviously polyphonic sounds (multiple instruments playing different notes at the same time) are extremely difficult to track, percussive or noisy audio has no pitch, most real-life audio have some noisy moments, some instruments have a lot of harmonics, etc...
The dywapitchtrack is based on a custom-tailored algorithm which is of very high quality: both very accurate (precision < 0.05 semitones), very low latency (< 23 ms) and very low error rate. It has been thoroughly tested on human voice.
It can best be described as a dynamic wavelet algorithm (dywa):
The heart of the algorithm is a very powerful wavelet algorithm, described in a paper by Eric Larson and Ross Maddox :
"Real-Time Time-Domain Pitch Tracking Using Wavelets" http://online.physics.uiuc.edu/courses/phys406/NSF_REU_Reports/2005_reu/Real-Time_Time-Domain_Pitch_Tracking_Using_Wavelets.pdf
This central algorithm has been improved by adding dynamic tracking, to reduce the common problems of frequency-halving and voiced/unvoiced errors. This dynamic tracking explains the need for a tracking structure (dywapitchtracker). The dynamic tracking assumes that the main function dywapitch_computepitch is called repeatedly, as it follows the pitch over time and makes assumptions about human voice capabilities and reallife conditions (as documented inside the code).
Note : The algorithm currently assumes a 44100Hz audio sampling rate. If you use a different samplerate, you can just multiply the resulting pitch by the ratio between your samplerate and 44100.
See the dywapitchtrack.h file for the library API documentation.
History :
This algorithm has been designed during a mission for a customer. As I had kept the author's rights on the source code, I eventually decided to make this code public and open source, because it is of really high quality. This algorithms has been extensibely tested, especially it has been included in an Adobe Director Xtra (plugin), asPitchDetect Xtra, that has been distributed widely for a few years.
I hope that it can be used with pleasure and efficiency in your project.
Thank you
Antoine Schmitt April 2010
+43
View File
@@ -0,0 +1,43 @@
dywapitchtrack
Dynamic Wavelet Algorithm Pitch Tracking library
Copyright (c) 2010 Antoine Schmitt as@schmittmachine.com
-- Documentation
See the dywapitchtrack.h file for the library API documentation.
-- Version history
Inital release : Antoine Schmitt April 2010
jan 2012 : Minor update : changed the link to the Eric Larson and Ross Maddox paper
apr 2012 : Included the SpeakHere iPhone app example
-- Building, Installing
- the algorithm can be included directly in your project by including the .h and .c files.
- the iPhone/dywapitchtrack.xcodeproj builds an iPhone static library, against which one can statically link an iPhone application.
-- MIT open source licence
Copyright (c) 2010 Antoine Schmitt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,451 @@
/* dywapitchtrack.c
Dynamic Wavelet Algorithm Pitch Tracking library
Released under the MIT open source licence
Copyright (c) 2010 Antoine Schmitt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include "dywapitchtrack.h"
#include <math.h>
#include <stdlib.h>
#include <string.h> // for memset
//**********************
// Utils
//**********************
#ifndef max
#define max(x, y) ((x) > (y)) ? (x) : (y)
#endif
#ifndef min
#define min(x, y) ((x) < (y)) ? (x) : (y)
#endif
// returns 1 if power of 2
int _power2p(int value) {
if (value == 0) return 1;
if (value == 2) return 1;
if (value & 0x1) return 0;
return (_power2p(value >> 1));
}
// count number of bits
int _bitcount(int value) {
if (value == 0) return 0;
if (value == 1) return 1;
if (value == 2) return 2;
return _bitcount(value >> 1) + 1;
}
// closest power of 2 above or equal
int _ceil_power2(int value) {
if (_power2p(value)) return value;
if (value == 1) return 2;
int j, i = _bitcount(value);
int res = 1;
for (j = 0; j < i; j++) res <<= 1;
return res;
}
// closest power of 2 below or equal
int _floor_power2(int value) {
if (_power2p(value)) return value;
return _ceil_power2(value)/2;
}
// abs value
int _iabs(int x) {
if (x >= 0) return x;
return -x;
}
// 2 power
int _2power(int i) {
int res = 1, j;
for (j = 0; j < i; j++) res <<= 1;
return res;
}
//******************************
// the Wavelet algorithm itself
//******************************
int dywapitch_neededsamplecount(int minFreq) {
int nbSam = 3*44100/minFreq; // 1017. for 130 Hz
nbSam = _ceil_power2(nbSam); // 1024
return nbSam;
}
typedef struct _minmax {
int index;
struct _minmax *next;
} minmax;
double _dywapitch_computeWaveletPitch(double * samples, int startsample, int samplecount, float* maxVolume, double volThreshold) {
double pitchF = 0.0;
int i, j;
double si, si1;
// must be a power of 2
samplecount = _floor_power2(samplecount);
double *sam = (double *)malloc(sizeof(double)*samplecount);
memcpy(sam, samples + startsample, sizeof(double)*samplecount);
int curSamNb = samplecount;
int *distances = (int *)malloc(sizeof(int)*samplecount);
int *mins = (int *)malloc(sizeof(int)*samplecount);
int *maxs = (int *)malloc(sizeof(int)*samplecount);
int nbMins, nbMaxs;
// algorithm parameters
int maxFLWTlevels = 6;
double maxF = 3000.;
int differenceLevelsN = 3;
double maximaThresholdRatio = 0.75;
double ampltitudeThreshold;
double theDC = 0.0;
{ // compute ampltitudeThreshold and theDC
//first compute the DC and maxAMplitude
double maxValue = 0.0;
double minValue = 0.0;
for (i = 0; i < samplecount;i++) {
si = sam[i];
theDC = theDC + si;
if (si > maxValue) maxValue = si;
if (si < minValue) minValue = si;
}
theDC = theDC/samplecount;
maxValue = maxValue - theDC;
minValue = minValue - theDC;
double amplitudeMax = (maxValue > -minValue ? maxValue : -minValue);
*maxVolume = static_cast<float>(amplitudeMax + theDC);
if(*maxVolume < volThreshold)
return 0.0;
ampltitudeThreshold = amplitudeMax*maximaThresholdRatio;
//asLog("dywapitch theDC=%f ampltitudeThreshold=%f\n", theDC, ampltitudeThreshold);
}
// levels, start without downsampling..
int curLevel = 0;
double curModeDistance = -1.;
int delta;
while(1) {
// delta
delta = static_cast<int>(44100./(_2power(curLevel)*maxF));
//("dywapitch doing level=%ld delta=%ld\n", curLevel, delta);
if (curSamNb < 2) goto cleanup;
// compute the first maximums and minumums after zero-crossing
// store if greater than the min threshold
// and if at a greater distance than delta
double dv, previousDV = -1000;
nbMins = nbMaxs = 0;
int lastMinIndex = -1000000;
int lastmaxIndex = -1000000;
int findMax = 0;
int findMin = 0;
for (i = 2; i < curSamNb; i++) {
si = sam[i] - theDC;
si1 = sam[i-1] - theDC;
if (si1 <= 0 && si > 0) findMax = 1;
if (si1 >= 0 && si < 0) findMin = 1;
// min or max ?
dv = si - si1;
if (previousDV > -1000) {
if (findMin && previousDV < 0 && dv >= 0) {
// minimum
if (fabs(si) >= ampltitudeThreshold) {
if (i > lastMinIndex + delta) {
mins[nbMins++] = i;
lastMinIndex = i;
findMin = 0;
//if DEBUGG then put "min ok"&&si
//
} else {
//if DEBUGG then put "min too close to previous"&&(i - lastMinIndex)
//
}
} else {
// if DEBUGG then put "min "&abs(si)&" < thresh = "&ampltitudeThreshold
//--
}
}
if (findMax && previousDV > 0 && dv <= 0) {
// maximum
if (fabs(si) >= ampltitudeThreshold) {
if (i > lastmaxIndex + delta) {
maxs[nbMaxs++] = i;
lastmaxIndex = i;
findMax = 0;
} else {
//if DEBUGG then put "max too close to previous"&&(i - lastmaxIndex)
//--
}
} else {
//if DEBUGG then put "max "&abs(si)&" < thresh = "&ampltitudeThreshold
//--
}
}
}
previousDV = dv;
}
if (nbMins == 0 && nbMaxs == 0) {
// no best distance !
//asLog("dywapitch no mins nor maxs, exiting\n");
// if DEBUGG then put "no mins nor maxs, exiting"
goto cleanup;
}
//if DEBUGG then put count(maxs)&&"maxs &"&&count(mins)&&"mins"
// maxs = [5, 20, 100,...]
// compute distances
int d;
memset(distances, 0, samplecount*sizeof(int));
for (i = 0 ; i < nbMins ; i++) {
for (j = 1; j < differenceLevelsN; j++) {
if (i+j < nbMins) {
d = _iabs(mins[i] - mins[i+j]);
//asLog("dywapitch i=%ld j=%ld d=%ld\n", i, j, d);
distances[d] = distances[d] + 1;
}
}
}
for (i = 0 ; i < nbMaxs ; i++) {
for (j = 1; j < differenceLevelsN; j++) {
if (i+j < nbMaxs) {
d = _iabs(maxs[i] - maxs[i+j]);
//asLog("dywapitch i=%ld j=%ld d=%ld\n", i, j, d);
distances[d] = distances[d] + 1;
}
}
}
// find best summed distance
int bestDistance = -1;
int bestValue = -1;
for (i = 0; i< curSamNb; i++) {
int summed = 0;
for (j = -delta ; j <= delta ; j++) {
if (i+j >=0 && i+j < curSamNb)
summed += distances[i+j];
}
//asLog("dywapitch i=%ld summed=%ld bestDistance=%ld\n", i, summed, bestDistance);
if (summed == bestValue) {
if (i == 2*bestDistance)
bestDistance = i;
} else if (summed > bestValue) {
bestValue = summed;
bestDistance = i;
}
}
//asLog("dywapitch bestDistance=%ld\n", bestDistance);
// averaging
double distAvg = 0.0;
double nbDists = 0;
for (j = -delta ; j <= delta ; j++) {
if (bestDistance+j >=0 && bestDistance+j < samplecount) {
int nbDist = distances[bestDistance+j];
if (nbDist > 0) {
nbDists += nbDist;
distAvg += (bestDistance+j)*nbDist;
}
}
}
// this is our mode distance !
distAvg /= nbDists;
//asLog("dywapitch distAvg=%f\n", distAvg);
// continue the levels ?
if (curModeDistance > -1.) {
double similarity = fabs(distAvg*2 - curModeDistance);
if (similarity <= 2*delta) {
//if DEBUGG then put "similarity="&similarity&&"delta="&delta&&"ok"
//asLog("dywapitch similarity=%f OK !\n", similarity);
// two consecutive similar mode distances : ok !
pitchF = 44100./(_2power(curLevel-1)*curModeDistance);
goto cleanup;
}
//if DEBUGG then put "similarity="&similarity&&"delta="&delta&&"not"
}
// not similar, continue next level
curModeDistance = distAvg;
curLevel = curLevel + 1;
if (curLevel >= maxFLWTlevels) {
// put "max levels reached, exiting"
//asLog("dywapitch max levels reached, exiting\n");
goto cleanup;
}
// downsample
if (curSamNb < 2) {
//asLog("dywapitch not enough samples, exiting\n");
goto cleanup;
}
for (i = 0; i < curSamNb/2; i++) {
sam[i] = (sam[2*i] + sam[2*i + 1])/2.;
}
curSamNb /= 2;
}
///
cleanup:
free(distances);
free(mins);
free(maxs);
free(sam);
return pitchF;
}
// ***********************************
// the dynamic postprocess
// ***********************************
/***
It states:
- a pitch cannot change much all of a sudden (20%) (impossible humanly,
so if such a situation happens, consider that it is a mistake and drop it.
- a pitch cannot double or be divided by 2 all of a sudden : it is an
algorithm side-effect : divide it or double it by 2.
- a lonely voiced pitch cannot happen, nor can a sudden drop in the middle
of a voiced segment. Smooth the plot.
***/
double _dywapitch_dynamicprocess(dywapitchtracker *pitchtracker, double pitch) {
// equivalence
if (pitch == 0.0) pitch = -1.0;
//
double estimatedPitch = -1;
double acceptedError = 0.2f;
int maxConfidence = 5;
if (pitch != -1) {
// I have a pitch here
if (pitchtracker->_prevPitch == -1) {
// no previous
estimatedPitch = pitch;
pitchtracker->_prevPitch = pitch;
pitchtracker->_pitchConfidence = 1;
} else if (abs(pitchtracker->_prevPitch - pitch)/pitch < acceptedError) {
// similar : remember and increment pitch
pitchtracker->_prevPitch = pitch;
estimatedPitch = pitch;
pitchtracker->_pitchConfidence = min(maxConfidence, pitchtracker->_pitchConfidence + 1); // maximum 3
} else if ((pitchtracker->_pitchConfidence >= maxConfidence-2) && abs(pitchtracker->_prevPitch - 2.*pitch)/(2.*pitch) < acceptedError) {
// close to half the last pitch, which is trusted
estimatedPitch = 2.*pitch;
pitchtracker->_prevPitch = estimatedPitch;
} else if ((pitchtracker->_pitchConfidence >= maxConfidence-2) && abs(pitchtracker->_prevPitch - 0.5*pitch)/(0.5*pitch) < acceptedError) {
// close to twice the last pitch, which is trusted
estimatedPitch = 0.5*pitch;
pitchtracker->_prevPitch = estimatedPitch;
} else {
// nothing like this : very different value
if (pitchtracker->_pitchConfidence >= 1) {
// previous trusted : keep previous
estimatedPitch = pitchtracker->_prevPitch;
pitchtracker->_pitchConfidence = max(0, pitchtracker->_pitchConfidence - 1);
} else {
// previous not trusted : take current
estimatedPitch = pitch;
pitchtracker->_prevPitch = pitch;
pitchtracker->_pitchConfidence = 1;
}
}
} else {
// no pitch now
if (pitchtracker->_prevPitch != -1) {
// was pitch before
if (pitchtracker->_pitchConfidence >= 1) {
// continue previous
estimatedPitch = pitchtracker->_prevPitch;
pitchtracker->_pitchConfidence = max(0, pitchtracker->_pitchConfidence - 1);
} else {
pitchtracker->_prevPitch = -1;
estimatedPitch = -1.;
pitchtracker->_pitchConfidence = 0;
}
}
}
// put "_pitchConfidence="&pitchtracker->_pitchConfidence
if (pitchtracker->_pitchConfidence >= 1) {
// ok
pitch = estimatedPitch;
} else {
pitch = -1;
}
// equivalence
if (pitch == -1) pitch = 0.0;
return pitch;
}
// ************************************
// the API main entry points
// ************************************
void dywapitch_inittracking(dywapitchtracker *pitchtracker) {
pitchtracker->_prevPitch = -1.;
pitchtracker->_pitchConfidence = -1;
}
double dywapitch_computepitch(dywapitchtracker *pitchtracker, double * samples, int startsample, int samplecount, float* maxVolume, double volThreshold) {
double raw_pitch = _dywapitch_computeWaveletPitch(samples, startsample, samplecount, maxVolume, volThreshold);
return _dywapitch_dynamicprocess(pitchtracker, raw_pitch);
}
@@ -0,0 +1,110 @@
/* dywapitchtrack.h
Dynamic Wavelet Algorithm Pitch Tracking library
Released under the MIT open source licence
Copyright (c) 2010 Antoine Schmitt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/* Documentation
The dywapitchtrack library computes the pitch of an audio stream in real time.
The pitch is the main frequency of the waveform (the 'note' being played or sung).
It is expressed as a float in Hz.
Unlike the human ear, pitch detection is difficult to achieve for computers. Many
algorithms have been designed and experimented, but there is no 'best' algorithm.
They all depend on the context and the tradeoffs acceptable in terms of speed and
latency. The context includes the quality and 'cleanness' of the audio : obviously
polyphonic sounds (multiple instruments playing different notes at the same time)
are extremely difficult to track, percussive or noisy audio has no pitch, most
real-life audio have some noisy moments, some instruments have a lot of harmonics,
etc...
The dywapitchtrack is based on a custom-tailored algorithm which is of very high quality:
both very accurate (precision < 0.05 semitones), very low latency (< 23 ms) and
very low error rate. It has been thoroughly tested on human voice.
It can best be described as a dynamic wavelet algorithm (dywa):
The heart of the algorithm is a very powerful wavelet algorithm, described in a paper
by Eric Larson and Ross Maddox : "Real-Time Time-Domain Pitch Tracking Using Wavelets"
http://online.physics.uiuc.edu/courses/phys498pom/NSF_REU_Reports/2005_reu/Real-Time_Time-Domain_Pitch_Tracking_Using_Wavelets.pdf
This central algorithm has been improved by adding dynamic tracking, to reduce the
common problems of frequency-halving and voiced/unvoiced errors. This dynamic tracking
explains the need for a tracking structure (dywapitchtracker). The dynamic tracking assumes
that the main function dywapitch_computepitch is called repeatedly, as it follows the pitch
over time and makes assumptions about human voice capabilities and reallife conditions
(as documented inside the code).
Note : The algorithm currently assumes a 44100Hz audio sampling rate. If you use a different
samplerate, you can just multiply the resulting pitch by the ratio between your samplerate and 44100.
*/
/* Usage
// Allocate your audio buffers and start the audio stream.
// Allocate a 'dywapitchtracker' structure.
// Start the pitch tracking by calling 'dywapitch_inittracking'.
dywapitchtracker pitchtracker;
dywapitch_inittracking(&pitchtracker);
// For each available audio buffer, call 'dywapitch_computepitch'
double thepitch = dywapitch_computepitch(&pitchtracker, samples, start, count);
*/
#ifndef dywapitchtrack__H
#define dywapitchtrack__H
#ifdef __cplusplus
extern "C" {
#endif
// structure to hold tracking data
typedef struct _dywapitchtracker {
double _prevPitch;
int _pitchConfidence;
} dywapitchtracker;
// returns the number of samples needed to compute pitch for fequencies equal and above the given minFreq (in Hz)
// useful to allocate large enough audio buffer
// ex : for frequencies above 130Hz, you need 1024 samples (assuming a 44100 Hz samplerate)
int dywapitch_neededsamplecount(int minFreq);
// call before computing any pitch, passing an allocated dywapitchtracker structure
void dywapitch_inittracking(dywapitchtracker *pitchtracker);
// computes the pitch. Pass the inited dywapitchtracker structure
// samples : a pointer to the sample buffer
// startsample : the index of the first sample to use in the sample buffer
// samplecount : the number of samples to use to compte the pitch
// return 0.0 if no pitch was found (sound too low, noise, etc..)
double dywapitch_computepitch(dywapitchtracker *pitchtracker, double * samples, int startsample, int samplecount, float* maxVolume, double volThreshold);
#ifdef __cplusplus
} // extern "C"
#endif
#endif
+31
View File
@@ -0,0 +1,31 @@
#include "ptDyWa.h"
#include "../Helper.h"
PtDyWa::PtDyWa(unsigned step){
_Step = step;
dywapitch_inittracking(&_State);
_VolTreshold = 0.01f;
_LastMaxVol = 0.f;
}
void PtDyWa::SetVolumeThreshold(float threshold){
_VolTreshold = threshold;
}
double PtDyWa::FindNote(float* maxVolume){
double AnaylsisBuf[_SampleCt];
size_t size = _AnalysisBuf.size();
if(size > _SampleCt)
_AnalysisBuf.pop(size - _SampleCt);
if(!_AnalysisBuf.read(AnaylsisBuf, AnaylsisBuf + _SampleCt)){
*maxVolume = _LastMaxVol * 0.85f;
return -1;
}
_AnalysisBuf.pop(_Step);
double pitch = dywapitch_computepitch(&_State, AnaylsisBuf, 0, _SampleCt, maxVolume, _VolTreshold);
if(pitch == 0.0)
return -1;
else
return FreqToNote(pitch);
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "../performous/pitch.hh"
#include "dywapitchtrack.h"
class PtDyWa{
public:
PtDyWa(unsigned step);
/** Add input data to buffer. This is thread-safe (against other functions). **/
template <typename InIt> void input(InIt begin, InIt end) {
_AnalysisBuf.insert(begin, end);
}
double FindNote(float* maxVolume);
void SetVolumeThreshold(float threshold);
float GetVolumeThreshold(){return _VolTreshold;}
private:
dywapitchtracker _State;
constexpr static size_t _SampleCt = 2048;
RingBuffer<_SampleCt * 2> _AnalysisBuf;
unsigned _Step;
float _VolTreshold;
float _LastMaxVol;
};
+10 -10
View File
@@ -139,7 +139,7 @@ void Analyzer::calcTones() {
double delta = phase - m_fftLastPhase[k];
m_fftLastPhase[k] = static_cast<float>(phase);
delta -= k * phaseStep; // subtract expected phase difference
delta = remainder(delta, 2.0 * M_PI); // map delta phase into +/- M_PI interval
delta = remainder(delta, 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) {
@@ -164,15 +164,16 @@ void Analyzer::calcTones() {
int bestScore = 0;
for (std::size_t div = 2; div <= Tone::MAXHARM && k / div > 1; ++div) {
double freq = peaks[k].freq / div; // Fundamental
double db = peaks[k].db;
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
if (std::abs(p.db/db - 1.0) > 0.4 || std::abs(p.freq / n / freq - 1.0) > .04) continue;
if (n == 1) score += 1; // Extra for fundamental
score += 2;
}
if (score > bestScore) {
if (score >= bestScore) {
bestScore = score;
bestDiv = div;
}
@@ -185,12 +186,11 @@ void Analyzer::calcTones() {
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;
}
if (std::abs(p.freq / n / freq - 1.0) > .04) continue; // Does it match the fundamental freq?
if (std::abs(p.db/t.db - 1.0) > 0.4) continue;
t.db = std::max(t.db, p.db);
++count;
t.freq += p.freq / n;
t.harmonics[n - 1] = p.db;
p.clear();
}
+1
View File
@@ -86,6 +86,7 @@ public:
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); }
double getPeakRaw() const { return 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. **/
+278 -44
View File
@@ -13,25 +13,45 @@
// You should have received a copy of the GNU General Public License
// along with Vocaluxe. If not, see <http://www.gnu.org/licenses/>.
#define _USE_MATH_DEFINES
#include "ptAKF.h"
#include <math.h>
#include <cmath>
static constexpr double BaseToneFrequency = 65.4064; // lowest (half-)tone to analyze (C2 = 65.4064 Hz)
static constexpr double HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave)
int PtAKF::_InitCount = 0;
float* PtAKF::_SamplesPerPeriodPerTone = NULL;
float* restrict PtAKF::_SamplesPerPeriodPerTone = NULL;
float* restrict PtAKF::_SamplesPerPeriodPerToneFine = NULL;
float* restrict PtAKF::_Window = NULL;
PtAKF::PtAKF(){
double GetFrequencyFromTone(double toneIndex){
return BaseToneFrequency * pow(HalftoneBase, toneIndex);
}
PtAKF::PtAKF(unsigned step){
_InitCount++;
if(_InitCount == 1){
_SamplesPerPeriodPerTone = new float[_MaxHalfTone + 1];
_SamplesPerPeriodPerTone = new float[_MaxHalfTone + 1 + _HalfTonesAdd];
_SamplesPerPeriodPerToneFine = new float[(_MaxHalfTone + 1 + _HalfTonesAdd) * 2];
//Init Array to avoid costly calculations
for (int toneIndex = 0; toneIndex <= _MaxHalfTone; toneIndex++)
for (int toneIndex = 0; toneIndex <= _MaxHalfTone + _HalfTonesAdd; toneIndex++)
{
double freq = BaseToneFrequency * pow(HalftoneBase, toneIndex);
_SamplesPerPeriodPerTone[toneIndex] = static_cast<float>(44100.0 / freq); // samples in one period
_SamplesPerPeriodPerTone[toneIndex] = static_cast<float>(44100.0 / GetFrequencyFromTone(toneIndex)); // samples in one period
_SamplesPerPeriodPerToneFine[2 * toneIndex] = static_cast<float>(44100.0 / GetFrequencyFromTone(toneIndex - 1./3.)); // samples for a bit below exact frequency
_SamplesPerPeriodPerToneFine[2 * toneIndex + 1] = static_cast<float>(44100.0 / GetFrequencyFromTone(toneIndex + 1./3.)); // samples for a bit above exact frequency
}
_Window = new float[_SampleCt];
double cosMult = 2. * M_PI / (_SampleCt - 1.); //To simplify and speed up
for (int i = 0; i < _SampleCt; i++) {
_Window[i] = static_cast<float>(0.64 - 0.36 * cos(i * cosMult));
}
}
_Step = step;
_VolTreshold = 0.01f;
_LastMaxVol = 0.f;
for(int i = 0; i < _SmoothCt; i++)
_LastTones[i] = -1;
_LastToneIndex = 0;
}
PtAKF::~PtAKF(){
@@ -39,63 +59,256 @@ PtAKF::~PtAKF(){
if(!_InitCount){
delete[] _SamplesPerPeriodPerTone;
_SamplesPerPeriodPerTone = NULL;
delete[] _SamplesPerPeriodPerToneFine;
_SamplesPerPeriodPerToneFine = NULL;
}
}
int PtAKF::GetNote(double* maxVolume, float* weights){
float AnaylsisBuf[_SampleCt];
size_t size = _AnalysisBuf.size();
if(size > _SampleCt)
_AnalysisBuf.pop(size - _SampleCt);
if(!_AnalysisBuf.read(AnaylsisBuf, AnaylsisBuf + _SampleCt))
return -1;
return _GetNote(AnaylsisBuf, maxVolume, weights);
void PtAKF::SetVolumeThreshold(float threshold){
_VolTreshold = threshold;
}
int PtAKF::_GetNote(float* samples, double* maxVolume, float* weights){
int PtAKF::GetNote(float* restrict maxVolume, float* restrict weights){
float AnaylsisBuf[_SampleCt];
int note = _LastTones[_LastToneIndex];
*maxVolume = _LastMaxVol * 0.85f;
if(_AnalysisBuf.read(AnaylsisBuf, AnaylsisBuf + _SampleCt)){
do{
_AnalysisBuf.pop(_Step);
note = _GetNote(AnaylsisBuf, maxVolume, weights);
if(++_LastToneIndex >= _SmoothCt)
_LastToneIndex = 0;
_LastTones[_LastToneIndex] = note;
}while(_AnalysisBuf.read(AnaylsisBuf, AnaylsisBuf + _SampleCt));
note = _GetSmoothTone();
}
return note;
}
int PtAKF::_GetSmoothTone(){
int tones[_SmoothCt];
int ct = 0;
for(int i=0; i < _SmoothCt; i++){
int j;
int tone = _LastTones[i];
if(tone < 0)
continue;
for(j = ct; j > 0; j--){
if(tones[j-1] > tone)
tones[j] = tones[j-1];
else break;
}
tones[j] = tone;
ct++;
}
if(ct == 0)
return -1; //Nothing detected
if(ct % 2 == 0)
return (tones[ct/2] + tones[ct/2-1]) / 2; //For even cts get the mean of the 2 middle tones
return tones[ct/2]; //For odd cts get the median (middle tone)
}
void PtAKF::InitPeaks(SPeak peaks[_MaxPeaks]){
for(int i = 0; i < _MaxPeaks; i++){
peaks[i].toneIndex = -1;
peaks[i].weight = -9.f;
}
}
void PtAKF::AddPeak(SPeak peaks[], float curWeight, int toneIndex){
if(curWeight > peaks[0].weight){
int i;
for(i = 1; i < _MaxPeaks; i++){
if(curWeight < peaks[i].weight){
peaks[i-1].toneIndex = toneIndex;
peaks[i-1].weight = curWeight;
break;
}else{
peaks[i-1] = peaks[i];
}
}
if(i == _MaxPeaks){
peaks[i-1].toneIndex = toneIndex;
peaks[i-1].weight = curWeight;
}
}
}
int PtAKF::_GetNote(float samples[_SampleCt], float* restrict maxVolume, float weights[_MaxHalfTone+1]){
// Calculate maximum volume
float maxVolumeL = 0;
for(int i = 0; i < _SampleCt; i++){
for(int i = _SampleCt / 2; i < _SampleCt; i++){
float vol = abs(samples[i]);
if(vol > maxVolumeL)
maxVolumeL = vol;
}
maxVolumeL /= 32767.;
//maxVolumeL /= 32767.f;
*maxVolume = maxVolumeL;
float maxWeight = -1;
float minWeight = 1;
int maxTone = -1;
for (int toneIndex = _MinHalfTone; toneIndex <= _MaxHalfTone; toneIndex++)
{
float curWeight = _AnalyzeToneFunc(samples, toneIndex);
if(maxVolumeL < _VolTreshold)
return -1;
if (curWeight > maxWeight)
{
maxWeight = curWeight;
maxTone = toneIndex;
}
float samplesWindowed[_SampleCt];
if (curWeight < minWeight)
minWeight = curWeight;
weights[toneIndex - _MinHalfTone] = curWeight;
for(int i = 0; i < _SampleCt; i++){
samplesWindowed[i] = samples[i] * _Window[i];
}
if(maxWeight - minWeight > 0.01){
// Now analyze the samples and get peaks at the most appropriate tones
//Attention: We have a peak at lag 0 that might stretch that far, that we detect a wrong "peak" at _MaxHalfTone
//Because of that we filter out all tones that are past the last zero crossing from below but keep tones with decreasing weights (going towards zero crossing from above)
int lastValidTone = 0;
float lastWeight = 1.f;
float maxWeight = 0.f;
#pragma omp parallel for schedule(static)
for (int toneIndex = 0; toneIndex <= _MaxHalfTone; toneIndex++){
float curWeight = _AnalyzeByTone(samples, samplesWindowed, toneIndex);
weights[toneIndex] = curWeight;
}
for (int toneIndex = 0; toneIndex <= _MaxHalfTone; toneIndex++){
float curWeight = weights[toneIndex];
if(curWeight > maxWeight){
maxWeight = curWeight;
}
if(lastWeight > curWeight || (lastWeight > 0.f && curWeight <= 0.f))
lastValidTone = toneIndex - 1;
lastWeight = curWeight;
}
// Now clear off the lag 0 peak if required
if(maxWeight >= weights[_MaxHalfTone]){
//We might have caught the lag 0 peak so go a bit further to check for other zero crossings (or we won't be able to detect _MaxHalfTone)
float lastWeight = _AKFByTone(samples, _MaxHalfTone);
for(int toneIndex = _MaxHalfTone+1; toneIndex <= _MaxHalfTone + _HalfTonesAdd; toneIndex++){
float curWeight = _AKFByTone(samples, toneIndex);
if(lastWeight > curWeight || (lastWeight > 0.f && curWeight <= 0.f)){
lastValidTone = toneIndex - 1;
break;
}
lastWeight = curWeight;
}
if(lastValidTone < 0)
return -1;
//Set all invalid weights to 0
for(int toneIndex = lastValidTone + 1;toneIndex <= _MaxHalfTone; toneIndex++){
weights[toneIndex] = 0.f;
}
}
SPeak peaks[_MaxPeaks];
InitPeaks(peaks);
int numPeaks = 0;
bool up = true;
for(int i = 1; i <= _MaxHalfTone; i++){
if(weights[i - 1] > weights[i]){
if(up){
AddPeak(peaks, weights[i - 1], i - 1);
numPeaks++;
up = false;
}
}else if(!up){
up = true;
}
}
if(up && weights[_MaxHalfTone] > 0.001f){
AddPeak(peaks, weights[_MaxHalfTone], _MaxHalfTone);
numPeaks++;
}
if(numPeaks > _MaxPeaks)
numPeaks = _MaxPeaks;
// Further analyze the found peaks and find the real maximum
// This is necessary because we may have our real peak a bit off the exact tone frequency
// and a 'wrong' peak that is exactly at another tone which might become higher than the one at the 'right' tone
maxWeight = peaks[_MaxPeaks-1].weight;
if(maxWeight < 0.00001f) // Just a small number to check for zero values
return -1;
int maxTone = peaks[_MaxPeaks-1].toneIndex;
int maxToneFine = -1;
for(int i = _MaxPeaks - numPeaks; i < _MaxPeaks; i++){
float curWeight = peaks[i].weight;
if(curWeight * 3.f < maxWeight)
continue;
int toneIndex = peaks[i].toneIndex;
float curWeightDown = _AnalyzeBySampleCt(samples, samplesWindowed, _SamplesPerPeriodPerToneFine[toneIndex * 2]);
int otherToneIndex;
int otherToneFineIndex;
if(curWeightDown > curWeight){
otherToneIndex = toneIndex - 1;
otherToneFineIndex = 1;
curWeight = curWeightDown;
}else{
float curWeightUp = _AnalyzeBySampleCt(samples, samplesWindowed, _SamplesPerPeriodPerToneFine[toneIndex * 2 + 1]);
if(curWeightUp > curWeight){
otherToneIndex = (toneIndex < _MaxHalfTone) ? toneIndex + 1: -1; // If the other tone is invalid just set it to -1 which will be skipped below
otherToneFineIndex = 0;
curWeight = curWeightUp;
}else
continue;
}
weights[toneIndex] = curWeight;
if(curWeight > maxWeight){
maxTone = toneIndex;
maxToneFine = 1 - otherToneFineIndex;
maxWeight = curWeight;
}
// Now check also neighbouring tone
if(otherToneIndex >= 0){
float curWeightOther = _AnalyzeBySampleCt(samples, samplesWindowed, _SamplesPerPeriodPerToneFine[otherToneIndex * 2 + otherToneFineIndex]);
if(curWeightOther > weights[otherToneIndex]){
weights[otherToneIndex] = curWeightOther;
if(curWeightOther > maxWeight){
maxTone = otherToneIndex;
maxToneFine = otherToneFineIndex;
maxWeight = curWeightOther;
}
}
}
}
float energy = _AKFBySampleCt(samplesWindowed, 0);
float maxAKF = (maxToneFine < 0) ? _AKFByTone(samplesWindowed, maxTone) : _AKFBySampleCt(samplesWindowed, _SamplesPerPeriodPerToneFine[maxTone * 2 + maxToneFine]);
//if(maxWeight - minWeight > 0.025){
if(maxAKF >= 0.33f * energy){
return maxTone;
}else return -1;
}
float PtAKF::_AnalyzeToneFunc(float* samples, int toneIndex){
// Use method by Kobayashi and Shimamura (1995): Combine AKF and AMDF to a new f(z)=AKF(z)/(AMDF(z)+k) with k=1
float PtAKF::_AnalyzeByTone(float samples[_SampleCt], float samplesWindowed[_SampleCt], int toneIndex){
return _AnalyzeBySampleCt(samples, samplesWindowed, _SamplesPerPeriodPerTone[toneIndex]);
}
float samplesPerPeriodD = _SamplesPerPeriodPerTone[toneIndex]; // samples in one period
float PtAKF::_AnalyzeBySampleCt(float samples[_SampleCt], float samplesWindowed[_SampleCt], float samplesPerPeriodD){
// Use method by Kobayashi and Shimamura (2001): Combine AKF and AMDF to a new f(z)=AKF(z)/(AMDF(z)+k) with k=1
float akf = _AKFBySampleCt(samplesWindowed, samplesPerPeriodD);
float amdf = _AMDFBySampleCt(samples, samplesPerPeriodD);
//return accumDistAKF / (accumDistAMDF * 32767.f + 32767.f * 32767.f); // Need to scale AKF by MAX^2 and AMDF by MAX, so do some maths to divide only once
float result = akf / (amdf + 1.f);
return result; // Need to scale AKF by MAX^2 and AMDF by MAX, so do some maths to divide only once
//{toneIndex}: {accumDistAKF} ; {accumDistAMDF}; {result}
}
float PtAKF::_AKFByTone(float samples[_SampleCt], int toneIndex){
return _AKFBySampleCt(samples, _SamplesPerPeriodPerTone[toneIndex]);
}
float PtAKF::_AKFBySampleCt(float samples[_SampleCt], float samplesPerPeriodD){
int samplesPerPeriod = static_cast<int>(samplesPerPeriodD);
float fHigh = samplesPerPeriodD - samplesPerPeriod;
float fLow = 1.0f - fHigh;
float accumDistAKF = 0; // accumulated distances
float accumDistAMDF = 0; // accumulated distances
float accumDist = 0; // accumulated distances
// compare correlating samples
int sampleIndex = 0; // index of sample to analyze
@@ -105,14 +318,35 @@ float PtAKF::_AnalyzeToneFunc(float* samples, int toneIndex){
// calc distance to corresponding sample in next period
float xn = samples[sampleIndex];
float xnt = samples[correlatingSampleIndex] * fLow + samples[correlatingSampleIndex + 1] * fHigh;
accumDistAKF += xn * xnt;
accumDistAMDF += abs(xn - xnt);
accumDist += xn * xnt;
}
accumDistAKF /= _SampleCt;
accumDistAMDF /= sampleIndex;
return accumDist / _SampleCt;
}
return accumDistAKF / (accumDistAMDF * 32767.f + 32767.f); // Need to scale AKF by MAX^2 and AMDF by MAX, so do some maths to divide only once
float PtAKF::_AMDFByTone(float samples[_SampleCt], int toneIndex){
return _AKFBySampleCt(samples, _SamplesPerPeriodPerTone[toneIndex]);
}
float PtAKF::_AMDFBySampleCt(float samples[_SampleCt], float samplesPerPeriodD){
int samplesPerPeriod = static_cast<int>(samplesPerPeriodD);
float fHigh = samplesPerPeriodD - samplesPerPeriod;
float fLow = 1.0f - fHigh;
float 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
float xn = samples[sampleIndex];
float xnt = samples[correlatingSampleIndex] * fLow + samples[correlatingSampleIndex + 1] * fHigh;
accumDist += abs(xn - xnt);
}
return accumDist / sampleIndex;
}
/*
+53 -10
View File
@@ -1,27 +1,70 @@
#pragma once
#include "performous/pitch.hh"
#if __STDC__ != 1
# define restrict __restrict
#else
# ifndef __STDC_VERSION__
# define restrict __restrict
# else
# if __STDC_VERSION__ < 199901L
# define restrict __restrict
# endif
# endif
#endif
struct SPeak{
int toneIndex;
float weight;
};
// A pitch detection that is based on the AKF and AMDF (initially from original Vocaluxe/USDx)
class PtAKF{
public:
PtAKF();
PtAKF(unsigned step);
~PtAKF();
void input(short* begin, short* end){
/** Add input data to buffer. This is thread-safe (against other functions). **/
template <typename InIt> void input(InIt begin, InIt end) {
_AnalysisBuf.insert(begin, end);
}
int GetNote(double* maxVolume, float* weights);
static int GetNumHalfTones(){ return _MaxHalfTone - _MinHalfTone + 1;}
int GetNote(float* restrict maxVolume, float* restrict weights);
void SetVolumeThreshold(float threshold);
float GetVolumeThreshold(){return _VolTreshold;}
static int GetNumHalfTones(){ return _MaxHalfTone + 1;}
private:
static int _InitCount;
static float* _SamplesPerPeriodPerTone;
static constexpr int _MinHalfTone = 0; //C2
static constexpr int _MaxHalfTone = 47; //B5
static float* restrict _SamplesPerPeriodPerTone;
// Use a 3 times finer resolution for exact peak detection
// So this array will store for each note the number of samples 1/3 below and up
// (cannot use halves as it would be ambiguous)
static float* restrict _SamplesPerPeriodPerToneFine;
static float* restrict _Window;
//static constexpr int _MinHalfTone = 0; //C2
static constexpr int _MaxHalfTone = 56;//47; //B5
static constexpr int _HalfTonesAdd = 4; //Additonal half tones to analyze to remove the peak at lag 0
static constexpr int _MaxPeaks = 10;
static constexpr int _SmoothCt = 3; //Number of samples used for smoothing the result
constexpr static size_t _SampleCt = 4096;
constexpr static size_t _SampleCt = 2048;
RingBuffer<_SampleCt * 2> _AnalysisBuf;
unsigned _Step;
float _VolTreshold;
float _LastMaxVol;
int _LastTones[_SmoothCt];
int _LastToneIndex;
static int _GetNote(float* samples, double* maxVolume, float* weights);
static float _AnalyzeToneFunc(float* samples, int toneIndex);
int _GetNote(float samples[_SampleCt], float* restrict maxVolume, float weights[_MaxHalfTone+1]);
int _GetSmoothTone();
static float _AnalyzeBySampleCt(float samples[_SampleCt], float samplesWindowed[_SampleCt], float samplesPerPeriodD);
static inline float _AnalyzeByTone(float samples[_SampleCt], float samplesWindowed[_SampleCt], int toneIndex);
static float _AKFBySampleCt(float samples[_SampleCt], float samplesPerPeriodD);
static inline float _AKFByTone(float samples[_SampleCt], int toneIndex);
static float _AMDFBySampleCt(float samples[_SampleCt], float samplesPerPeriodD);
static inline float _AMDFByTone(float samples[_SampleCt], int toneIndex);
static inline void InitPeaks(SPeak peaks[_MaxPeaks]);
static inline void AddPeak(SPeak peaks[_MaxPeaks], float curWeight, int toneIndex);
};
+3
View File
@@ -164,6 +164,9 @@ namespace Vocaluxe.Base
public static void LogDebug(string text)
{
_DebugLog.Add(String.Format("{0:HH:mm:ss.ffff}", DateTime.Now) + ":" + text);
#if DEBUG
Console.WriteLine(text);
#endif
}
public static void LogSongInfo(string text)
-162
View File
@@ -1,162 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Vocaluxe.Lib.Sound.Record
{
/// <summary>
/// Analyzer from Performous
/// Should be very reliable but needs some proof (artificial tests show some errors)
/// </summary>
class CAnalyzer : IDisposable
{
#region Imports
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr Analyzer_Create(uint step = 200);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_Free(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_InputFloat(IntPtr analyzer, [In] float[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_InputShort(IntPtr analyzer, [In] short[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_InputByte(IntPtr analyzer, [In] byte[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_Process(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern double Analyzer_GetPeak(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern double Analyzer_FindNote(IntPtr analyzer, double minFreq, double maxFreq);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool Analyzer_OutputFloat(IntPtr analyzer, [Out] float[] data, int sampleCt, float rate);
#endregion
private IntPtr _Instance;
public CAnalyzer(uint step = 200)
{
_Instance = Analyzer_Create(step);
}
~CAnalyzer()
{
_Dispose(false);
}
public void Input(float[] data)
{
Analyzer_InputFloat(_Instance, data, data.Length);
}
public void Input(short[] data)
{
Analyzer_InputShort(_Instance, data, data.Length);
}
public void Input(byte[] data)
{
Analyzer_InputByte(_Instance, data, data.Length / 2);
}
public void Process()
{
Analyzer_Process(_Instance);
}
public double GetPeak()
{
return Analyzer_GetPeak(_Instance);
}
public double FindNote(double minFreq = 35.0, double maxFreq = 1000.0)
{
return Analyzer_FindNote(_Instance, minFreq, maxFreq);
}
public bool Output(float[] data, float rate)
{
return Analyzer_OutputFloat(_Instance, data, data.Length, rate);
}
private void _Dispose(bool disposing)
{
if (_Instance == IntPtr.Zero)
throw new ObjectDisposedException(GetType().Name);
Analyzer_Free(_Instance);
_Instance = IntPtr.Zero;
}
public void Dispose()
{
_Dispose(true);
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Pitchtracker (Pt) that uses autocorrelation (AKF) and AMDF
/// Quite fast and perfect in artificial tests, but may fail in real world scenarios (e.g. missing fundamental)
/// </summary>
class CPtAKF : IDisposable
{
#region Imports
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr PtAKF_Create();
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtAKF_Free(IntPtr analyzer);
[DllImport("PitchTracker.dll", EntryPoint = "PtAKF_GetNumHalfTones", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetNumHalfTones();
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtAKF_InputByte(IntPtr analyzer, [In] byte[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int PtAKF_GetNote(IntPtr analyzer, [Out] out double maxVolume, [Out] float[] weights);
#endregion
private IntPtr _Instance;
public CPtAKF()
{
_Instance = PtAKF_Create();
}
~CPtAKF()
{
_Dispose(false);
}
public void Input(byte[] data)
{
PtAKF_InputByte(_Instance, data, data.Length / 2);
}
public int GetNote(out double maxVolume, float[] weights)
{
return PtAKF_GetNote(_Instance, out maxVolume, weights);
}
private void _Dispose(bool disposing)
{
if (_Instance == IntPtr.Zero)
throw new ObjectDisposedException(GetType().Name);
PtAKF_Free(_Instance);
_Instance = IntPtr.Zero;
}
public void Dispose()
{
_Dispose(true);
GC.SuppressFinalize(this);
}
}
}
+25 -177
View File
@@ -15,36 +15,31 @@
// along with Vocaluxe. If not, see <http://www.gnu.org/licenses/>.
#endregion
#define TEST_PITCH
//#define TEST_PITCH
using System;
#if TEST_PITCH
using System.Windows.Forms;
using System.Diagnostics;
using VocaluxeLib;
#endif
using Vocaluxe.Lib.Sound.Record.PitchTracker;
namespace Vocaluxe.Lib.Sound.Record
{
class CBuffer : IDisposable
{
#if TEST_PITCH
//Half tones: C C♯ D D# E F F♯ G G♯ A A# B
private const double _BaseToneFreq = 65.4064; // lowest (half-)tone to analyze (C2 = 65.4064 Hz)
private const double _HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave)
#endif
private CPtAKF _Analyzer = new CPtAKF();
private CPitchTracker _PitchTracker = new CPtAKF();
private double _MaxVolume;
private float _MaxVolume;
public CBuffer()
{
MinVolume = 0.02f;
VolTreshold = 0.02f;
ToneWeigths = new float[GetNumHalfTones()];
Reset();
#if TEST_PITCH
_TestPitchDetection();
CPitchTrackerTest tester = new CPitchTrackerTest();
tester.AddAnalyzer(new CAnalyzer());
tester.AddAnalyzer(new CPtAKF());
tester.AddAnalyzer(new CPtDyWa());
tester.AddAnalyzer(new CPtSharp());
tester.RunTest();
#endif
}
@@ -53,9 +48,9 @@ namespace Vocaluxe.Lib.Sound.Record
_Dispose();
}
public static int GetNumHalfTones()
public int GetNumHalfTones()
{
return CPtAKF.GetNumHalfTones();
return _PitchTracker.GetNumHalfTones();
}
public int ToneAbs { get; private set; }
@@ -64,7 +59,7 @@ namespace Vocaluxe.Lib.Sound.Record
public float MaxVolume
{
get { return (float)_MaxVolume; }
get { return _MaxVolume; }
}
public bool ToneValid { get; private set; }
@@ -74,7 +69,11 @@ namespace Vocaluxe.Lib.Sound.Record
/// <summary>
/// Minimum volume for a tone to be valid
/// </summary>
public float MinVolume { get; set; }
public float VolTreshold
{
get { return _PitchTracker.VolumeTreshold; }
set { _PitchTracker.VolumeTreshold = value; }
}
public void Reset()
{
@@ -93,15 +92,13 @@ namespace Vocaluxe.Lib.Sound.Record
// voice passthrough (send data to playback-device)
//if (assigned(fVoiceStream)) then
//fVoiceStream.WriteData(Buffer, BufferSize);
_Analyzer.Input(buffer);
_PitchTracker.Input(buffer);
}
public void AnalyzeBuffer()
{
//_Analyzer.Process();
//_MaxVolume = _Analyzer.GetPeak() / 43 + 1;
int tone = _Analyzer.GetNote(out _MaxVolume, ToneWeigths); //(int)Math.Round(_Analyzer.FindNote()); //
if (tone >= 0 && _MaxVolume >= MinVolume)
int tone = _PitchTracker.GetNote(out _MaxVolume, ToneWeigths);
if (tone >= 0)
{
ToneAbs = tone;
Tone = ToneAbs % 12;
@@ -119,160 +116,11 @@ namespace Vocaluxe.Lib.Sound.Record
private void _Dispose()
{
if (_Analyzer != null)
if (_PitchTracker != null)
{
_Analyzer.Dispose();
_Analyzer = null;
_PitchTracker.Dispose();
_PitchTracker = null;
}
}
#if TEST_PITCH
private static bool _PitchTestRun;
private static string _ToneToNote(int tone, bool withOctave = true)
{
tone += 24;
string[] notes = {"C", "C♯", "D", "D#", "E", "F", "F♯", "G", "G♯", "A", "A#", "B"};
string result = notes[tone % 12];
if (withOctave)
result += (tone / 12);
return result;
}
private void _TestPitchDetection()
{
if (_PitchTestRun)
return;
_PitchTestRun = true;
int toneFrom = 0;
const int toneTo = 47; //B5
Console.WriteLine("Testing notes " + _ToneToNote(toneFrom) + " - " + _ToneToNote(toneTo));
byte[] data;
byte[] data2 = new byte[2048];
int ok = 0;
double angle = 0;
for (int distort = 0; distort < 10; distort++)
{
for (int tone = toneFrom; tone <= toneTo; tone++)
{
_GetSineWave(_BaseToneFreq * Math.Pow(_HalftoneBase, tone), 44100, ref angle, out data);
_Distort(data, tone, distort);
for (int i = 0; i < 4; i++)
{
Buffer.BlockCopy(data, i * 2048, data2, 0, 2048);
ProcessNewBuffer(data2);
AnalyzeBuffer();
}
if (!ToneValid)
CBase.Log.LogDebug("Note " + _ToneToNote(tone) + " not detected (Distortion: " + distort + ")");
else if (Tone != tone % 12)
CBase.Log.LogDebug("Note " + _ToneToNote(tone) + " wrongly detected as " + _ToneToNote(Tone, false) + " (Distortion: " + distort + ")");
else
ok++;
}
}
_GetSineWave(_BaseToneFreq * Math.Pow(_HalftoneBase, 5), 44100, ref angle, out data);
Stopwatch sw = new Stopwatch();
sw.Start();
const int repeats = 100;
for (int i = 0; i < repeats; i++)
{
ProcessNewBuffer(data);
AnalyzeBuffer();
}
sw.Stop();
string msg = "Analyser: Errors: " + ((toneTo - toneFrom + 1) * 10 - ok) + "; Speed: " +
(int)(repeats / (sw.ElapsedMilliseconds / 1000.0)) + " buffers/s";
Console.WriteLine(msg);
MessageBox.Show(msg);
Reset();
}
private static short[] _GetDistort(int len, int tone, int type)
{
short[] sdata = new short[len];
if (type < 9)
{
int newTone = 1;
switch (type)
{
case 0:
newTone = 2;
break;
case 1:
newTone = 3;
break;
case 2:
newTone = 6;
break;
case 3:
newTone = 8;
break;
case 4:
newTone = 10;
break;
case 5:
newTone = 15;
break;
case 6:
newTone = 25;
break;
case 7:
newTone = 26;
break;
case 8:
newTone = 29;
break;
}
byte[] data2;
double angle = 0;
_GetSineWave(_BaseToneFreq * Math.Pow(_HalftoneBase, tone + newTone), 44100, ref angle, out data2);
Buffer.BlockCopy(data2, 0, sdata, 0, len * 2);
}
else
{
Random r = new Random(0xBEEF);
for (int i = 0; i < sdata.Length; i++)
sdata[i] = (short)(r.Next() - Int16.MinValue);
}
return sdata;
}
private static void _Distort(byte[] data, int tone, int distortCt)
{
if (distortCt < 1)
return;
short[] sdata = new short[data.Length / 2];
Buffer.BlockCopy(data, 0, sdata, 0, data.Length);
short[][] distortions = new short[distortCt][];
for (int i = 0; i < distortCt; i++)
distortions[i] = _GetDistort(data.Length / 2, tone, i);
for (int i = 0; i < sdata.Length; i++)
{
double distortion = 0;
for (int j = 0; j < distortCt; j++)
distortion += distortions[j][i];
distortion /= distortCt;
sdata[i] = (short)(sdata[i] * 4.0 / 5.0 + distortion / 5.0);
}
Buffer.BlockCopy(sdata, 0, data, 0, data.Length);
}
private static void _GetSineWave(double freq, int sampleRate, ref double angle, out byte[] data)
{
const short max = short.MaxValue;
const int len = 4096; //sampleRate * durationMs / 1000;
short[] data16Bit = new short[len];
for (int i = 0; i < len; i++)
data16Bit[i] = (short)(Math.Sin(2 * Math.PI * i / sampleRate * freq + angle) * max);
angle = 2 * Math.PI * len / sampleRate * freq + angle;
angle = angle % (2 * Math.PI);
data = new byte[data16Bit.Length * 2];
Buffer.BlockCopy(data16Bit, 0, data, 0, data.Length);
}
#endif
}
}
+1 -1
View File
@@ -132,7 +132,7 @@ namespace Vocaluxe.Lib.Sound.Record
public int NumHalfTones()
{
return CBuffer.GetNumHalfTones();
return _Buffer[0].GetNumHalfTones();
}
public float[] ToneWeigth(int player)
@@ -0,0 +1,90 @@
using System;
using System.Runtime.InteropServices;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker
{
/// <summary>
/// Analyzer from Performous
/// Should be very reliable but needs some proof (artificial tests show some errors)
/// </summary>
class CAnalyzer : CPitchTracker
{
#region Imports
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr Analyzer_Create(uint step = 200);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_Free(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_InputFloat(IntPtr analyzer, [In] float[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_InputShort(IntPtr analyzer, [In] short[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_InputByte(IntPtr analyzer, [In] byte[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Analyzer_Process(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern float Analyzer_GetPeak(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern double Analyzer_FindNote(IntPtr analyzer, double minFreq, double maxFreq);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool Analyzer_OutputFloat(IntPtr analyzer, [Out] float[] data, int sampleCt, float rate);
#endregion
private IntPtr _Instance;
private float _VolumeTreshold;
public CAnalyzer(uint step = 200)
{
_Instance = Analyzer_Create(step);
_VolumeTreshold = 0.01f;
}
public override void Input(byte[] data)
{
Analyzer_InputByte(_Instance, data, data.Length / 2);
}
public override int GetNote(out float maxVolume, float[] weights)
{
Analyzer_Process(_Instance);
maxVolume = Analyzer_GetPeak(_Instance);
if (maxVolume < _VolumeTreshold)
return -1;
int note = (int)Math.Round(Analyzer_FindNote(_Instance, 60, 1800));
_SetWeights(note, weights);
return note;
}
public bool Output(float[] data, float rate)
{
return Analyzer_OutputFloat(_Instance, data, data.Length, rate);
}
protected override void _Dispose(bool disposing)
{
if (_Instance == IntPtr.Zero)
throw new ObjectDisposedException(GetType().Name);
Analyzer_Free(_Instance);
_Instance = IntPtr.Zero;
}
public override int GetNumHalfTones()
{
return NumHalfTonesDef;
}
public override float VolumeTreshold
{
get { return _VolumeTreshold; }
set { _VolumeTreshold = value; }
}
}
}
@@ -0,0 +1,399 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using VocaluxeLib;
using VocaluxeLib.Utils;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker
{
class CPitchTrackerTest
{
//Half tones: C C# D D# E F F# G G# A A# B
private const double _BaseToneFreq = 65.4064; // lowest (half-)tone to analyze (C2 = 65.4064 Hz)
private const double _HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave)
private readonly List<CPitchTracker> _Analyzers = new List<CPitchTracker>();
private int[] _Tones;
private float[][] _Weights;
private float _MaxVolume;
private int _CurTestCount;
private int[] _CurPassedCount;
private int _TestCount;
private int[] _PassedCount;
private int[] _SamplesPerSec;
private static bool _IsRun;
public void AddAnalyzer(CPitchTracker analyzer)
{
_Analyzers.Add(analyzer);
}
#region Init
private void _InitTests()
{
_Tones = new int[_Analyzers.Count];
_TestCount = 0;
_PassedCount = new int[_Analyzers.Count];
_CurTestCount = 0;
_CurPassedCount = new int[_Analyzers.Count];
_Weights = new float[_Analyzers.Count][];
for (int i = 0; i < _Analyzers.Count; i++)
_Weights[i] = new float[_Analyzers[i].GetNumHalfTones()];
_SamplesPerSec = new int[_Analyzers.Count];
}
private void _InitNext(string lastTest)
{
string msg = "Errors " + lastTest + ": ";
for (int i = 0; i < _CurPassedCount.Length; i++)
{
msg += _CurTestCount - _CurPassedCount[i] + " ";
_PassedCount[i] += _CurPassedCount[i];
_CurPassedCount[i] = 0;
}
msg += "of " + _CurTestCount;
CBase.Log.LogDebug(msg);
_TestCount += _CurTestCount;
_CurTestCount = 0;
//Do a reset first as we actually have an impossible situation (drop by multiple octaves)
byte[] data = new byte[4096 * 2];
_Process(data);
}
#endregion
public void RunTest(bool reRun = false)
{
if (_IsRun && !reRun)
return;
_IsRun = true;
_InitTests();
_TestSines();
_InitNext("Sines");
_TestFile("toneG3.wav", 19);
_InitNext("Real G3");
_TestFile("toneG3Miss.wav", 19);
_InitNext("Real G3 with miss. fundamental");
_TestFile("toneG4.wav", 31);
_InitNext("Real G4");
_TestFile("whistling3.wav", "whistling3.txt");
_InitNext("High whistling");
_TestFile("sClausVoc.wav", "sClausVoc.txt");
_InitNext("Real song");
_TestSpeed();
CBase.Log.LogDebug("Finished: ");
for (int i = 0; i < _Analyzers.Count; i++)
{
string msg = _Analyzers[i].GetType().Name + ":";
msg += " Errors=" + (_TestCount - _PassedCount[i]);
msg += " Passed=" + _PassedCount[i];
msg += " Total=" + _TestCount;
msg += " Speed=" + _SamplesPerSec[i] / 1000 + "kSamples/s (=" + (_SamplesPerSec[i] / 44100) + "rec.s/s)";
CBase.Log.LogDebug(msg);
}
}
private void _TestSpeed()
{
const int samplesPerBuffer = 512;
byte[] data;
byte[] data2 = new byte[samplesPerBuffer * 2];
double angle = 0;
const int repeats = 100;
_GetSineWave(_BaseToneFreq * Math.Pow(_HalftoneBase, 5), 44100, samplesPerBuffer * repeats, ref angle, out data);
for (int i = 0; i < _Analyzers.Count; i++)
{
CPitchTracker analyzer = _Analyzers[i];
Stopwatch sw = new Stopwatch();
sw.Start();
for (int j = 0; j < repeats; j++)
{
Buffer.BlockCopy(data, 0, data2, 0, samplesPerBuffer * 2);
analyzer.Input(data2);
analyzer.GetNote(out _MaxVolume, _Weights[i]);
}
sw.Stop();
_SamplesPerSec[i] = (int)Math.Round(samplesPerBuffer * repeats / (sw.ElapsedMilliseconds / 1000.0));
}
}
private void _Process(byte[] data)
{
for (int j = 0; j < _Analyzers.Count; j++)
{
_Analyzers[j].Input(data);
_Tones[j] = _Analyzers[j].GetNote(out _MaxVolume, _Weights[j]);
}
}
private static string _ToneToNote(int tone, bool withOctave = true)
{
if (tone < 0)
return "inv.";
tone += 24;
string[] notes = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
string result = notes[tone % 12];
if (withOctave)
result += (tone / 12);
return result;
}
#region Sines
private void _TestSines()
{
const int toneFrom = 0;
const int toneTo = 47; //B5
const int sampleCt = 4096;
const int batchCt = 512;
byte[] data2 = new byte[batchCt * 2];
Console.WriteLine("Testing notes " + _ToneToNote(toneFrom) + " - " + _ToneToNote(toneTo));
double angle = 0;
bool[] valids = new bool[_Analyzers.Count];
for (int distort = 0; distort < 10; distort++)
{
//Do a reset first as we actually have an impossible situation (drop by multiple octaves)
byte[] data = new byte[sampleCt * 2];
_Process(data);
for (int tone = toneFrom; tone <= toneTo; tone++)
{
_GetSineWave(_BaseToneFreq * Math.Pow(_HalftoneBase, tone), 44100, sampleCt, ref angle, out data);
if (tone == 46 && distort == 4)
data = new byte[data.Length];
_Distort(data, tone, distort);
for (int i = 0; i < sampleCt / batchCt; i++)
{
Buffer.BlockCopy(data, i * batchCt * 2, data2, 0, batchCt * 2);
_Process(data2);
if (i * batchCt < 2048)
continue;
_CurTestCount++;
bool ok = true;
for (int j = 0; j < valids.Length; j++)
{
valids[j] = _Tones[j] == tone;
if (!valids[j])
ok = false;
else
_CurPassedCount[j]++;
}
if (ok)
continue;
string msg = "Note " + _ToneToNote(tone) + "(" + distort + ") at buffer " + (i + 1) + "/" + (sampleCt / batchCt) + " detected as ";
for (int j = 0; j < valids.Length; j++)
msg += _ToneToNote(_Tones[j]) + (valids[j] ? "" : "(!)") + "; ";
CBase.Log.LogDebug(msg);
/*CWavFile w = new CWavFile();
w.Create(tone + "-" + distort + ".wav", 1, 44100, 16);
w.Write16BitSamples(data);
w.Close();*/
}
}
}
}
private static short[] _GetDistort(int sampleCt, int tone, int type)
{
short[] sdata = new short[sampleCt];
if (type < 9)
{
int newTone = 1;
switch (type)
{
case 0:
newTone = 2;
break;
case 1:
newTone = 3;
break;
case 2:
newTone = 6;
break;
case 3:
newTone = 8;
break;
case 4:
newTone = 10;
break;
case 5:
newTone = 15;
break;
case 6:
newTone = 25;
break;
case 7:
newTone = 26;
break;
case 8:
newTone = 29;
break;
}
byte[] data2;
double angle = 0;
_GetSineWave(_BaseToneFreq * Math.Pow(_HalftoneBase, tone + newTone), 44100, sampleCt, ref angle, out data2);
Buffer.BlockCopy(data2, 0, sdata, 0, sampleCt * 2);
}
else
{
Random r = new Random(0xBEEF);
for (int i = 0; i < sdata.Length; i++)
sdata[i] = (short)(r.Next() - Int16.MinValue);
}
return sdata;
}
private static void _Distort(byte[] data, int tone, int distortCt)
{
if (distortCt < 1)
return;
short[] sdata = new short[data.Length / 2];
Buffer.BlockCopy(data, 0, sdata, 0, data.Length);
short[][] distortions = new short[distortCt][];
for (int i = 0; i < distortCt; i++)
distortions[i] = _GetDistort(data.Length / 2, tone, i);
for (int i = 0; i < sdata.Length; i++)
{
double distortion = 0;
for (int j = 0; j < distortCt; j++)
distortion += distortions[j][i];
distortion /= distortCt;
sdata[i] = (short)(sdata[i] * 4.0 / 5.0 + distortion / 5.0);
}
Buffer.BlockCopy(sdata, 0, data, 0, data.Length);
}
private static void _GetSineWave(double freq, int sampleRate, int sampleCt, ref double angle, out byte[] data)
{
const short max = short.MaxValue;
short[] data16Bit = new short[sampleCt];
for (int i = 0; i < sampleCt; i++)
data16Bit[i] = (short)(Math.Sin(2 * Math.PI * i / sampleRate * freq + angle) * max);
angle = 2 * Math.PI * sampleCt / sampleRate * freq + angle;
angle = angle % (2 * Math.PI);
data = new byte[data16Bit.Length * 2];
Buffer.BlockCopy(data16Bit, 0, data, 0, data.Length);
}
#endregion
#region File
private struct STimedNote
{
public int Time, Note;
}
private void _TestFile(string fileName, string testFileName)
{
if (!File.Exists(testFileName))
return;
List<STimedNote> tones = new List<STimedNote>();
using (StreamReader reader = new StreamReader(testFileName))
{
String line;
while ((line = reader.ReadLine()) != null)
{
int p = line.IndexOf(' ');
if (p < 0)
continue;
STimedNote note;
note.Time = int.Parse(line.Substring(0, p));
note.Note = int.Parse(line.Substring(p + 1));
tones.Add(note);
}
}
_TestFile(fileName, tones);
}
private void _TestFile(string fileName, int tone)
{
STimedNote note;
note.Note = tone;
note.Time = 46;
List<STimedNote> tones = new List<STimedNote> {note};
_TestFile(fileName, tones);
}
private static bool _IsNoteValid(int note, int time, IList<STimedNote> tones)
{
const int lastNoteMaxTimeDiff = 1536 * 1000 / 44100; // old note is valid for 1536 more samples
for (int i = 0; i < tones.Count; i++)
{
if (tones[i].Time > time)
break;
if (i + 1 == tones.Count || time <= tones[i + 1].Time + lastNoteMaxTimeDiff)
{
if (tones[i].Note == note || tones[i].Note < 0)
return true;
}
}
return false;
}
private void _TestFile(string fileName, IList<STimedNote> tones)
{
CWavFile wavFile = new CWavFile();
try
{
if (!wavFile.Open(fileName))
return;
if (wavFile.BitsPerSample != 16)
{
wavFile.Close();
return;
}
int samplesRead = 0;
int curTimeIndex = -1;
int curNote = -1;
const int maxSamplesPerBatch = 512;
bool[] valids = new bool[_Tones.Length];
while (wavFile.NumSamplesLeft > maxSamplesPerBatch)
{
byte[] samples = wavFile.GetNextSamples16BitAsBytes(maxSamplesPerBatch, 1);
samplesRead += samples.Length / 2;
int time = samplesRead * 1000 / wavFile.SampleRate;
_Process(samples);
while (curTimeIndex + 1 < tones.Count && time >= tones[curTimeIndex + 1].Time)
{
curTimeIndex++;
curNote = tones[curTimeIndex].Note;
}
if (curNote < 0)
continue;
_CurTestCount++;
bool error = false;
for (int i = 0; i < _Tones.Length; i++)
{
valids[i] = _IsNoteValid(_Tones[i], time, tones);
if (valids[i])
_CurPassedCount[i]++;
else
error = true;
}
if (error)
{
string msg = "Note " + _ToneToNote(curNote) + " at " + time + "ms detected as ";
for (int i = 0; i < _Tones.Length; i++)
msg += _ToneToNote(_Tones[i]) + (valids[i] ? "" : "(!)") + "; ";
CBase.Log.LogDebug(msg);
}
}
}
catch (Exception e)
{
Console.WriteLine("Error on file " + fileName + ": " + e);
}
wavFile.Close();
}
#endregion
}
}
@@ -0,0 +1,71 @@
using System;
using System.Runtime.InteropServices;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker
{
/// <summary>
/// Pitchtracker (Pt) that uses autocorrelation (AKF) and AMDF
/// Quite fast and perfect in artificial tests, but may fail in real world scenarios (e.g. missing fundamental)
/// </summary>
class CPtAKF : CPitchTracker
{
#region Imports
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr PtAKF_Create(uint step);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtAKF_Free(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int PtAKF_GetNumHalfTones();
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtAKF_InputByte(IntPtr analyzer, [In] byte[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtAKF_SetVolumeThreshold(IntPtr analyzer, float threshold);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern float PtAKF_GetVolumeThreshold(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int PtAKF_GetNote(IntPtr analyzer, [Out] out float maxVolume, [Out] float[] weights);
#endregion
private IntPtr _Instance;
public CPtAKF(uint step = 400)
{
_Instance = PtAKF_Create(step);
}
public override int GetNumHalfTones()
{
return PtAKF_GetNumHalfTones();
}
public override float VolumeTreshold
{
get { return PtAKF_GetVolumeThreshold(_Instance); }
set { PtAKF_SetVolumeThreshold(_Instance, value); }
}
public override void Input(byte[] data)
{
PtAKF_InputByte(_Instance, data, data.Length / 2);
}
public override int GetNote(out float maxVolume, float[] weights)
{
return PtAKF_GetNote(_Instance, out maxVolume, weights);
}
protected override void _Dispose(bool disposing)
{
if (_Instance == IntPtr.Zero)
throw new ObjectDisposedException(GetType().Name);
PtAKF_Free(_Instance);
_Instance = IntPtr.Zero;
}
}
}
@@ -0,0 +1,69 @@
using System;
using System.Runtime.InteropServices;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker
{
/// <summary>
/// Pitchtracker (Pt) that uses a dynamic wavelet (DyWa) algorithm
/// </summary>
class CPtDyWa : CPitchTracker
{
#region Imports
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr PtDyWa_Create(uint step);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtDyWa_Free(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtDyWa_SetVolumeThreshold(IntPtr analyzer, float threshold);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern float PtDyWa_GetVolumeThreshold(IntPtr analyzer);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void PtDyWa_InputByte(IntPtr analyzer, [In] byte[] data, int sampleCt);
[DllImport("PitchTracker.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern double PtDyWa_FindNote(IntPtr analyzer, [Out] out float maxVolume);
#endregion
private IntPtr _Instance;
public CPtDyWa(uint step = 200)
{
_Instance = PtDyWa_Create(step);
}
public override void Input(byte[] data)
{
PtDyWa_InputByte(_Instance, data, data.Length / 2);
}
public override int GetNote(out float maxVolume, float[] weights)
{
int note = (int)Math.Round(PtDyWa_FindNote(_Instance, out maxVolume));
_SetWeights(note, weights);
return note;
}
public override int GetNumHalfTones()
{
return NumHalfTonesDef;
}
public override float VolumeTreshold
{
get { return PtDyWa_GetVolumeThreshold(_Instance); }
set { PtDyWa_SetVolumeThreshold(_Instance, value); }
}
protected override void _Dispose(bool disposing)
{
if (_Instance == IntPtr.Zero)
throw new ObjectDisposedException(GetType().Name);
PtDyWa_Free(_Instance);
_Instance = IntPtr.Zero;
}
}
}
@@ -0,0 +1,54 @@
using System;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker
{
/// <summary>
/// Pitchtracker (Pt) in C# that uses IIR filters for smoothing and an intelligent detection algorithm to reduce calculations
/// DO NOT USE in 32 Bit! Due to floating point errors the filter will raise all values to NAN (TODO: FIX it!)
/// </summary>
class CPtSharp : CPitchTracker
{
private Pitch.PitchTracker _Instance;
public CPtSharp()
{
_Instance = new Pitch.PitchTracker {SampleRate = 44100, DetectLevelThreshold = 0.01f, PitchRecordHistorySize = 1};
}
public override void Input(byte[] data)
{
short[] samplesShort = new short[data.Length / 2];
Buffer.BlockCopy(data, 0, samplesShort, 0, data.Length);
float[] samplesFloat = new float[samplesShort.Length];
for (int i = 0; i < samplesShort.Length; i++)
samplesFloat[i] = (float)samplesShort[i] / short.MaxValue;
_Instance.ProcessBuffer(samplesFloat);
}
public override int GetNote(out float maxVolume, float[] weights)
{
int note = _Instance.CurrentPitchRecord.MidiNote - 15 - 21;
maxVolume = _Instance.LastMaxVol;
_SetWeights(note, weights);
return note;
}
public override int GetNumHalfTones()
{
return NumHalfTonesDef;
}
public override float VolumeTreshold
{
get { return _Instance.DetectLevelThreshold; }
set { _Instance.DetectLevelThreshold = value; }
}
protected override void _Dispose(bool disposing)
{
if (_Instance == null)
throw new ObjectDisposedException(GetType().Name);
_Instance = null;
}
}
}
@@ -0,0 +1,73 @@
using System;
using System.Diagnostics;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker
{
/// <summary>
/// Base class/Interface for pitch detectors
/// Most likely some structures or system resources are used, hence it should be disposed if no longer needed
/// </summary>
abstract class CPitchTracker : IDisposable
{
//Default number of half tones (1 octave)
protected const int NumHalfTonesDef = 12;
~CPitchTracker()
{
_Dispose(false);
}
protected abstract void _Dispose(bool disposing);
public void Dispose()
{
_Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Method to set weights if the algorithm does not provide anything that could simulate a visualizer.
/// Should be called with each valid detection
/// </summary>
/// <param name="note">Current note</param>
/// <param name="weights">Array of weights (Length = NumHalfTonesDef)</param>
protected static void _SetWeights(int note, float[] weights)
{
Debug.Assert(weights.Length == NumHalfTonesDef);
for (int i = 0; i < NumHalfTonesDef; i++)
weights[i] *= 0.9f;
if (note < 0)
return;
int w = note % NumHalfTonesDef;
weights[w] = Math.Min(1f, weights[w] + 0.15f);
}
/// <summary>
/// Gets the number of half tones the algorithm provides weights for.
/// This may not be the number of half tones the algorithm detects!
/// </summary>
/// <returns></returns>
public abstract int GetNumHalfTones();
/// <summary>
/// The treshold for detecting silence
/// Value (0-1) for which everything below is considered silence
/// </summary>
public abstract float VolumeTreshold { get; set; }
/// <summary>
/// Recorded data. Has to be 16 bit short values.
/// </summary>
/// <param name="data">16bit short values of data</param>
public abstract void Input(byte[] data);
/// <summary>
/// Returns the current note detected
/// </summary>
/// <param name="maxVolume">Current maximum volume (0-1)</param>
/// <param name="weights">Array of NumHalfTones floats that gets filled with the current weights (0-1) where each value indicates how strong that tone is.
/// Only for basic visualization as it may not be accurate.</param>
/// <returns>Current note index (0 = C2, 12 = C3, ...)</returns>
public abstract int GetNote(out float maxVolume, float[] weights);
}
}
@@ -0,0 +1,173 @@
using System;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker.Pitch
{
public class CircularBuffer<T> : IDisposable
{
int m_bufSize;
int m_begBufOffset;
int m_availBuf;
long m_startPos; // total circular buffer position
T[] m_buffer;
/// <summary>
/// Constructor
/// </summary>
public CircularBuffer()
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="bufCount"></param>
public CircularBuffer(int bufCount)
{
SetSize(bufCount);
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
SetSize(0);
}
/// <summary>
/// Reset to the beginning of the buffer
/// </summary>
public void Reset()
{
m_begBufOffset = 0;
m_availBuf = 0;
m_startPos = 0;
}
/// <summary>
/// Set the buffer to the specified size
/// </summary>
/// <param name="newSize"></param>
public void SetSize(int newSize)
{
Reset();
if (m_bufSize == newSize)
return;
if (m_buffer != null)
m_buffer = null;
m_bufSize = newSize;
if (m_bufSize > 0)
m_buffer = new T[m_bufSize];
}
/// <summary>
/// Clear the buffer
/// </summary>
public void Clear()
{
Array.Clear(m_buffer, 0, m_buffer.Length);
}
/// <summary>
/// Get or set the start position
/// </summary>
public long StartPosition
{
get { return m_startPos; }
set { m_startPos = value; }
}
/// <summary>
/// Get the end position
/// </summary>
public long EndPosition
{
get { return m_startPos + m_availBuf; }
}
/// <summary>
/// Get or set the amount of avaliable space
/// </summary>
public int Available
{
get { return m_availBuf; }
set { m_availBuf = Math.Min(value, m_bufSize); }
}
/// <summary>
/// Write data into the buffer
/// </summary>
/// <param name="m_pInBuffer"></param>
/// <param name="count"></param>
/// <returns></returns>
public int WriteBuffer(T[] m_pInBuffer, int count)
{
count = Math.Min(count, m_bufSize);
var startPos = m_availBuf != m_bufSize ? m_availBuf : m_begBufOffset;
var pass1Count = Math.Min(count, m_bufSize - startPos);
var pass2Count = count - pass1Count;
PitchDsp.CopyBuffer(m_pInBuffer, 0, m_buffer, startPos, pass1Count);
if (pass2Count > 0)
PitchDsp.CopyBuffer(m_pInBuffer, pass1Count, m_buffer, 0, pass2Count);
if (pass2Count == 0)
{
// did not wrap around
if (m_availBuf != m_bufSize)
m_availBuf += count; // have never wrapped around
else
{
m_begBufOffset += count;
m_startPos += count;
}
}
else
{
// wrapped around
if (m_availBuf != m_bufSize)
m_startPos += pass2Count; // first time wrap-around
else
m_startPos += count;
m_begBufOffset = pass2Count;
m_availBuf = m_bufSize;
}
return count;
}
/// <summary>
/// Read from the buffer
/// </summary>
/// <param name="outBuffer"></param>
/// <param name="startRead"></param>
/// <param name="readCount"></param>
/// <returns></returns>
public bool ReadBuffer(T[] outBuffer, long startRead, int readCount)
{
var endRead = (int)(startRead + readCount);
var endAvail = (int)(m_startPos + m_availBuf);
if (startRead < m_startPos || endRead > endAvail)
return false;
var startReadPos = (int)(((startRead - m_startPos) + m_begBufOffset) % m_bufSize);
var block1Samples = Math.Min(readCount, m_bufSize - startReadPos);
var block2Samples = readCount - block1Samples;
PitchDsp.CopyBuffer(m_buffer, startReadPos, outBuffer, 0, block1Samples);
if (block2Samples > 0)
PitchDsp.CopyBuffer(m_buffer, 0, outBuffer, block1Samples, block2Samples);
return true;
}
}
}
@@ -0,0 +1,105 @@
using System;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker.Pitch
{
public static class Extensions
{
/// <summary>
/// Clear the buffer
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="buffer"></param>
public static void Clear(this float[] buffer)
{
Array.Clear(buffer, 0, buffer.Length);
}
/// <summary>
/// Clear the buffer
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="buffer"></param>
public static void Clear(this double[] buffer)
{
Array.Clear(buffer, 0, buffer.Length);
}
/// <summary>
/// Copy the values from one buffer to a different or the same buffer.
/// It is safe to copy to the same buffer, even if the areas overlap
/// </summary>
/// <param name="buffer"></param>
/// <param name="shiftBy"></param>
/// <param name="startIdx"></param>
/// <param name="length"></param>
public static void Copy(this float[] fromBuffer, float[] toBuffer, int fromStart, int toStart, int length)
{
if (toBuffer == null || fromBuffer.Length == 0 || toBuffer.Length == 0)
return;
int fromBegIdx = fromStart;
int fromEndIdx = fromStart + length;
int toBegIdx = toStart;
int toEndIdx = toStart + length;
if (fromBegIdx < 0)
{
toBegIdx -= fromBegIdx;
fromBegIdx = 0;
}
if (toBegIdx < 0)
{
fromBegIdx -= toBegIdx;
toBegIdx = 0;
}
if (fromEndIdx >= fromBuffer.Length)
{
toEndIdx -= fromEndIdx - fromBuffer.Length + 1;
fromEndIdx = fromBuffer.Length - 1;
}
if (toEndIdx >= toBuffer.Length)
{
fromEndIdx -= toEndIdx - toBuffer.Length + 1;
toEndIdx = fromBuffer.Length - 1;
}
if (fromBegIdx < toBegIdx)
{
// Shift right, so start at the right
for (int fromIdx = fromEndIdx, toIdx = toEndIdx; fromIdx >= fromBegIdx; fromIdx--, toIdx--)
toBuffer[toIdx] = fromBuffer[fromIdx];
}
else
{
// Shift left, so start at the left
for (int fromIdx = fromBegIdx, toIdx = toBegIdx; fromIdx <= fromEndIdx; fromIdx++, toIdx++)
toBuffer[toIdx] = fromBuffer[fromIdx];
}
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="buffer"></param>
public static void Clear(this float[] buffer, int startIdx, int endIdx)
{
Array.Clear(buffer, startIdx, endIdx - startIdx + 1);
}
/// <summary>
/// Fill the buffer with the specified value
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="buffer"></param>
/// <param name="?"></param>
public static void Fill(this double[] buffer, double value)
{
for (int idx = 0; idx < buffer.Length; idx++)
buffer[idx] = value;
}
}
}
@@ -0,0 +1,613 @@
using System;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker.Pitch
{
/// <summary>
/// Infinite impulse response filter (old style analog filters)
/// </summary>
class IIRFilter
{
/// <summary>
/// The type of filter
/// </summary>
public enum FilterType
{
None = 0,
LP,
HP,
BP
}
/// <summary>
/// The filter prototype
/// </summary>
public enum ProtoType
{
None = 0,
Butterworth,
Chebyshev,
}
const int kHistMask = 31;
const int kHistSize = 32;
private int m_order;
private ProtoType m_protoType;
private FilterType m_filterType;
private float m_fp1;
private float m_fp2;
private float m_fN;
private float m_ripple;
private float m_sampleRate;
private double[] m_real;
private double[] m_imag;
private double[] m_z;
private double[] m_aCoeff;
private double[] m_bCoeff;
private double[] m_inHistory;
private double[] m_outHistory;
private int m_histIdx;
private bool m_invertDenormal;
public IIRFilter()
{
}
/// <summary>
/// Returns true if all the filter parameters are valid
/// </summary>
public bool FilterValid
{
get
{
if (m_order < 1 || m_order > 16 ||
m_protoType == ProtoType.None ||
m_filterType == FilterType.None ||
m_sampleRate <= 0.0f ||
m_fN <= 0.0f)
return false;
switch (m_filterType)
{
case FilterType.LP:
if (m_fp2 <= 0.0f)
return false;
break;
case FilterType.BP:
if (m_fp1 <= 0.0f || m_fp2 <= 0.0f || m_fp1 >= m_fp2)
return false;
break;
case FilterType.HP:
if (m_fp1 <= 0.0f)
return false;
break;
}
// For bandpass, the order must be even
if (m_filterType == FilterType.BP && (m_order & 1) != 0)
return false;
return true;
}
}
/// <summary>
/// Set the filter prototype
/// </summary>
public ProtoType Proto
{
get { return m_protoType; }
set
{
m_protoType = value;
Design();
}
}
/// <summary>
/// Set the filter type
/// </summary>
public FilterType Type
{
get { return m_filterType; }
set
{
m_filterType = value;
Design();
}
}
public int Order
{
get { return m_order; }
set
{
m_order = Math.Min(16, Math.Max(1, Math.Abs(value)));
if (m_filterType == FilterType.BP && Odd(m_order))
m_order++;
Design();
}
}
public float SampleRate
{
get { return m_sampleRate; }
set
{
m_sampleRate = value;
m_fN = 0.5f * m_sampleRate;
Design();
}
}
public float FreqLow
{
get { return m_fp1; }
set
{
m_fp1 = value;
Design();
}
}
public float FreqHigh
{
get { return m_fp2; }
set
{
m_fp2 = value;
Design();
}
}
public float Ripple
{
get { return m_ripple; }
set
{
m_ripple = value;
Design();
}
}
/// <summary>
/// Returns true if n is odd
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
private bool Odd(int n)
{
return (n & 1) == 1;
}
/// <summary>
/// Square
/// </summary>
/// <param name="f"></param>
/// <returns></returns>
private float Sqr(float value)
{
return value * value;
}
/// <summary>
/// Square
/// </summary>
/// <param name="f"></param>
/// <returns></returns>
private double Sqr(double value)
{
return value * value;
}
/// <summary>
/// Determines poles and zeros of IIR filter
/// based on bilinear transform method
/// </summary>
private void LocatePolesAndZeros()
{
m_real = new double[m_order + 1];
m_imag = new double[m_order + 1];
m_z = new double[m_order + 1];
double ln10 = Math.Log(10.0);
// Butterworth, Chebyshev parameters
int n = m_order;
if (m_filterType == FilterType.BP)
n = n / 2;
int ir = n % 2;
int n1 = n + ir;
int n2 = (3 * n + ir) / 2 - 1;
double f1;
switch (m_filterType)
{
case FilterType.LP:
f1 = m_fp2;
break;
case FilterType.HP:
f1 = m_fN - m_fp1;
break;
case FilterType.BP:
f1 = m_fp2 - m_fp1;
break;
default:
f1 = 0.0;
break;
}
double tanw1 = Math.Tan(0.5 * Math.PI * f1 / m_fN);
double tansqw1 = Sqr(tanw1);
// Real and Imaginary parts of low-pass poles
double t, a = 1.0, r = 1.0, i = 1.0;
for (int k = n1; k <= n2; k++)
{
t = 0.5 * (2 * k + 1 - ir) * Math.PI / (double)n;
switch (m_protoType)
{
case ProtoType.Butterworth:
double b3 = 1.0 - 2.0 * tanw1 * Math.Cos(t) + tansqw1;
r = (1.0 - tansqw1) / b3;
i = 2.0 * tanw1 * Math.Sin(t) / b3;
break;
case ProtoType.Chebyshev:
double d = 1.0 - Math.Exp(-0.05 * m_ripple * ln10);
double e = 1.0 / Math.Sqrt(1.0 / Sqr(1.0 - d) - 1.0);
double x = Math.Pow(Math.Sqrt(e * e + 1.0) + e, 1.0 / (double)n);
a = 0.5 * (x - 1.0 / x);
double b = 0.5 * (x + 1.0 / x);
double c3 = a * tanw1 * Math.Cos(t);
double c4 = b * tanw1 * Math.Sin(t);
double c5 = Sqr(1.0 - c3) + Sqr(c4);
r = 2.0 * (1.0 - c3) / c5 - 1.0;
i = 2.0 * c4 / c5;
break;
}
int m = 2 * (n2 - k) + 1;
m_real[m + ir] = r;
m_imag[m + ir] = Math.Abs(i);
m_real[m + ir + 1] = r;
m_imag[m + ir + 1] = -Math.Abs(i);
}
if (Odd(n))
{
if (m_protoType == ProtoType.Butterworth)
r = (1.0 - tansqw1) / (1.0 + 2.0 * tanw1 + tansqw1);
if (m_protoType == ProtoType.Chebyshev)
r = 2.0 / (1.0 + a * tanw1) - 1.0;
m_real[1] = r;
m_imag[1] = 0.0;
}
switch (m_filterType)
{
case FilterType.LP:
for (int m = 1; m <= n; m++)
m_z[m] = -1.0;
break;
case FilterType.HP:
// Low-pass to high-pass transformation
for (int m = 1; m <= n; m++)
{
m_real[m] = -m_real[m];
m_z[m] = 1.0;
}
break;
case FilterType.BP:
// Low-pass to bandpass transformation
for (int m = 1; m <= n; m++)
{
m_z[m] = 1.0;
m_z[m + n] = -1.0;
}
double f4 = 0.5 * Math.PI * m_fp1 / m_fN;
double f5 = 0.5 * Math.PI * m_fp2 / m_fN;
double aa = Math.Cos(f4 + f5) / Math.Cos(f5 - f4);
double aR, aI, h1, h2, p1R, p2R, p1I, p2I;
for (int m1 = 0; m1 <= (m_order - 1) / 2; m1++)
{
int m = 1 + 2 * m1;
aR = m_real[m];
aI = m_imag[m];
if (Math.Abs(aI) < 0.0001)
{
h1 = 0.5 * aa * (1.0 + aR);
h2 = Sqr(h1) - aR;
if (h2 > 0.0)
{
p1R = h1 + Math.Sqrt(h2);
p2R = h1 - Math.Sqrt(h2);
p1I = 0.0;
p2I = 0.0;
}
else
{
p1R = h1;
p2R = h1;
p1I = Math.Sqrt(Math.Abs(h2));
p2I = -p1I;
}
}
else
{
double fR = aa * 0.5 * (1.0 + aR);
double fI = aa * 0.5 * aI;
double gR = Sqr(fR) - Sqr(fI) - aR;
double gI = 2 * fR * fI - aI;
double sR = Math.Sqrt(0.5 * Math.Abs(gR + Math.Sqrt(Sqr(gR) + Sqr(gI))));
double sI = gI / (2.0 * sR);
p1R = fR + sR;
p1I = fI + sI;
p2R = fR - sR;
p2I = fI - sI;
}
m_real[m] = p1R;
m_real[m + 1] = p2R;
m_imag[m] = p1I;
m_imag[m + 1] = p2I;
}
if (Odd(n))
{
m_real[2] = m_real[n + 1];
m_imag[2] = m_imag[n + 1];
}
for (int k = n; k >= 1; k--)
{
int m = 2 * k - 1;
m_real[m] = m_real[k];
m_real[m + 1] = m_real[k];
m_imag[m] = Math.Abs(m_imag[k]);
m_imag[m + 1] = -Math.Abs(m_imag[k]);
}
break;
}
}
/// <summary>
/// Calculate all the values
/// </summary>
public void Design()
{
if (!this.FilterValid)
return;
m_aCoeff = new double[m_order + 1];
m_bCoeff = new double[m_order + 1];
m_inHistory = new double[kHistSize];
m_outHistory = new double[kHistSize];
double[] newA = new double[m_order + 1];
double[] newB = new double[m_order + 1];
// Find filter poles and zeros
LocatePolesAndZeros();
// Compute filter coefficients from pole/zero values
m_aCoeff[0] = 1.0;
m_bCoeff[0] = 1.0;
for (int i = 1; i <= m_order; i++)
{
m_aCoeff[i] = 0.0;
m_bCoeff[i] = 0.0;
}
int k = 0;
int n = m_order;
int pairs = n / 2;
if (Odd(m_order))
{
// First subfilter is first order
m_aCoeff[1] = -m_z[1];
m_bCoeff[1] = -m_real[1];
k = 1;
}
for (int p = 1; p <= pairs; p++)
{
int m = 2 * p - 1 + k;
double alpha1 = -(m_z[m] + m_z[m + 1]);
double alpha2 = m_z[m] * m_z[m + 1];
double beta1 = -2.0 * m_real[m];
double beta2 = Sqr(m_real[m]) + Sqr(m_imag[m]);
newA[1] = m_aCoeff[1] + alpha1 * m_aCoeff[0];
newB[1] = m_bCoeff[1] + beta1 * m_bCoeff[0];
for (int i = 2; i <= n; i++)
{
newA[i] = m_aCoeff[i] + alpha1 * m_aCoeff[i - 1] + alpha2 * m_aCoeff[i - 2];
newB[i] = m_bCoeff[i] + beta1 * m_bCoeff[i - 1] + beta2 * m_bCoeff[i - 2];
}
for (int i = 1; i <= n; i++)
{
m_aCoeff[i] = newA[i];
m_bCoeff[i] = newB[i];
}
}
// Ensure the filter is normalized
FilterGain(1000);
}
/// <summary>
/// Reset the history buffers
/// </summary>
public void Reset()
{
if (m_inHistory != null)
m_inHistory.Clear();
if (m_outHistory != null)
m_outHistory.Clear();
m_histIdx = 0;
}
/// <summary>
/// Reset the filter, and fill the appropriate history buffers with the specified value
/// </summary>
/// <param name="historyValue"></param>
public void Reset(double startValue)
{
m_histIdx = 0;
if (m_inHistory == null || m_outHistory == null)
return;
m_inHistory.Fill(startValue);
if (m_inHistory != null)
{
switch (m_filterType)
{
case FilterType.LP:
m_outHistory.Fill(startValue);
break;
default:
m_outHistory.Clear();
break;
}
}
}
/// <summary>
/// Apply the filter to the buffer
/// </summary>
/// <param name="bufIn"></param>
public void FilterBuffer(float[] srcBuf, long srcPos, float[] dstBuf, long dstPos, long nLen)
{
const double kDenormal = 0.000000000000001;
double denormal = m_invertDenormal ? -kDenormal : kDenormal;
m_invertDenormal = !m_invertDenormal;
for (int sampleIdx = 0; sampleIdx < nLen; sampleIdx++)
{
double sum = 0.0f;
m_inHistory[m_histIdx] = srcBuf[srcPos + sampleIdx] + denormal;
for (int idx = 0; idx < m_aCoeff.Length; idx++)
sum += m_aCoeff[idx] * m_inHistory[(m_histIdx - idx) & kHistMask];
for (int idx = 1; idx < m_bCoeff.Length; idx++)
sum -= m_bCoeff[idx] * m_outHistory[(m_histIdx - idx) & kHistMask];
m_outHistory[m_histIdx] = sum;
m_histIdx = (m_histIdx + 1) & kHistMask;
dstBuf[dstPos + sampleIdx] = (float)sum;
}
}
public float FilterSample(float inVal)
{
double sum = 0.0f;
m_inHistory[m_histIdx] = inVal;
for (int idx = 0; idx < m_aCoeff.Length; idx++)
sum += m_aCoeff[idx] * m_inHistory[(m_histIdx - idx) & kHistMask];
for (int idx = 1; idx < m_bCoeff.Length; idx++)
sum -= m_bCoeff[idx] * m_outHistory[(m_histIdx - idx) & kHistMask];
m_outHistory[m_histIdx] = sum;
m_histIdx = (m_histIdx + 1) & kHistMask;
return (float)sum;
}
/// <summary>
/// Get the gain at the specified number of frequency points
/// </summary>
/// <param name="freqPoints"></param>
/// <returns></returns>
public float[] FilterGain(int freqPoints)
{
// Filter gain at uniform frequency intervals
float[] g = new float[freqPoints];
double theta, s, c, sac, sas, sbc, sbs;
float gMax = -100.0f;
float sc = 10.0f / (float)Math.Log(10.0f);
double t = Math.PI / (freqPoints - 1);
for (int i = 0; i < freqPoints; i++)
{
theta = i * t;
if (i == 0)
theta = Math.PI * 0.0001;
if (i == freqPoints - 1)
theta = Math.PI * 0.9999;
sac = 0.0f;
sas = 0.0f;
sbc = 0.0f;
sbs = 0.0f;
for (int k = 0; k <= m_order; k++)
{
c = Math.Cos(k * theta);
s = Math.Sin(k * theta);
sac += c * m_aCoeff[k];
sas += s * m_aCoeff[k];
sbc += c * m_bCoeff[k];
sbs += s * m_bCoeff[k];
}
g[i] = sc * (float)Math.Log((Sqr(sac) + Sqr(sas)) / (Sqr(sbc) + Sqr(sbs)));
gMax = Math.Max(gMax, g[i]);
}
// Normalize to 0 dB maximum gain
for (int i = 0; i < freqPoints; i++)
g[i] -= gMax;
// Normalize numerator (a) coefficients
float normFactor = (float)Math.Pow(10.0, -0.05 * gMax);
for (int i = 0; i <= m_order; i++)
m_aCoeff[i] *= normFactor;
return g;
}
}
}
@@ -0,0 +1,561 @@
using System;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker.Pitch
{
/// <summary>
/// Pitch related DSP
/// </summary>
public class PitchDsp
{
public static readonly double InverseLog2 = 1.0 / Math.Log10(2.0);
private const int kCourseOctaveSteps = 96;
private const int kScanHiSize = 31;
private const float kScanHiFreqStep = 1.005f;
private const int kMinMidiNote = 21; // A0
private const int kMaxMidiNote = 108; // C8
private float m_minPitch;
private float m_maxPitch;
private int m_minNote;
private int m_maxNote;
private int m_blockLen14; // 1/4 block len
private int m_blockLen24; // 2/4 block len
private int m_blockLen34; // 3/4 block len
private int m_blockLen44; // 4/4 block len
private double m_sampleRate;
private int m_numCourseSteps;
private float[] m_pCourseFreqOffset;
private float[] m_pCourseFreq;
private float[] m_scanHiOffset = new float[kScanHiSize];
private float[] m_peakBuf = new float[kScanHiSize];
private int m_prevPitchIdx;
private float[] m_detectCurve;
/// <summary>
/// Constructor
/// </summary>
/// <param name="fSampleRate"></param>
/// <param name="minFreq"></param>
/// <param name="maxFreq"></param>
/// <param name="fFreqStep"></param>
public PitchDsp(double sampleRate, float minPitch, float maxPitch)
{
m_sampleRate = sampleRate;
m_minPitch = minPitch;
m_maxPitch = maxPitch;
m_minNote = (int)(PitchToMidiNote(m_minPitch) + 0.5f) + 2;
m_maxNote = (int)(PitchToMidiNote(m_maxPitch) + 0.5f) - 2;
m_blockLen44 = (int)(m_sampleRate / m_minPitch + 0.5f);
m_blockLen34 = (m_blockLen44 * 3) / 4;
m_blockLen24 = m_blockLen44 / 2;
m_blockLen14 = m_blockLen44 / 4;
m_numCourseSteps = (int)((Math.Log((double)m_maxPitch / (double)m_minPitch) / Math.Log(2.0)) * kCourseOctaveSteps + 0.5) + 3;
m_pCourseFreqOffset = new float[m_numCourseSteps + 10000];
m_pCourseFreq = new float[m_numCourseSteps + 10000];
m_detectCurve = new float[m_numCourseSteps];
double freqStep = 1.0 / Math.Pow(2.0, 1.0 / kCourseOctaveSteps);
double curFreq = m_maxPitch / freqStep;
// frequency is stored from high to low
for (int idx = 0; idx < m_numCourseSteps; idx++)
{
m_pCourseFreq[idx] = (float)curFreq;
m_pCourseFreqOffset[idx] = (float)(m_sampleRate / curFreq);
curFreq *= freqStep;
}
for (int idx = 0; idx < kScanHiSize; idx++)
m_scanHiOffset[idx] = (float)Math.Pow(kScanHiFreqStep, (kScanHiSize / 2) - idx);
}
/// <summary>
/// Get the max detected pitch
/// </summary>
public float MaxPitch
{
get { return m_maxPitch; }
}
/// <summary>
/// Get the min detected pitch
/// </summary>
public float MinPitch
{
get { return m_minPitch; }
}
/// <summary>
/// Get the max note
/// </summary>
public int MaxNote
{
get { return m_maxNote; }
}
/// <summary>
/// Get the min note
/// </summary>
public int MinNote
{
get { return m_minNote; }
}
/// <summary>
/// Detect the pitch
/// </summary>
public float DetectPitch(float[] samplesLo, float[] samplesHi)
{
return DetectPitchLo(samplesLo, samplesHi);
}
/// <summary>
/// Low resolution pitch detection
/// </summary>
/// <param name="dataIdx"></param>
/// <param name="begFreqIdx"></param>
/// <param name="endFreqIdx"></param>
/// <param name="blockLen"></param>
/// <param name="stepSize"></param>
/// <returns></returns>
private float DetectPitchLo(float[] samplesLo, float[] samplesHi)
{
m_detectCurve.Clear();
const int skipSize = 8;
const int peakScanSize = 23;
const int peakScanSizeHalf = peakScanSize / 2;
float peakThresh1 = 200.0f;
float peakThresh2 = 600.0f;
bool bufferSwitched = false;
for (int idx = 0; idx < m_numCourseSteps; idx += skipSize)
{
int blockLen = Math.Min(m_blockLen44, (int)m_pCourseFreqOffset[idx] * 2);
float[] curSamples = null;
// 258 is at 250 Hz, which is the switchover frequency for the two filters
bool loBuffer = idx >= 258;
if (loBuffer)
{
if (!bufferSwitched)
{
m_detectCurve.Clear(258 - peakScanSizeHalf, 258 + peakScanSizeHalf);
bufferSwitched = true;
}
curSamples = samplesLo;
}
else
curSamples = samplesHi;
int stepSizeLoRes = blockLen / 10;
int stepSizeHiRes = Math.Max(1, Math.Min(5, idx * 5 / m_numCourseSteps));
float fValue = RatioAbsDiffLinear(curSamples, idx, blockLen, stepSizeLoRes, false);
if (fValue > peakThresh1)
{
// Do a closer search for the peak
int peakIdx = -1;
float peakVal = 0.0f;
float prevVal = 0.0f;
int dir = 4; // start going forward
int curPos = idx; // start at center of the scan range
int begSearch = Math.Max(idx - peakScanSizeHalf, 0);
int endSearch = Math.Min(idx + peakScanSizeHalf, m_numCourseSteps - 1);
while (curPos >= begSearch && curPos < endSearch)
{
float curVal = RatioAbsDiffLinear(curSamples, curPos, blockLen, stepSizeHiRes, true);
if (peakVal < curVal)
{
peakVal = curVal;
peakIdx = curPos;
}
if (prevVal > curVal)
{
dir = -dir >> 1;
if (dir == 0)
{
if (peakVal > peakThresh2 && peakIdx >= 6 && peakIdx <= m_numCourseSteps - 7)
{
float fValL = RatioAbsDiffLinear(curSamples, peakIdx - 5, blockLen, stepSizeHiRes, true);
float fValR = RatioAbsDiffLinear(curSamples, peakIdx + 5, blockLen, stepSizeHiRes, true);
float fPointy = peakVal / (fValL + fValR) * 2.0f;
float minPointy = (m_prevPitchIdx > 0 && Math.Abs(m_prevPitchIdx - peakIdx) < 10) ? 1.2f : 1.5f;
if (fPointy > minPointy)
{
float pitchHi = DetectPitchHi(curSamples, peakIdx);
if (pitchHi > 1.0f)
{
m_prevPitchIdx = peakIdx;
return pitchHi;
}
}
}
break;
}
}
prevVal = curVal;
curPos += dir;
}
}
}
m_prevPitchIdx = 0;
return 0.0f;
}
/// <summary>
/// High resolution pitch detection
/// </summary>
/// <param name="dataIdx"></param>
/// <param name="lowFreqIdx"></param>
/// <returns></returns>
private float DetectPitchHi(float[] samples, int lowFreqIdx)
{
int peakIdx = -1;
float prevVal = 0.0f;
int dir = 4; // start going forward
int curPos = kScanHiSize >> 1; // start at center of the scan range
m_peakBuf.Clear();
float offset = m_pCourseFreqOffset[lowFreqIdx];
while (curPos >= 0 && curPos < kScanHiSize)
{
if (m_peakBuf[curPos] == 0.0f)
m_peakBuf[curPos] = SumAbsDiffHermite(samples, offset * m_scanHiOffset[curPos], m_blockLen44, 1);
if (peakIdx < 0 || m_peakBuf[peakIdx] < m_peakBuf[curPos])
peakIdx = curPos;
if (prevVal > m_peakBuf[curPos])
{
dir = -dir >> 1;
if (dir == 0)
{
// found the peak
float minVal = Math.Min(m_peakBuf[peakIdx - 1], m_peakBuf[peakIdx + 1]);
minVal -= minVal * (1.0f / 32.0f);
var y1 = (float)Math.Log10(m_peakBuf[peakIdx - 1] - minVal);
var y2 = (float)Math.Log10(m_peakBuf[peakIdx] - minVal);
var y3 = (float)Math.Log10(m_peakBuf[peakIdx + 1] - minVal);
float fIdx = (float)peakIdx + (y3 - y1) / (2.0f * (2.0f * y2 - y1 - y3));
return (float)Math.Pow(kScanHiFreqStep, fIdx - (kScanHiSize / 2)) * m_pCourseFreq[lowFreqIdx];
}
}
prevVal = m_peakBuf[curPos];
curPos += dir;
}
return 0.0f;
}
/// <summary>
/// Create a sine wave with the specified frequency, amplitude and starting angle.
/// Returns the updated angle.
/// </summary>
/// <param name="buffer"></param>
/// <param name="numSamples"></param>
/// <param name="freq"></param>
/// <param name="amplitude"></param>
/// <param name="startAngle"></param>
/// <returns></returns>
public static double CreateSineWave(float[] buffer, int numSamples, float sampleRate,
float freq, float amplitude, double startAngle)
{
double angleStep = freq / sampleRate * Math.PI * 2.0;
double curAngle = startAngle;
for (int idx = 0; idx < numSamples; idx++)
{
buffer[idx] = (float)Math.Sin(curAngle) * amplitude;
curAngle += angleStep;
while (curAngle > Math.PI)
curAngle -= Math.PI * 2.0;
}
return curAngle;
}
/// <summary>
/// Returns true if the level is above the specified value
/// </summary>
/// <param name="buffer"></param>
/// <param name="startIdx"></param>
/// <param name="len"></param>
/// <param name="level"></param>
/// <returns></returns>
public bool LevelIsAbove(float[] buffer, int len, float level)
{
if (buffer == null || buffer.Length == 0)
return false;
int endIdx = Math.Min(buffer.Length, len);
for (int idx = 0; idx < endIdx; idx++)
{
if (Math.Abs(buffer[idx]) >= level)
return true;
}
return false;
}
/// <summary>
/// Copy the data from the source to the destination buffer
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <param name="startPos"></param>
/// <param name="length"></param>
public static void CopyBuffer<T>(T[] source, int srcStart, T[] destination, int dstStart, int length)
{
if (length < 0)
throw new ArgumentOutOfRangeException("length");
if (source == null || source.Length < srcStart + length)
throw new Exception("Source buffer is null or not large enough");
if (destination == null || destination.Length < dstStart + length)
throw new Exception("Destination buffer is null or not large enough");
int srcIdx = srcStart;
int dstIdx = dstStart;
for (int idx = 0; idx < length; idx++)
destination[dstIdx++] = source[srcIdx++];
}
/// <summary>
/// // 4-point, 3rd-order Hermite (x-form)
/// </summary>
private float InterpolateHermite(float fY0, float fY1, float fY2, float fY3, float frac)
{
float c1 = 0.5f * (fY2 - fY0);
float c3 = 1.5f * (fY1 - fY2) + 0.5f * (fY3 - fY0);
float c2 = fY0 - fY1 + c1 - c3;
return ((c3 * frac + c2) * frac + c1) * frac + fY1;
}
/// <summary>
/// Linear interpolation
/// nFrac is based on 1.0 = 256
/// </summary>
private float InterpolateLinear(float y0, float y1, float frac)
{
return y0 * (1.0f - frac) + y1 * frac;
}
/// <summary>
/// Medium Low res SumAbsDiff
/// </summary>
private float RatioAbsDiffLinear(float[] samples, int freqIdx, int blockLen, int stepSize, bool hiRes)
{
if (hiRes && m_detectCurve[freqIdx] > 0.0f)
return m_detectCurve[freqIdx];
var offsetInt = (int)m_pCourseFreqOffset[freqIdx];
float offsetFrac = m_pCourseFreqOffset[freqIdx] - offsetInt;
float rect = 0.0f;
float absDiff = 0.01f; // prevent divide by zero
int count = 0;
float interp;
float sample;
// Do a scan using linear interpolation and the specified step size
for (int idx = 0; idx < blockLen; idx += stepSize, count++)
{
sample = samples[idx];
interp = InterpolateLinear(samples[offsetInt + idx], samples[offsetInt + idx + 1], offsetFrac);
absDiff += Math.Abs(sample - interp);
rect += Math.Abs(sample) + Math.Abs(interp);
}
float finalVal = rect / absDiff * 100.0f;
if (hiRes)
m_detectCurve[freqIdx] = finalVal;
return finalVal;
}
/// <summary>
/// Medium High res SumAbsDiff
/// </summary>
private float SumAbsDiffHermite(float[] samples, float fOffset, int blockLen, int stepSize)
{
var offsetInt = (int)fOffset;
float offsetFrac = fOffset - offsetInt;
float value = 0.001f; // prevent divide by zero
int count = 0;
// do a scan using linear interpolation and the specified step size
for (int idx = 0; idx < blockLen; idx += stepSize, count++)
{
int offsetIdx = offsetInt + idx;
value += Math.Abs(samples[idx] - InterpolateHermite(samples[offsetIdx - 1],
samples[offsetIdx],
samples[offsetIdx + 1],
samples[offsetIdx + 2],
offsetFrac));
}
return count / value;
}
/// <summary>
/// Get the MIDI note and cents of the pitch
/// </summary>
/// <param name="pitch"></param>
/// <param name="note"></param>
/// <param name="cents"></param>
/// <returns></returns>
public static bool PitchToMidiNote(float pitch, out int note, out int cents)
{
if (pitch < 20.0f)
{
note = 0;
cents = 0;
return false;
}
float fNote = (float)((12.0 * Math.Log10(pitch / 55.0) * InverseLog2)) + 33.0f;
note = (int)(fNote + 0.5f);
cents = (int)((note - fNote) * 100);
return true;
}
/// <summary>
/// Get the pitch from the MIDI note
/// </summary>
/// <param name="pitch"></param>
/// <returns></returns>
public static float PitchToMidiNote(float pitch)
{
if (pitch < 20.0f)
return 0.0f;
return (float)(12.0 * Math.Log10(pitch / 55.0) * InverseLog2) + 33.0f;
}
/// <summary>
/// Get the pitch from the MIDI note
/// </summary>
/// <param name="note"></param>
/// <returns></returns>
public float MidiNoteToPitch(float note)
{
if (note < 33.0f)
return 0.0f;
float pitch = (float)Math.Pow(10.0, (note - 33.0f) / InverseLog2 / 12.0f) * 55.0f;
return pitch <= m_maxPitch ? pitch : 0.0f;
}
/// <summary>
/// Format a midi note to text
/// </summary>
/// <param name="note"></param>
/// <param name="sharps"></param>
/// <param name="showOctave"></param>
/// <returns></returns>
public static string GetNoteName(int note, bool sharps, bool showOctave)
{
if (note < kMinMidiNote || note > kMaxMidiNote)
return null;
note -= kMinMidiNote;
int octave = (note + 9) / 12;
note = note % 12;
string noteText = null;
switch (note)
{
case 0:
noteText = "A";
break;
case 1:
noteText = sharps ? "A#" : "Bb";
break;
case 2:
noteText = "B";
break;
case 3:
noteText = "C";
break;
case 4:
noteText = sharps ? "C#" : "Db";
break;
case 5:
noteText = "D";
break;
case 6:
noteText = sharps ? "D#" : "Eb";
break;
case 7:
noteText = "E";
break;
case 8:
noteText = "F";
break;
case 9:
noteText = sharps ? "F#" : "Gb";
break;
case 10:
noteText = "G";
break;
case 11:
noteText = sharps ? "G#" : "Ab";
break;
}
if (showOctave)
noteText += " " + octave.ToString();
return noteText;
}
}
}
@@ -0,0 +1,431 @@
using System;
using System.Collections.Generic;
using System.Collections;
namespace Vocaluxe.Lib.Sound.Record.PitchTracker.Pitch
{
/// <summary>
/// Tracks pitch
/// </summary>
// ReSharper disable InconsistentNaming
public class PitchTracker
{
private const int kOctaveSteps = 96;
private const int kStepOverlap = 4;
private const float kMinFreq = 50.0f; // A1, Midi note 33, 55.0Hz
private const float kMaxFreq = 1800.0f; // A#6. Midi note 92
private const int kStartCircular = 40; // how far into the sample buffer do we start checking (allow for filter settling)
private const float kDetectOverlapSec = 0.005f;
private const float kMaxOctaveSecRate = 10.0f;
private const float kAvgOffset = 0.005f; // time offset between pitch averaging values
private const int kAvgCount = 1; // number of average pitch samples to take
private const float kCircularBufSaveTime = 1.0f; // Amount of samples to store in the history buffer
private PitchDsp m_dsp;
private CircularBuffer<float> m_circularBufferLo;
private CircularBuffer<float> m_circularBufferHi;
private double m_sampleRate;
private float m_detectLevelThreshold = 0.01f; // -40dB
private int m_pitchRecordsPerSecond = 50; // default is 50, or one record every 20ms
private float[] m_pitchBufLo;
private float[] m_pitchBufHi;
private int m_pitchBufSize;
private int m_samplesPerPitchBlock;
private int m_curPitchIndex;
private long m_curPitchSamplePos;
private int m_detectOverlapSamples;
private float m_maxOverlapDiff;
private bool m_recordPitchRecords;
private int m_pitchRecordHistorySize;
private List<PitchRecord> m_pitchRecords = new List<PitchRecord>();
private PitchRecord m_curPitchRecord = new PitchRecord();
private IIRFilter m_iirFilterLoLo;
private IIRFilter m_iirFilterLoHi;
private IIRFilter m_iirFilterHiLo;
private IIRFilter m_iirFilterHiHi;
public float LastMaxVol = 0;
public delegate void PitchDetectedHandler(PitchTracker sender, PitchRecord pitchRecord);
public event PitchDetectedHandler PitchDetected;
/// <summary>
/// Constructor
/// </summary>
public PitchTracker() {}
/// <summary>
/// Set the sample rate
/// </summary>
public double SampleRate
{
set
{
if (m_sampleRate == value)
return;
m_sampleRate = value;
Setup();
}
}
/// <summary>
/// Set the detect level threshold, The value must be between 0.0001f and 1.0f (-80 dB to 0 dB)
/// </summary>
public float DetectLevelThreshold
{
set
{
float newValue = Math.Max(0.0001f, Math.Min(1.0f, value));
if (m_detectLevelThreshold == newValue)
return;
m_detectLevelThreshold = newValue;
//Setup();
}
get { return m_detectLevelThreshold; }
}
/// <summary>
/// Return the samples per pitch block
/// </summary>
public int SamplesPerPitchBlock
{
get { return m_samplesPerPitchBlock; }
}
/// <summary>
/// Get or set the number of pitch records per second (default is 50, or one record every 20ms)
/// </summary>
public int PitchRecordsPerSecond
{
get { return m_pitchRecordsPerSecond; }
set
{
m_pitchRecordsPerSecond = Math.Max(1, Math.Min(100, value));
Setup();
}
}
/// <summary>
/// Get or set whether pitch records should be recorded into a history buffer
/// </summary>
public bool RecordPitchRecords
{
get { return m_recordPitchRecords; }
set
{
if (m_recordPitchRecords == value)
return;
m_recordPitchRecords = value;
if (!m_recordPitchRecords)
m_pitchRecords = new List<PitchRecord>();
}
}
/// <summary>
/// Get or set the max number of pitch records to keep. A value of 0 means no limit.
/// Don't leave this at 0 when RecordPitchRecords is true and this is used in a realtime
/// application since the buffer will grow indefinately!
/// </summary>
public int PitchRecordHistorySize
{
get { return m_pitchRecordHistorySize; }
set
{
m_pitchRecordHistorySize = value;
m_pitchRecords.Capacity = m_pitchRecordHistorySize;
}
}
/// <summary>
/// Get the current pitch records
/// </summary>
public IList PitchRecords
{
get { return m_pitchRecords.AsReadOnly(); }
}
/// <summary>
/// Get the latest pitch record
/// </summary>
public PitchRecord CurrentPitchRecord
{
get { return m_curPitchRecord; }
}
/// <summary>
/// Get the current pitch position
/// </summary>
public long CurrentPitchSamplePosition
{
get { return m_curPitchSamplePos; }
}
/// <summary>
/// Get the minimum frequency that can be detected
/// </summary>
public static float MinDetectFrequency
{
get { return kMinFreq; }
}
/// <summary>
/// Get the maximum frequency that can be detected
/// </summary>
public static float MaxDetectFrequency
{
get { return kMaxFreq; }
}
/// <summary>
/// Get the frequency step
/// </summary>
public static double FrequencyStep
{
get { return Math.Pow(2.0, 1.0 / kOctaveSteps); }
}
/// <summary>
/// Get the number of samples that the detected pitch is offset from the input buffer.
/// This is just an estimate to sync up the samples and detected pitch
/// </summary>
public int DetectSampleOffset
{
get { return (m_pitchBufSize + m_detectOverlapSamples) / 2; }
}
/// <summary>
/// Reset the pitch tracker. Call this when the sample position is
/// not consecutive from the previous position
/// </summary>
public void Reset()
{
m_curPitchIndex = 0;
m_curPitchSamplePos = 0;
m_pitchRecords.Clear();
m_iirFilterLoLo.Reset();
m_iirFilterLoHi.Reset();
m_iirFilterHiLo.Reset();
m_iirFilterHiHi.Reset();
m_circularBufferLo.Reset();
m_circularBufferLo.Clear();
m_circularBufferHi.Reset();
m_circularBufferHi.Clear();
m_pitchBufLo.Clear();
m_pitchBufHi.Clear();
m_circularBufferLo.StartPosition = -m_detectOverlapSamples;
m_circularBufferLo.Available = m_detectOverlapSamples;
m_circularBufferHi.StartPosition = -m_detectOverlapSamples;
m_circularBufferHi.Available = m_detectOverlapSamples;
}
/// <summary>
/// Process the passed in buffer of data. During this call, the PitchDetected event will
/// be fired zero or more times, depending how many pitch records will fit in the new
/// and previously cached buffer.
///
/// This means that there is no size restriction on the buffer that is passed into ProcessBuffer.
/// For instance, ProcessBuffer can be called with one very large buffer that contains all of the
/// audio to be processed (many PitchDetected events will be fired), or just a small buffer at
/// a time which is more typical for realtime applications. In the latter case, the PitchDetected
/// event might not be fired at all since additional calls must first be made to accumulate enough
/// data do another pitch detect operation.
/// </summary>
/// <param name="inBuffer">Input buffer. Samples must be in the range -1.0 to 1.0</param>
/// <param name="maxVolume"></param>
/// <param name="sampleCount">Number of samples to process. Zero means all samples in the buffer</param>
public void ProcessBuffer(float[] inBuffer, int sampleCount = 0)
{
if (inBuffer == null)
throw new ArgumentNullException("inBuffer", "Input buffer cannot be null");
int samplesProcessed = 0;
int srcLength = sampleCount == 0 ? inBuffer.Length : Math.Min(sampleCount, inBuffer.Length);
float maxVolume = LastMaxVol * 0.85f;
while (samplesProcessed < srcLength)
{
int frameCount = Math.Min(srcLength - samplesProcessed, m_pitchBufSize + m_detectOverlapSamples);
m_iirFilterLoLo.FilterBuffer(inBuffer, samplesProcessed, m_pitchBufLo, 0, frameCount);
m_iirFilterLoHi.FilterBuffer(m_pitchBufLo, 0, m_pitchBufLo, 0, frameCount);
m_iirFilterHiLo.FilterBuffer(inBuffer, samplesProcessed, m_pitchBufHi, 0, frameCount);
m_iirFilterHiHi.FilterBuffer(m_pitchBufHi, 0, m_pitchBufHi, 0, frameCount);
m_circularBufferLo.WriteBuffer(m_pitchBufLo, frameCount);
m_circularBufferHi.WriteBuffer(m_pitchBufHi, frameCount);
// Loop while there is enough samples in the circular buffer
while (m_circularBufferLo.ReadBuffer(m_pitchBufLo, m_curPitchSamplePos, m_pitchBufSize + m_detectOverlapSamples))
{
float pitch1;
float pitch2;
float detectedPitch = 0.0f;
m_circularBufferHi.ReadBuffer(m_pitchBufHi, m_curPitchSamplePos, m_pitchBufSize + m_detectOverlapSamples);
maxVolume = 0f;
for (int i = 0; i < (m_pitchBufSize + m_detectOverlapSamples) / 2; i++)
{
if (maxVolume < m_pitchBufHi[i])
maxVolume = m_pitchBufHi[i];
if (maxVolume < m_pitchBufLo[i])
maxVolume = m_pitchBufLo[i];
}
pitch1 = (maxVolume >= m_detectLevelThreshold) ? m_dsp.DetectPitch(m_pitchBufLo, m_pitchBufHi) : 0f;
if (pitch1 > 0.0f)
{
// Shift the buffers left by the overlapping amount
m_pitchBufLo.Copy(m_pitchBufLo, m_detectOverlapSamples - 1, 0, m_pitchBufSize);
m_pitchBufHi.Copy(m_pitchBufHi, m_detectOverlapSamples - 1, 0, m_pitchBufSize);
pitch2 = m_dsp.DetectPitch(m_pitchBufLo, m_pitchBufHi);
if (pitch2 > 0.0f)
{
float fDiff = Math.Max(pitch1, pitch2) / Math.Min(pitch1, pitch2) - 1.0f;
if (fDiff < m_maxOverlapDiff)
detectedPitch = (pitch1 + pitch2) * 0.5f;
}
}
// Log the pitch record
AddPitchRecord(detectedPitch);
m_curPitchSamplePos += m_samplesPerPitchBlock;
m_curPitchIndex++;
}
samplesProcessed += frameCount;
}
LastMaxVol = maxVolume;
}
/// <summary>
/// Setup
/// </summary>
private void Setup()
{
if (m_sampleRate < 1.0f)
return;
m_dsp = new PitchDsp(m_sampleRate, kMinFreq, kMaxFreq);
m_iirFilterLoLo = new IIRFilter();
m_iirFilterLoLo.Proto = IIRFilter.ProtoType.Butterworth;
m_iirFilterLoLo.Type = IIRFilter.FilterType.HP;
m_iirFilterLoLo.Order = 5;
m_iirFilterLoLo.FreqLow = 45.0f;
m_iirFilterLoLo.SampleRate = (float)m_sampleRate;
m_iirFilterLoHi = new IIRFilter();
m_iirFilterLoHi.Proto = IIRFilter.ProtoType.Butterworth;
m_iirFilterLoHi.Type = IIRFilter.FilterType.LP;
m_iirFilterLoHi.Order = 5;
m_iirFilterLoHi.FreqHigh = 280.0f;
m_iirFilterLoHi.SampleRate = (float)m_sampleRate;
m_iirFilterHiLo = new IIRFilter();
m_iirFilterHiLo.Proto = IIRFilter.ProtoType.Butterworth;
m_iirFilterHiLo.Type = IIRFilter.FilterType.HP;
m_iirFilterHiLo.Order = 5;
m_iirFilterHiLo.FreqLow = 45.0f;
m_iirFilterHiLo.SampleRate = (float)m_sampleRate;
m_iirFilterHiHi = new IIRFilter();
m_iirFilterHiHi.Proto = IIRFilter.ProtoType.Butterworth;
m_iirFilterHiHi.Type = IIRFilter.FilterType.LP;
m_iirFilterHiHi.Order = 5;
m_iirFilterHiHi.FreqHigh = 2000.0f;
m_iirFilterHiHi.SampleRate = (float)m_sampleRate;
m_detectOverlapSamples = (int)(kDetectOverlapSec * m_sampleRate);
m_maxOverlapDiff = kMaxOctaveSecRate * kDetectOverlapSec;
m_pitchBufSize = (int)(((1.0f / (float)kMinFreq) * 2.0f + ((kAvgCount - 1) * kAvgOffset)) * m_sampleRate) + 16;
m_pitchBufLo = new float[m_pitchBufSize + m_detectOverlapSamples];
m_pitchBufHi = new float[m_pitchBufSize + m_detectOverlapSamples];
m_samplesPerPitchBlock = (int)Math.Round(m_sampleRate / m_pitchRecordsPerSecond);
m_circularBufferLo = new CircularBuffer<float>((int)(kCircularBufSaveTime * m_sampleRate + 0.5f) + 10000);
m_circularBufferHi = new CircularBuffer<float>((int)(kCircularBufSaveTime * m_sampleRate + 0.5f) + 10000);
}
/// <summary>
/// The pitch was detected - add the record
/// </summary>
/// <param name="pitch"></param>
private void AddPitchRecord(float pitch)
{
int midiNote = 0;
int midiCents = 0;
PitchDsp.PitchToMidiNote(pitch, out midiNote, out midiCents);
var record = new PitchRecord();
record.RecordIndex = m_curPitchIndex;
record.Pitch = pitch;
record.MidiNote = midiNote;
record.MidiCents = midiCents;
m_curPitchRecord = record;
if (m_recordPitchRecords)
{
if (m_pitchRecordHistorySize > 0 && m_pitchRecords.Count >= m_pitchRecordHistorySize)
m_pitchRecords.RemoveAt(0);
m_pitchRecords.Add(record);
}
if (this.PitchDetected != null)
this.PitchDetected(this, record);
}
/// <summary>
/// Stores one record
/// </summary>
public struct PitchRecord
{
/// <summary>
/// The index of the pitch record since the last Reset call
/// </summary>
public int RecordIndex { get; set; }
/// <summary>
/// The detected pitch
/// </summary>
public float Pitch { get; set; }
/// <summary>
/// The detected MIDI note, or 0 for no pitch
/// </summary>
public int MidiNote { get; set; }
/// <summary>
/// The offset from the detected MIDI note in cents, from -50 to +50.
/// </summary>
public int MidiCents { get; set; }
}
}
// ReSharper restore InconsistentNaming
}
+11 -1
View File
@@ -223,7 +223,7 @@
<Compile Include="Lib\Sound\Playback\OpenAL\COpenAlStream.cs" />
<Compile Include="Lib\Sound\Playback\PortAudio\CPortAudioStream.cs" />
<Compile Include="Lib\Sound\CPortAudioHandle.cs" />
<Compile Include="Lib\Sound\Record\CAnalyzer.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\CAnalyzer.cs" />
<Compile Include="Lib\Sound\Record\CBuffer.cs" />
<Compile Include="Base\CConfig.cs" />
<Compile Include="Base\CCover.cs" />
@@ -268,6 +268,16 @@
<Compile Include="Lib\Sound\Record\CRecordBase.cs" />
<Compile Include="Lib\Sound\Record\DirectSound\CSampleDataEventArgs.cs" />
<Compile Include="Lib\Sound\Record\DirectSound\CSoundCardSource.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\CPitchTrackerTest.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\CPtAKF.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\CPtDyWa.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\CPtSharp.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\IPitchTracker.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\Pitch\CircularBuffer.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\Pitch\Extensions.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\Pitch\IIRFilter.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\Pitch\PitchDsp.cs" />
<Compile Include="Lib\Sound\Record\PitchTracker\Pitch\PitchTracker.cs" />
<Compile Include="Lib\Video\Acinerella\CDecoder.cs" />
<Compile Include="Lib\Video\Acinerella\CDecoderThread.cs" />
<Compile Include="Lib\Video\Acinerella\CFramebuffer.cs" />
+294
View File
@@ -0,0 +1,294 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace VocaluxeLib.Utils
{
//Simple class to read and write WAV files
public class CWavFile
{
public short NumChannels { get; private set; }
public int SampleRate { get; private set; }
public short BitsPerSample { get; private set; }
public string FileName { get; private set; }
//Unless this is true, all fields are to be considered as invalid
public int DataSize { get; private set; }
//Number of samples (per channel)
public int NumSamples
{
get { return (DataSize / (BitsPerSample / 8 * NumChannels)); }
}
//Number of samples left to be read (per channel)
public int NumSamplesLeft { get; private set; }
public bool IsOpen { get; private set; }
private int _DataPos;
private FileStream _File;
private bool _IsWritable;
public CWavFile()
{
IsOpen = false;
}
~CWavFile()
{
Debug.Assert(!IsOpen);
Close();
}
public void Create(string fileName, short numChannels, int sampleRate, short bitsPerSample)
{
if (IsOpen)
Close();
_File = new FileStream(fileName, FileMode.Create);
FileName = fileName;
NumChannels = numChannels;
SampleRate = sampleRate;
BitsPerSample = bitsPerSample;
DataSize = 0;
NumSamplesLeft = 0;
IsOpen = true;
_IsWritable = true;
_WriteHeader();
}
public void Close()
{
if (!IsOpen)
return;
if (_IsWritable)
{
BinaryWriter writer = new BinaryWriter(_File);
writer.Seek(4, SeekOrigin.Begin);
writer.Write((int)_File.Length - 8);
writer.Seek(_DataPos - 4, SeekOrigin.Begin);
writer.Write(DataSize);
}
_File.Dispose();
_File = null;
IsOpen = false;
}
public bool Open(string fileName)
{
if (IsOpen)
Close();
if (!File.Exists(fileName))
return false;
FileName = fileName;
_File = new FileStream(FileName, FileMode.Open);
try
{
BinaryReader reader = new BinaryReader(_File);
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
return false;
reader.ReadInt32(); //Chunksize (file)
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
return false;
long curPos = _File.Position;
if (_SeekTo(reader, "fmt ") < 0)
return false;
short audioFormat = reader.ReadInt16();
if (audioFormat != 1) //PCM
return false;
NumChannels = reader.ReadInt16();
SampleRate = reader.ReadInt32();
int bytesPerSec = reader.ReadInt32();
short blockAlign = reader.ReadInt16();
BitsPerSample = reader.ReadInt16();
if (BitsPerSample != 8 && BitsPerSample != 16)
return false;
if (blockAlign != NumChannels * (BitsPerSample / 8))
return false;
if (bytesPerSec != blockAlign * SampleRate)
return false;
_File.Position = curPos;
DataSize = _SeekTo(reader, "data");
if (DataSize <= 0)
return false;
_DataPos = (int)_File.Position;
DataSize = Math.Min(DataSize, (int)_File.Length - _DataPos);
NumSamplesLeft = NumSamples;
}
catch (Exception)
{
_File.Dispose();
throw;
}
IsOpen = true;
return true;
}
/// <summary>
/// Gets an array of numSamples samples per channel for the selected channel(s)
/// </summary>
/// <param name="numSamples">Samples per channel</param>
/// <param name="channel">Channel for which to get the samples (0=all)</param>
/// <returns></returns>
public byte[] GetNextSamples8Bit(int numSamples, int channel = 0)
{
if (channel < 0 || channel > NumChannels || BitsPerSample != 8)
return null;
if (numSamples > NumSamplesLeft)
return null;
byte[] result;
BinaryReader reader = new BinaryReader(_File);
if (channel == 0 || NumChannels == 1)
result = reader.ReadBytes(numSamples * NumChannels);
else
{
result = new byte[numSamples];
for (int i = 0; i < numSamples; i++)
{
_File.Seek(channel - 1, SeekOrigin.Current);
result[i] = reader.ReadByte();
_File.Seek(NumChannels - channel, SeekOrigin.Current);
}
}
NumSamplesLeft -= numSamples;
return result;
}
/// <summary>
/// Gets an array of numSamples samples per channel for the selected channel(s)
/// </summary>
/// <param name="numSamples">Samples per channel</param>
/// <param name="channel">Channel for which to get the samples (0=all)</param>
/// <returns></returns>
public short[] GetNextSamples16Bit(int numSamples, int channel = 0)
{
if (channel < 0 || channel > NumChannels || BitsPerSample != 16)
return null;
if (numSamples > NumSamplesLeft)
return null;
short[] result;
BinaryReader reader = new BinaryReader(_File);
try
{
if (channel == 0 || NumChannels == 1)
{
result = new short[numSamples * NumChannels];
for (int i = 0; i < numSamples * NumChannels; i++)
result[i] = reader.ReadInt16();
}
else
{
result = new short[numSamples];
for (int i = 0; i < numSamples; i++)
{
_File.Seek((channel - 1) * 2, SeekOrigin.Current);
result[i] = reader.ReadInt16();
_File.Seek((NumChannels - channel) * 2, SeekOrigin.Current);
}
}
NumSamplesLeft -= numSamples;
}
catch (EndOfStreamException e)
{
Console.WriteLine("Error: " + e);
return null;
}
return result;
}
public byte[] GetNextSamples16BitAsBytes(int numSamples, int channel = 0)
{
short[] samples = GetNextSamples16Bit(numSamples, channel);
if (samples == null)
return null;
byte[] samplesByte = new byte[samples.Length * 2];
Buffer.BlockCopy(samples, 0, samplesByte, 0, samplesByte.Length);
return samplesByte;
}
public bool WriteSamples(byte[] samples, int channel = 0)
{
if (!_IsWritable || channel < 0 || channel > NumChannels || BitsPerSample != 8)
return false;
BinaryWriter writer = new BinaryWriter(_File);
if (channel == 0 || NumChannels == 1)
writer.Write(samples);
else
{
foreach (byte sample in samples)
{
_File.Seek(channel - 1, SeekOrigin.Current);
writer.Write(sample);
_File.Seek(NumChannels - channel, SeekOrigin.Current);
}
}
DataSize += samples.Length;
return true;
}
public bool Write16BitSamples(short[] samples, int channel = 0)
{
if (!_IsWritable || channel < 0 || channel > NumChannels || BitsPerSample != 16)
return false;
BinaryWriter writer = new BinaryWriter(_File);
if (channel == 0 || NumChannels == 1)
{
foreach (short sample in samples)
writer.Write(sample);
}
else
{
foreach (short sample in samples)
{
_File.Seek((channel - 1) * 2, SeekOrigin.Current);
writer.Write(sample);
_File.Seek((NumChannels - channel) * 2, SeekOrigin.Current);
}
}
DataSize += samples.Length * 2;
return true;
}
public bool Write16BitSamples(byte[] samples, int channel = 0)
{
if (samples.Length == 0)
return true;
short[] samplesShort = new short[samples.Length / 2];
Buffer.BlockCopy(samples, 0, samplesShort, 0, samples.Length);
return Write16BitSamples(samplesShort, channel);
}
private void _WriteHeader()
{
if (!IsOpen || !_IsWritable)
return;
BinaryWriter writer = new BinaryWriter(_File);
writer.Write(Encoding.ASCII.GetBytes("RIFF"));
writer.Write(0);
writer.Write(Encoding.ASCII.GetBytes("WAVE"));
writer.Write(Encoding.ASCII.GetBytes("fmt "));
writer.Write(16);
writer.Write((short)1); // PCM
writer.Write(NumChannels); // Channels
writer.Write(SampleRate); // Sample rate
writer.Write(SampleRate * NumChannels * (BitsPerSample / 8)); // Average bytes per second
writer.Write((short)((BitsPerSample / 8) * NumChannels)); // block align
writer.Write(BitsPerSample); // bits per sample
writer.Write(Encoding.ASCII.GetBytes("data"));
writer.Write(DataSize);
_DataPos = (int)_File.Position;
writer.Seek(4, SeekOrigin.Begin);
writer.Write((int)writer.BaseStream.Length);
writer.Seek(_DataPos, SeekOrigin.Begin);
}
private static int _SeekTo(BinaryReader reader, string chunkId)
{
do
{
string curChunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
int chunkSize = reader.ReadInt32();
if (curChunkId == chunkId)
return chunkSize;
reader.BaseStream.Seek(chunkSize, SeekOrigin.Current);
} while (reader.BaseStream.Position < reader.BaseStream.Length);
return -1;
}
}
}
+1
View File
@@ -188,6 +188,7 @@
<Compile Include="PartyModes\CScreenPartyDummy.cs" />
<Compile Include="PartyModes\IPartyMode.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Utils\CWavFile.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>