Por que esse código de detecção de batidas está falhando ao registrar algumas batidas corretamente?

38

Eu criei essa classe SoundAnalyzer para detectar batidas nas músicas:

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++;
        }
    }
}

Por alguma razão, ele só detecta batimentos de 637 segundos a cerca de 641 segundos, e não faço ideia do porquê. Eu sei que as batidas estão sendo inseridas de várias bandas desde que eu estou encontrando duplicatas, e parece que está atribuindo uma batida a cada valor instantâneo de energia entre esses valores.

É modelado após isso: http://www.flipcode.com/misc/BeatDetectionAlgorithms.pdf

Então, por que as batidas não são registradas corretamente?

Quincy
fonte
2
Você pode postar um gráfico da evolução do instantEnergyList [index + 1] e historyBuffer ao longo do tempo para uma banda? Os dois gráficos se sobrepunham. Isso daria pistas sobre qual poderia ser o problema. Além disso, a energia deve ser o quadrado da magnitude, não se esqueça disso.
Ceejay
Ahh sim que pode desvendar o problema, deixe-me ver se consigo alguma forma fazer alguns gráficos
Quincy
2
Mas esse gráfico é apenas historyBuffer, ou historyBuffer / numInBuffer * C? Parece que você tem um C enorme lá. Olhando para o código, historyBuffer deve ter valores semelhantes ao instantEnergy, esse gráfico só pode ser se C for muito alto ou numInBuffer for muito baixo (muito abaixo de 1), o que eu acho que não é o caso.
Ceejay
7
A pergunta que não iria morrer ...
Engenheiro
3
Tente fazer esta pergunta em dsp.stackexchange.com
Atav32 20/12/12

Respostas:

7

Eu dei uma facada nele, o que foi idiota porque eu não estava familiarizado com transformações de Fourier ou teoria musical. Então, depois de algum estudo, não tenho solução, mas vejo várias coisas preocupantes:

  • O código do Sound and Soundbuffer está ausente e pode ser facilmente o culpado
  • As transformações de Fourier
    • Não consegui encontrar a mesma biblioteca de transformadas de Fourier pesquisando no namespace e nos nomes dos métodos, o que significa que o código pode ser personalizado e pode ser a fonte do problema
    • O fato de FastFourier.Calculate levar uma série de curtos é incomum
  • O método GetEnergyList usa uma lista ref, mas essa lista não é usada novamente?
  • Em vários pontos, você vê o SampleSize codificado para 1024, mas não está claro que sempre seja o caso.
  • É preocupante que o comentário do PlaceBeatMarkers observe que N deve ser dividido por 1024, talvez o código de chamada tenha esquecido de fazer isso?
  • Suspeito muito da maneira como o historyBuffer é manipulado no PlaceMarkers, especialmente porque N é passado e usado para manipular o historyBuffer.
  • O comentário *// Fill the history buffer with n * instant energy*e o código a seguir não são exibidos.

Depois de um tempo, tive a sensação de que o código não está realmente bem organizado e seria uma perda de tempo tentando consertar. Se você acha que vale a pena, o próximo passo que daria é:

  1. Divida-o na parte mais simples
  2. Reescreva o código da maneira mais detalhada, nomeie todas as variáveis ​​ocultas
  3. Escreva testes de unidade para garantir que pequena parte do código funcione corretamente
  4. Adicione outra pequena seção de código e repita até que tudo funcione corretamente

Dicas

  • Você pode querer fixar o número de bandas para simplificar a lógica do loop
  • Dê bons nomes a variáveis ​​como N, C e B que sejam claros e concisos, isso ajudará você a ver erros lógicos com mais facilidade
  • Divida grandes seções de código em vários métodos chamados, cada um executando uma pequena etapa concisa do processo maior e pode ter testes de unidade escritos para garantir que esteja funcionando corretamente.
Ludington
fonte
Sou fã de resolver enigmas de código, desde que o enigma seja bom. Daí a recompensa. Fico feliz que você tenha aceitado e suas respostas para encontrar erros no código são a melhor resposta que um enigma do código pode obter.
Seth Battin