Dlaczego ten kod wykrywania uderzeń nie rejestruje poprawnie niektórych uderzeń?

38

Zrobiłem tę klasę SoundAnalyzer do wykrywania uderzeń w piosenkach:

class SoundAnalyzer
{
    public SoundBuffer soundData;
    public Sound sound;
    public List<double> beatMarkers = new List<double>();

    public SoundAnalyzer(string path)
    {
        soundData = new SoundBuffer(path);
        sound = new Sound(soundData);
    }

    // C = threshold, N = size of history buffer / 1024  B = bands
    public void PlaceBeatMarkers(float C, int N, int B)
    {
        List<double>[] instantEnergyList = new List<double>[B];
        GetEnergyList(B, ref instantEnergyList);
        for (int i = 0; i < B; i++)
        {
            PlaceMarkers(instantEnergyList[i], N, C);
        }
        beatMarkers.Sort();
    }

    private short[] getRange(int begin, int end, short[] array)
    {
        short[] result = new short[end - begin];
        for (int i = 0; i < end - begin; i++)
        {
            result[i] = array[begin + i];
        }
        return result;
    }

    // get a array of with a list of energy for each band
    private void GetEnergyList(int B, ref List<double>[] instantEnergyList)
    {
        for (int i = 0; i < B; i++)
        {
            instantEnergyList[i] = new List<double>();
        }
        short[] samples = soundData.Samples;

        float timePerSample = 1 / (float)soundData.SampleRate;
        int sampleIndex = 0;
        int nextSamples = 1024;
        int samplesPerBand = nextSamples / B;

        // for the whole song
        while (sampleIndex + nextSamples < samples.Length)
        {
            complex[] FFT = FastFourier.Calculate(getRange(sampleIndex, nextSamples + sampleIndex, samples));
            // foreach band
            for (int i = 0; i < B; i++)
            {
                double energy = 0;
                for (int j = 0; j < samplesPerBand; j++)
                    energy += FFT[i * samplesPerBand + j].GetMagnitude();

                energy /= samplesPerBand;
                instantEnergyList[i].Add(energy);

            }

            if (sampleIndex + nextSamples >= samples.Length)
                nextSamples = samples.Length - sampleIndex - 1;
            sampleIndex += nextSamples;
            samplesPerBand = nextSamples / B;
        }
    }

    // place the actual markers
    private void PlaceMarkers(List<double> instantEnergyList, int N, float C)
    {
        double timePerSample = 1 / (double)soundData.SampleRate;
        int index = N;
        int numInBuffer = index;
        double historyBuffer = 0;

        //Fill the history buffer with n * instant energy
        for (int i = 0; i < index; i++)
        {
            historyBuffer += instantEnergyList[i];
        }

        // If instantEnergy / samples in buffer < instantEnergy for the next sample then add beatmarker.
        while (index + 1 < instantEnergyList.Count)
        {
            if(instantEnergyList[index + 1] > (historyBuffer / numInBuffer) * C)
                beatMarkers.Add((index + 1) * 1024 * timePerSample); 
            historyBuffer -= instantEnergyList[index - numInBuffer];
            historyBuffer += instantEnergyList[index + 1];
            index++;
        }
    }
}

Z jakiegoś powodu wykrywa on tylko uderzenia od 637 sekund do około 641 sekund i nie mam pojęcia, dlaczego. Wiem, że uderzenia są wstawiane z wielu pasm, ponieważ znajduję duplikaty i wydaje się, że przypisuje rytm każdej wartości energii pomiędzy tymi wartościami.

Jest wzorowany na następującym: http://www.flipcode.com/misc/BeatDetectionAlameterms.pdf

Dlaczego więc rytmy nie rejestrują się poprawnie?

Quincy
źródło
2
Czy możesz opublikować wykres ewolucji instantEnergyList [indeks + 1] i historyBuffer w czasie dla jednego zespołu? Dwa wykresy nakładają się na siebie. To dałoby wskazówki na temat problemu. Energia musi być kwadratem wielkości, nie zapominaj o tym.
CeeJay,
Ach tak, to może odsłonić problem, pozwól mi zobaczyć, czy mogę w jakiś sposób zrobić wykresy
Quincy
2
Ale ten wątek to po prostu historyBuffer czy historyBuffer / numInBuffer * C? Wygląda na to, że masz tam ogromne C. Patrząc na kod, historyBuffer powinien mieć podobne wartości do instantEnergy, ten wykres może być tylko wtedy, gdy C jest zbyt wysokie lub numInBuffer jest zbyt niski (znacznie poniżej 1), co chyba nie jest prawdą.
CeeJay
7
Pytanie, które nie umrze ...
Inżynier
3
Spróbuj zadać to pytanie na dsp.stackexchange.com
Atav32

Odpowiedzi:

7

Uderzyłem go, co było głupie, ponieważ nie znałem transformacji Fouriera ani teorii muzyki. Po kilku studiach nie mam rozwiązania, ale widzę kilka niepokojących rzeczy:

  • Brakuje kodu Sound and Buffer i może być łatwo sprawcą
  • Transformacje Fouriera
    • Nie mogłem znaleźć tej samej biblioteki transformacji Fouriera, przeglądając nazwy przestrzeni nazw i metod, co oznacza, że ​​kod może być niestandardowy i może być źródłem problemu
    • Fakt, że FastFourier.Calculate zajmuje szereg skrótów, jest niezwykły
  • Metoda GetEnergyList przyjmuje listę referencyjną, ale ta lista nie jest ponownie używana?
  • W kilku miejscach widać SampleSize zakodowany na 1024, ale nie jest jasne, że tak jest zawsze.
  • Niepokojące jest to, że komentarz do PlaceBeatMarkers zauważa, że ​​N należy podzielić przez 1024, może kod wywołujący zapomniał to zrobić?
  • Jestem bardzo podejrzliwy w stosunku do sposobu manipulacji historyBuffer w PlaceMarkers, zwłaszcza że N jest przekazywane, a następnie wykorzystywane do manipulowania historyBuffer.
  • Komentarz *// Fill the history buffer with n * instant energy*i poniższy kod nie wahają się.

Po chwili poczułem, że kod nie jest naprawdę dobrze zorganizowany i próba naprawy byłaby stratą czasu. Jeśli uważasz, że warto, następnym krokiem jest:

  1. Podziel go na najprostszą część
  2. Przepisz kod w najbardziej szczegółowy sposób, nazwij wszystkie ukryte zmienne
  3. Napisz testy jednostkowe, aby upewnić się, że niewielka część kodu działa poprawnie
  4. Dodaj kolejną małą sekcję kodu i powtarzaj, aż wszystko będzie działać poprawnie

Napiwki

  • Możesz ustawić stałą liczbę pasm, aby uprościć logikę pętli
  • Daj zmiennym, takim jak N, C i B dobre nazwy, które są jasne i zwięzłe, to pomoże ci łatwiej dostrzec błędy logiczne
  • Podziel duże sekcje kodu na kilka nazwanych metod, z których każda wykonuje mały zwięzły krok większego procesu i może mieć napisane testy jednostkowe, aby upewnić się, że działa poprawnie.
Ludington
źródło
Jestem fanem rozwiązywania zagadek kodowych, o ile zagadka jest dobra. Stąd nagroda. Cieszę się, że go wziąłeś, a twoje odpowiedzi na znalezienie błędów w kodzie są najlepszą odpowiedzią na zagadkę kodową.
Seth Battin