Pitch tracking in a DaisySP/libdaisy project using ChatGPT code

I’ll just leave this here…

It’s not perfect, but holy crap it WORKS! Enjoy

#include "daisysp.h"
#include "daisy_patch.h"
#include <string>
#include <cmath>
#include <complex>
#include <vector>
#include <algorithm>

# define M_PI 3.14159265358979323846  /* pi */

// Interleaved audio definitions
#define LEFT (i)
#define RIGHT (i + 1)

//Set FFT window size
#define WINDOW 2048.0

// Set print interval in ms
#define PRINT_INTERVAL 250

//Set sample rate, block size
#define SAMPLE_RATE 48000.0
#define BLOCK_SIZE 64


using namespace daisysp;
using namespace daisy;


// Set up Daisy hardware seed
static DaisyPatch hw;

// Global variables
float detectedPitch = 440.0;
float lastPitch=100000000000.0;
double count  = 0.0;
int printout=0;


// begin CHATGPT-written pitch detection code------------------------------------------------------------------------------
static DelayLine<float, static_cast<size_t>(WINDOW)> DSY_SDRAM_BSS PDsignal;

float findInterpolatedFrequency(const std::vector<std::complex<float>>& signal, float sampleRate) {
    size_t N = signal.size();
    float maxMag = 0.0f;
    size_t maxIndex = 0;

    // Find the index with the highest magnitude (real part squared + imaginary part squared)
    for (size_t i = 1; i < N / 2 - 1; ++i) {  // Avoid boundary issues
        float magnitude = std::norm(signal[i]);
        if (magnitude > maxMag) {
            maxMag = magnitude;
            maxIndex = i;
        }
    }

    // Parabolic interpolation for more accurate peak frequency estimation
    size_t i = maxIndex;
    float magnitude0 = std::norm(signal[i - 1]);
    float magnitude1 = std::norm(signal[i]);
    float magnitude2 = std::norm(signal[i + 1]);

    // Apply quadratic interpolation formula:
    // f = f_max + 0.5 * (magnitude1 - magnitude2) / (magnitude0 - 2 * magnitude1 + magnitude2)
    float peakShift = 0.5f * (magnitude0 - magnitude2) / (magnitude0 - 2.0f * magnitude1 + magnitude2);
    
    // The frequency corresponding to the peak
    float frequency = (float)i * sampleRate / N + peakShift * sampleRate / N;

    return frequency;
}

void applyHanningWindow(std::vector<std::complex<float>>& signal) {
    size_t N = signal.size();
    for (size_t i = 0; i < N; ++i) {
        float windowValue = 0.5f * (1.0f - cos(2.0f * (float)M_PI * i / (N - 1)));
        signal[i] *= windowValue;  // Apply the window to each signal sample
    }
}

class PitchDetector {
public:
    PitchDetector(float sampleRate) : m_sampleRate(sampleRate) {}

    // Function to perform the FFT (Cooley-Tukey Radix-2 FFT)
    void fft(std::vector<std::complex<float>>& signal) {
        size_t N = signal.size();
        
        // Base case: single element, do nothing
        if (N <= 1)
            return;
        
        // Split the signal into even and odd indexed parts
        std::vector<std::complex<float>> even(N / 2);
        std::vector<std::complex<float>> odd(N / 2);
        
        for (size_t i = 0; i < N / 2; ++i) {
            even[i] = signal[2 * i];    // Even indexed
            odd[i] = signal[2 * i + 1]; // Odd indexed
        }
        
        // Recursively compute the FFT of the even and odd parts
        fft(even);
        fft(odd);
        
        // Combine the results
        for (size_t k = 0; k < N / 2; ++k) {
            std::complex<float> t = std::polar(1.0f, -2.0f * (float)M_PI * k / N) * odd[k];
            signal[k] = even[k] + t;
            signal[k + N / 2] = even[k] - t;
        }
    }

