¿Por qué este código de detección de latido no puede registrar algunos latidos correctamente?

38

Hice esta clase de SoundAnalyzer para detectar ritmos en canciones:

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 alguna razón, solo detecta latidos de 637 segundos a alrededor de 641 segundos, y no tengo idea de por qué. Sé que los latidos se están insertando desde múltiples bandas ya que estoy encontrando duplicados, y parece que está asignando un latido a cada valor de energía instantánea entre esos valores.

Está modelado después de esto: http://www.flipcode.com/misc/BeatDetectionAlgorithms.pdf

Entonces, ¿por qué los ritmos no se registran correctamente?

Quincy
fuente
2
¿Puedes publicar una trama de la evolución de instantEnergyList [index + 1] e historyBuffer a lo largo del tiempo para una banda? Los dos gráficos se superponen uno encima del otro. Eso daría pistas sobre cuál podría ser el problema. Además, la energía debe ser el cuadrado de la magnitud, no lo olvides.
CeeJay
Ah, sí, eso podría revelar el problema, déjame ver si de alguna manera puedo hacer algunos gráficos
Quincy
2
Pero este argumento es solo historyBuffer, o historyBuffer / numInBuffer * C? Parece que tienes una C masiva allí. Mirando el código, historyBuffer debería tener valores similares a instantEnergy, ese gráfico solo puede ser si C es demasiado alto o numInBuffer es demasiado bajo (muy por debajo de 1), lo que supongo que no es el caso.
CeeJay
77
La pregunta que no moriría ...
Ingeniero
3
Intente hacer esta pregunta en dsp.stackexchange.com
Atav32

Respuestas:

7

Le di una puñalada, lo cual fue tonto porque no estaba familiarizado con las transformadas de Fourier o la teoría de la música. Entonces, después de algunos estudios, no tengo una solución, pero veo varias cosas problemáticas:

  • Falta el código para Sound and Soundbuffer y podría ser fácilmente el culpable
  • Las transformadas de Fourier
    • No pude encontrar la misma biblioteca de transformadas de Fourier buscando en Google el espacio de nombres y los nombres de los métodos, lo que significa que el código podría ser personalizado y podría ser la fuente del problema
    • El hecho de que FastFourier.Calculate tome una serie de cortos es inusual
  • El método GetEnergyList toma una lista de referencia, pero esta lista no se usa de nuevo.
  • En varios puntos, puede ver el SampleSize codificado en 1024, pero no está claro que siempre sea así.
  • Es preocupante que el comentario de PlaceBeatMarkers señala que N debe dividirse por 1024, ¿tal vez el código de llamada olvidó hacer eso?
  • Sospecho mucho de cómo se manipula historyBuffer en PlaceMarkers, especialmente porque N se pasa y luego se utiliza para manipular historyBuffer.
  • El comentario *// Fill the history buffer with n * instant energy*y el código que sigue no coinciden.

Después de un tiempo, tuve la sensación de que el código no está realmente bien organizado y sería una pérdida de tiempo intentar solucionarlo. Si crees que vale la pena, el siguiente paso que tomaría es:

  1. Divídalo en la parte más simple
  2. Reescribe el código de la manera más detallada, nombra todas las variables ocultas
  3. Escriba pruebas unitarias para asegurarse de que una pequeña parte del código funcione correctamente
  4. Agregue otra pequeña sección de código y repita hasta que todo funcione correctamente

Consejos

  • Es posible que desee establecer el número de bandas para simplificar la lógica del bucle
  • Dé a las variables como N, C y B buenos nombres que sean claros y concisos, esto le ayudará a ver los errores lógicos más fácilmente
  • Divida secciones grandes de código en varios métodos llamados, cada uno de los cuales realiza un pequeño paso conciso del proceso más grande y puede tener pruebas unitarias escritas para asegurarse de que funciona correctamente.
Ludington
fuente
Soy fanático de resolver acertijos de código, siempre que el acertijo sea bueno. De ahí la generosidad. Me alegra que lo hayas tomado, y tus respuestas para encontrar errores en el código son la mejor respuesta que podría obtener un acertijo de código.
Seth Battin