    // Function to find the frequency with the highest magnitude (FFT result)
    float findDominantFrequency(const std::vector<std::complex<float>>& signal, float sampleRate) {
        size_t N = signal.size();
        float maxMag = 0.0f;
        size_t maxIndex = 0;

        // Find the index with the highest magnitude (real part squared + imaginary part squared)
        for (size_t i = 0; i < N / 2; ++i) {
            float magnitude = std::norm(signal[i]); // norm = real^2 + imag^2
            if (magnitude > maxMag) {
                maxMag = magnitude;
                maxIndex = i;
            }
        }

        // Calculate the frequency corresponding to that index
        return (float)maxIndex * sampleRate / N;
    }

    // Function to detect harmonics and the fundamental frequency (up to the 5th harmonic)
    float detectPitch() {
        // Create a complex vector to hold the FFT result (size must be a power of 2)
        size_t N = 1024;
        while (N < WINDOW) N *= 2; // Ensure N is a power of 2 (padded if necessary)

        std::vector<std::complex<float>> fftSignal(N, {0.0f, 0.0f});
        
        // Copy the delay line (time-domain signal) into the real part of the FFT input
        for (int i = 0; i < WINDOW; ++i) {
            fftSignal[i] = std::complex<float>(PDsignal.ReadSetLoc(i), 0.0f);  // Only use the real part (left channel)
        }

        // Apply the Hanning window to the signal
        applyHanningWindow(fftSignal);

        // Perform the FFT
        fft(fftSignal);

        // Find the dominant frequency (fundamental frequency)
        float fundamentalFrequency = findInterpolatedFrequency(fftSignal, m_sampleRate);

        // Detect harmonics up to the 5th harmonic
        std::vector<float> harmonics;
        for (int i = 1; i <= 5; ++i) {
            float harmonicFrequency = fundamentalFrequency * i;
            
            // Find the closest FFT bin to the harmonic frequency
            size_t binIndex = (size_t)(harmonicFrequency * N / m_sampleRate);
            
            // If the bin has a significant magnitude, consider it a harmonic
            if (binIndex < N / 2 && std::norm(fftSignal[binIndex]) > 0.5f) {
                harmonics.push_back(harmonicFrequency);
            }
        }

        // If we found harmonics, we return the fundamental frequency as the most likely pitch
        if (!harmonics.empty()) {
            return fundamentalFrequency;
        }

        // Otherwise, return the fundamental frequency based on the highest peak
        return fundamentalFrequency;
    }

private:
    float m_sampleRate;  // Sample rate of the audio (e.g., 44100 Hz)
};

// end CHATGPT-written pitch detection code----------------------------------------------------------------------------------------------


// DECLARE PITCH DETECTOR
PitchDetector pitchDetector(SAMPLE_RATE);



//
// Audio Callback function  (the audio loop)
//
static void AudioCallback(AudioHandle::InterleavingInputBuffer  in,
                          AudioHandle::InterleavingOutputBuffer out,
                          size_t                                size)
{
    for(size_t i = 0; i < size; i += 2)
    {
        PDsignal.Write(in[LEFT]);
        out[LEFT]  = in[LEFT];
        out[RIGHT] = in[RIGHT];
    }
    count  += BLOCK_SIZE;
}


int main(void)
{
    // Initialize seed hardware and daisysp modules
    hw.seed.Configure();
    hw.Init();
    hw.SetAudioBlockSize(BLOCK_SIZE);


    
    // Initialize and set up the delay lines
    PDsignal.Init();
    PDsignal.SetDelay((float)1200.0);


    // Attempt serial communication
    hw.seed.StartLog(false);

    // Start audio loop

    hw.seed.StartAudio(AudioCallback);



    while(1) {

        detectedPitch = pitchDetector.detectPitch();

        // Trigger pring flag every PRINT_INTERVAL ms
        if (count >= (((float)PRINT_INTERVAL)*(SAMPLE_RATE/1000.0f))){
            printout = 1;
            count = 0;
        }

        // Print messages if print flag is triggered
        if((printout>0)){
            hw.seed.PrintLine("pitch=%f",
                              detectedPitch);
        }else{
            hw.seed.Print("");
        }



    }
}

Here’s the VScode project folder if anyone wants it. I just modified the delayline example in DaisyExamples\seed\DSP

https://drive.google.com/drive/folders/1ShyX8Pt48Y2r5DEL3fvwszXrc2o0EyAj?usp=sharing

this is really cool. i’d like to learn to implement this but i’m over here in oopsy~ world so i can’t figure out how yet.

Hi all,

well, ryan, i don’t really understand your example yet, but i was able to command Claude to use this code to generate a simpler pitch tracker in gen~ based on autocorrelation, which is not perfect, but works.

this patch compares it to the native pitchtracker fzero~ and it isn’t amazing but it isn’t garbage. @ryan7585 thanks so much! now onwards to making some cool music with it.


----------begin_max5_patcher----------
4807.3oc6cr1biaa7y99UvQocpShNaB9Rj024oWa5zjNoWRSam9gbYzPIBIw
yzjpjP5ruL1+1KdQRPJ9.jhR12EcybxR.fXefE6tX4Bfe8EmMZVzcvjQJ+Qk
eV4ry90Wb1YzhHEbF+2mM5V26lG3lPa1nP3Ghl89QiYUgf2gnEGD45cKLIQA
jV0ZWz7U9gKmFCmiXP..ztPcrhlM8OSTo+.+ckeg+Lgat0OL.hnfBvKz2iBA
LTeooyn7VFsAUtorhP2uFx.3nQYc8JeOOXHswjBd3Euf7w38ipWqfoxUIq7Q
MP01T5bBkls.clls6HI6GhpjpEQPXLmV4D6YiV3G.2BiS7iDa8YibWuVn3yD
dDBG58QzNxdbVQ9grhrxJJFt0O84MxJ0MFylPXdzlXJhO5NKiQ4cSjGLNbie
1vEcrhiRzQkPWrz1Z24rGlL3kVs.iWylvsMT0o+QaB4O51SxY93g8kAQyuAR
Y2poEFsFF5GtNFl.CQtHNxmUsGbg6l.zzEQgnD+ORQApjcE0ufihUVIgHn3+
ah8cCxnfkw9dQgDjnvPAo3TvgmKYRnFxm4DCsEgtqq3gwRRX9RMUlfIxMIyb
iIiTyBnPPKsRTTTPwpxdt.3BDu509ggk3hnn00WYr+xUM7ryhvUdaS8Msljo
aBY0NEKTfll3tsH2F4FDvm6Vr6uyMz+VWDD4yFBzTypDF5hIzUIyiiBBJPur
Z1VQMdXg74vO36gVQAjnv.t49qSEhFkMJ64uDlfJVFxcYRwRRP2yX5BEsYFe
R7TD710AXpHUGU1uyl9lj3mfRVE8gD9yjJyIxKxs.HN+VTmXgxKoajoAJaxa
Mp9UMny9TUY+I8yLg25TEVm5vJTIpJTy7nauEFVj8R6oPO3cBpU3VB3ZX5IG
nf0ghVHH5UI1HTRfDUdJpsvmz3lH0cH+wYRYyERymlTOeR7Ipxh4wgyfUuh0
C4gYLH43I.G6bCo8hmX8bmmrHJ.a3qsoR5lWnK9OfQl.C6ydwbLqm4n0HyYb
pOGGGdDRQUIPgf.IJAxo0AXZQc9xp2LGi5YNFMwbHLlwLdDEio+3XwoRVGig
+Bkj6I3qxuOIIZSnWxkRxz.TkO.Cydy0zeVLeihlRZdxQVqSpUSwZ8mh2WiW
4tAF3GVmEcJoSpuZ9FV.IddJNw0XpTjKf8mA4Gl4T7OmYsgzNoF05JNXJINX
e.wAimI3ftD3f0AFG.RfClGXbP6IFGzkTdv3.hCZRhC50hC7BSWZ8Hxxm7lx
Vp3TWDJ1e1FDSUhXrB5zRZvqudlaPoEeT0JddQNxMLAmAsxOcQOMDclI5rnB
PcPXRmiNCPu5vynUaDoFWSToDFC3M8KdK0uAQxPYzUjO9h+I4Sr2yrRXiicj
qsAahYSqgsBnx78Vc2f04iWsI2Lynkn3X3hK9f6VBFMl7Qh6sahc8mAcQW35
uHq7+0euhedgah2H4Y5l43cLVjBgWJLas4BllKMfn2T7xFmMtLfwFju9FZzV
ZfMSk+.5TeSY+naBfS5dHQGNhLKruVpMPi5SnSrXKeqGywdlE0Woh0M0q4Wp
o1yHca.ddQytdyeroQX9hErXNPa1c5sq5QS7WFRhYZ125kXMYXzcIbGxskIs
F5B5FaiZ0pfZMdJmzFF4m.aZzTyhQeZTkRFFcezzrY5q5HmW6f7Aw9lYlJIG
yFssEsH2ZEcEzjX5kWx8gnUY+hxZy9EdRi7F0Lr5uQMY4mGJacekh5ENNMIQ
wdcE17WHS2mvzUqbKvJoQCJMdKJZQSuaSlgMSVbDz59TF6ihJgaRB78ZzgXf
fwZfCUCftQWFpb5tbrVKBr4wWbfjWeDKvZ1NSfaOqGBrlpcb3beT0UCURHmG
aeRYpZ9VHR8JHRvSOQl39gGkXfjQi5S59.o1SOMRb3bla3xlnSUaAGNMs6dp
UnOLlro34.4KZcdmQdgZsyKrkQSbUi3FCCmXn3BnnkKCfMMOVSPDWyH8S4G5
6wxoaI8SFv42KVG3d+iJ+ofnn0MuhKMAU1Nc2obSqlWBRGcZcb1bgA1t0DI7
bu+1sl7z64dcRBv.LcFE+nhQ6q7ViMOP2pkHpXVAKv9omE.+nmayK3dBkLmv
zuwdGcFlcwMsZbPQcHyNN9hipmJXKHvRmlVblcWbUWefGqDdtJyNqeKk+brw
CG5DIKltDCa0SYO2orm6yvrm6nlxbMkzF9Ker0jUvT3cxnyrv0mboCHY5JTy
TUYzjVm1ziSFvf8lccxsQQnUOh4PF1zOjj6xRdXfgU6rW8pYuNelycodi1B2
jYCAXwDUkfWpsWoy3mtRpBwpoEdIOq16ubIv5yAloLo0EiiwWFjY5mcNqtrs
e5RqqCaVuYNL7GmiC+Qa.4ORk057UjHOCpFCsNpGvrV+fj3eNcHw+.GnDbxV
Vj.nc.SyJaaYwhCYh2MQRjfHocvPBvygzAEHadu474ddugQRTTSoofoI+sAX
l5qPQOFZMxv5FCXjg6ezPWDDggQV7kvdRDtrHJTUNZvhAJm34Zx06Bwa1+Wj
YAWmza3cDLt7aLYQT7stHgP.UmaTGCdHO9b6AOz5yWdXkxb5rj.PSl3YVE+R
a.e44MxXFv3xG5tNYUD5Qr+jMlyTrPU6PmVZC5drdAGub.o03uaYvd8h.POi
+tl4yf3uawxsKvDsdF+cGvo3ueJ96mh+9o3ueJ96mh+9o3ueJ96mh+9o3ueJ
96mh+9o3ueJ96mh+9o3u+a43uaqZIlY1cO96ZfOci+9DMwcWROhcrl1u4i+d
Z745OOT+yIdXMw4bwGgwQMEr1IpB4WrQ22adfZ1CP5MG44wBeYHxD9st4JxV
6ioRTr67ax2LeYLjkvvGU9SHeLVoTUCqZiCXv1w.FrsKUO1A20jI1lRuAta9
aY3wmkA.1KY8EKy1x4EBBrC6v3hed.RGdlXadJJvmhB7on.evhBL47xyrkEm
NgE+WK9V9t+GSdMDEX0iSzuHjqQKjqEKXE6O457rfb0Z6PGUyg8d2lrmjq8y
BxUuExUeBXXFcmbbH24XCrBu0z5BvlM+7+jsY43QtcG5pJibCvYJqUOika1Q
zg3+qOxtu5efMGRckv65QEBlkGqAeiKxUwcimezzYaVr.FeN40vfYKe4UuK7
cg+HYkAJItX0vvowXMwmaXqhMknfI9Wy+l6cu1wB+UxSvZOtxoKhg+uys4sT
i2NyBsx8NVq.Y8nlZdCEZIZE1UkUQAdmqdgEqk71wQyuEaOHJ9dkODic6Zpu
2cmSe5zh8fDmwvFQoN7Vrt4QgK78fgygEKeMz8loD51GswqbcwQygXiPgKK0
WahiwdTgsUWphYXKg6VJgALOJNtbmC2VQwj9thhQQXW.vKaCFu79h0rfbbzL
kPFrxeW3kWp72fH1nsB1+uMn2Eh8fV403e.Raw+kvBwcqBSb3cgqitAdtnDx
XE7CMNmWOVg1+Y+F2e4e+qUx64UvPdupf8HOHA2QH2Xj.67cg9KNO+gu90zW
J3Wp7quKTA+OQPndEqr7GFWHfWnv.AtT7BshhOWPHV4xLguuj+DoiP3lK7v7
JSGnvU9RPFb4CSB3R1XjPY4CCBEx9DyS9KtAy2P7vQgMDp7l29MTQOkLQOVa
EGmEo9BRoBU7dgu+gU30+b96UdEVmGdZUJ2j7us3lsFBuoz.764iooMycVxT
RSw+87shUfGtX0ccITo.TpDSoOmPW8P9WKQrE94Wqb9VkuRo.VPH12yE03c0
CbgtejIdn7Cu8upPFdWiE9XRBTYMAomqUxYMjA7Fjc3p2RQAb+j9.WicEpHG
Nuqv0jie4RA3mVTb8UoOQgdA2HQAoWqntCCFSr+K7DeHUxzOZShBQRDFPWeW
owBAY2LY1qJ1lc572rAEIziXA63zoJDNaw1WZRP5+tohxXRm2TkzYF0SmweC
dDd24lEZ2hyIM8UE0ZT9eaAUKzeSIg9BOiV0OSt5updpTVL4OD4V.QvUqpV+
vtEcCmjAkZ9CsLN8VR.1BvtSTrFLuov7nqIKhSETIWRDuurvzulwEQ8exHR8
8QQ2PkiXZ7lEsElaqeGzm1wWm2fJQ8B5TCwd4pTVlkBKlGA6Lwf7OxCMsF42
5jgkTNtVY4JFl6lLcekq6urcYVU92aVRuFo8lj3q4Q5hDc8Xaix20.4JE69t
EYpC8STV4ubE1TCZkK1mmHzJLDwkLKJF60iO5Ojn3REDqjp3B5Y3X8xRYsMS
idiRH4sO0ilFadIWepXFc4+0nWT08ulqkXC+M+za9y+v2+c+Eku6s+6+5O8i
+v2+l+828CukNaNYyrWxrNq3NGCS2422b+gc0O5Vh.elMvWpngkV4htYL8VP
ZliOmS6sujK50fr2NdBEsXQB1W7WSNq4vP+bQzQXf+RF9dU68JwYFgN9ZkWh
6YovmJd5WozkGt9QdLCUnauRtt6g1aVKM4g8QfqfC6fFv4GjV41C0Z0T3q4e
q3pWp1LwCJvfDn3HDdhx2DEBKrZppce75J7dj6CqT1XKtbZLJVz83TQgqpxw
hzEaiepT3ckLbmBKySsje9QaPDae3kkdE8GZh+PG+ihXLqbCpJsT7gUlI0Rn
3BUthoApvJxvqUaKLHZMj9LVTD5HkJh9gYGuUsjIhri5M61iUWM4MF3498gD
IxkswK.rPVt+QtznaQt7fceqnuW4Vy3AAG.Ow20IVRdGeL4.iCO02wGDbvPB
b3Pl.d.I4CVCc9dAuacTLhemnQlk9Obm6GhhRVo7seye7x+SBLN4RO2PeXvB
+jU23Gd4ReD1JB80De4O4dq6OB87Bf4W4U79r5znn+YBFMwZZ7n1UkuCZoJo
3WsFcIQd.FGwC.6M2NqYxQeBauUqSIGyctW0HIsQg2gbUuhkV2bg.PyGHm2x
eAG8+.5LkIcLxRII2lrZN5621j043Inz59gVah09senm7Ld6PW2oS7MgQyzl
hwwPOkeGnwyea.6PoVOeed1scD8w4LiOveK7BOg8fqXhnkU4Oa7KMdAgvtqL
32EyrcMscWRurFEE.cWTnzbelMoLiQMXVZqavFXzhcSmrbDCOuXYFGZwlO9w
eV+WxSNr71cKlOSQ+7WHrXsQdop3ppZrxDAKYDnTEHRo5p5gMg9nrjyIKM05
uYPIET.MInXvtIY1CAE0OoETzNInjInn0jfhI6Z5TicGI2CAEMmOoET.+VPP
oFeo95FO.vAbaM7qyOmKL6ncUMqQGoC89VOa+SoEMPOOb+0l7L9v8eoKB9nh
V6zugJK8zc5rORZ1OqtXwHNGpO0CtFspYmC0YI9cpygc+5L.ndTbNrIB0fEt
3FoSS1k0AmNs6NcZcTHyVOPf.r6bEK19BuGGHPfg67.hZAcoqe3i0ZesPsUQ
NVrk1CrRsqxujJaldHYv97Utggvf4QaX6jV0JnU6ta5sSajjB68l.+jihY4c
XrUZY1pZKyXlIo1WNQsMK25sZ4tYDQZ62FGL62RbgFMgsN.C1VnrGWnQftpX
3XbyEgbiWBQJqgH2fw72oFUGoi0Me6Gkfcvtnh4GXWcyP3SzEJ5rR2Aya8ge
ftQb7C7Q2W.EnuAYF3o91KtUYYawIuX2k4ackTMKyC7meCZUbzlkqDKemcAS
wJ1taELnjFDwzRmsrjWyXRNlGT5l1Ar.V9lyLpAXYXO8OMKDmMONJZcx8Wf4
lqcSOoDJbQ0TiECdoXQszqvRJxsGA+QBaIBqpel67aVFSdWzEFci8E2IZfV2
0qZraEF.aeSqKJLP1WPk1OPUyJkzdjVOhYfQpvGq6k5Rg6HZYxGqImr.aJ7e
otvKvY2FsKsJiQtVVA5damS8SH6bROKQevlkjd2I4ndrlkn6bZVR2mkn+4+r
DJWqXZPvlzT9UMym5ryqX1rvIMScuiagW1+NSOkFR1R.ohGwIixs0e3AMIbE
4YeQNnA6EnkhnMKzn1b5SVPCzKPPMlJICMnMk.zfgPpBLQBFro0PH+JEjrYo
Qz9AIKYgDXegj4wh6AjQOigyAQZzzPlIg5GFXqKCG13v.aMYfs5PL2.bzfjp
ryMz1SHYHkDq8.PSFxXjvrFKifCOnMlbHLJaHk9TvPvekVe5dCIozmND18Lz
kklL1WHIyb6Rr3gRClgwwx9jiLTo1PnASF0JCgDhLSuzGBVmtTJJUODpujGz
fgV8ktT72gPohtLpuz0GBHYJKj1WGO0kQ8ktyPPSx3DTJguePRFUH5ZCAjjw
IHvPnrRWb2.z9NhXufiQarNv.AnV4b1UqqR63.4AOFDRAYiCgOd5hapl5f7P
LrpIigfgvXWonDUij5PAn9JoBNRfdvsppIiUUsAYfzR1XBt2PRl4eNCxr.MY
gz9Z+VSFKPZlGDoSoA8vKcJypQFhfkI0Dvg.PRMI3vDhXYXlZCglTJjZSks1
t9kwd2JkNEaI.ozoWaoSt1cO0Zq+Dqs7oUKce5U20T1Kd3E+eRsXoDB
-----------end_max5_patcher-----------