Как получить реальное время миди-события, используя Naudio - PullRequest
0 голосов
/ 01 апреля 2020

Я пытаюсь читать миди-заметки и извлекать реальное время каждого из них, используя библиотеку NAudio. Я написал этот код, но он не рассчитывает время правильно, я использовал формулу, которую нашел здесь ((note.AbsTime - lastTempoEvent.AbsTime) / midi.ticksPerQuarterNote) * tempo + lastTempoEvent.RealTime

Код:

    var strictMode = false;
    var mf = new MidiFile("Assets/Audios/Evangelion Midi.mid", strictMode);
    mf.Events.MidiFileType = 0;
    List<MidiEvent> midiNotes = new List<MidiEvent>();
    List<TempoEvent> tempoEvents = new List<TempoEvent>();


    for (int n = 0; n < mf.Tracks; n++)
    {
        foreach (var midiEvent in mf.Events[n])
        {
            if (!MidiEvent.IsNoteOff(midiEvent))
            {
                midiNotes.Add(midiEvent);

                TempoEvent tempoE;
                try { tempoE = (TempoEvent)midiEvent; tempoEvents.Add(tempoE);

                    Debug.Log("Absolute Time " + tempoE.AbsoluteTime);

                }
                catch { }


            }
        }
    }

    notesArray = midiNotes.ToArray();
    tempoEventsArr = tempoEvents.ToArray();

    eventsTimesArr = new float[notesArray.Length];
    eventsTimesArr[0] = 0;

    for (int i = 1; i < notesArray.Length; i++)
    {
        ((notesArray[i].AbsoluteTime - tempoEventsArr[tempoEventsArr.Length - 1].AbsoluteTime) / mf.DeltaTicksPerQuarterNote)
            * tempoEventsArr[tempoEventsArr.Length - 1].MicrosecondsPerQuarterNote + eventsTimesArr[i-1];


    }

Я получил эти значения , которые явно не верны

Кто-нибудь, где я не прав?

1 Ответ

1 голос
/ 01 апреля 2020

Очень приятно видеть кого-то здесь в MIDI.

Часть note.AbsTime - lastTempoEvent.AbsTime в ссылочном коде неправильно реализована на вашей стороне.

Переменная lastTempoEvent в этом коде не может означать последнее изменение темпа в миди-файле (как вы реализовали его, используя notesArray[i].AbsoluteTime - tempoEventsArr[tempoEventsArr.Length - 1].AbsoluteTime).

То, что пытается сделать ссылочный код, - это получить темп во время текущего note, (возможно, путем сохранения последнего появившегося события изменения темпа в этой переменной), пока ваш код вычитает абсолютное время последнего изменения темпа во всем миди-файле. Это причина root отрицательных чисел (если есть какие-либо изменения темпа после текущей ноты).

Примечание: я также рекомендую сохранять время событий примечаний. Как закрыть заметку, если вы не знаете, когда она будет выпущена? Попробуй это. Я проверил это, и это работает. Пожалуйста, внимательно прочитайте встроенные комментарии.

Будьте осторожны.

static void CalculateMidiRealTimes()
{
    var strictMode = false;
    var mf = new MidiFile("C:\\Windows\\Media\\onestop.mid", strictMode);
    mf.Events.MidiFileType = 0;

    // Have just one collection for both non-note-off and tempo change events
    List<MidiEvent> midiEvents = new List<MidiEvent>();

    for (int n = 0; n < mf.Tracks; n++)
    {
        foreach (var midiEvent in mf.Events[n])
        {
            if (!MidiEvent.IsNoteOff(midiEvent))
            {
                midiEvents.Add(midiEvent);

                // Instead of causing stack unwinding with try/catch,
                // we just test if the event is of type TempoEvent
                if (midiEvent is TempoEvent)
                {
                    Debug.Write("Absolute Time " + (midiEvent as TempoEvent).AbsoluteTime);
                }
            }
        }
    }

    // Now we have only one collection of both non-note-off and tempo events
    // so we cannot be sure of the size of the time values array.
    // Just employ a List<float>
    List<float> eventsTimesArr = new List<float>();

    // we introduce this variable to keep track of the tempo changes
    // during play, which affects the timing of all the notes coming
    // after it.
    TempoEvent lastTempoChange = null;

    for (int i = 0; i < midiEvents.Count; i++)
    {
        MidiEvent midiEvent = midiEvents[i];
        TempoEvent tempoEvent = midiEvent as TempoEvent;

        if (tempoEvent != null)
        {
            lastTempoChange = tempoEvent;
            // Remove the tempo event to make events and timings match - index-wise
            // Do not add to the eventTimes
            midiEvents.RemoveAt(i);
            i--;
            continue;
        }

        if (lastTempoChange == null)
        {
            // If we haven't come accross a tempo change yet,
            // set the time to zero.
            eventsTimesArr.Add(0);
            continue;
        }

        // This is the correct formula for calculating the real time of the event
        // in microseconds:
        var realTimeValue =
            ((midiEvent.AbsoluteTime - lastTempoChange.AbsoluteTime) / mf.DeltaTicksPerQuarterNote)
            *
            lastTempoChange.MicrosecondsPerQuarterNote + eventsTimesArr[eventsTimesArr.Count - 1];

        // Add the time to the collection.
        eventsTimesArr.Add(realTimeValue);

        Debug.WriteLine("Time for {0} is: {1}", midiEvents.ToString(), realTimeValue);
    }

}

РЕДАКТИРОВАТЬ:

Деление при расчете реального времени было int / float, которое приводило к нулю, когда тики между событиями меньше, чем дельта-тики на квартальную ноту.

Вот правильный способ вычисления значений с использованием десятичного числа типа цифра c, который имеет наилучшую точность.

Midi песня onestop.mid длительностью 4:08 (248 секунд), а наше последнее событие в реальном времени - 247,3594906770833

static void CalculateMidiRealTimes()
{
    var strictMode = false;
    var mf = new MidiFile("C:\\Windows\\Media\\onestop.mid", strictMode);
    mf.Events.MidiFileType = 0;

    // Have just one collection for both non-note-off and tempo change events
    List<MidiEvent> midiEvents = new List<MidiEvent>();

    for (int n = 0; n < mf.Tracks; n++)
    {
        foreach (var midiEvent in mf.Events[n])
        {
            if (!MidiEvent.IsNoteOff(midiEvent))
            {
                midiEvents.Add(midiEvent);

                // Instead of causing stack unwinding with try/catch,
                // we just test if the event is of type TempoEvent
                if (midiEvent is TempoEvent)
                {
                    Debug.Write("Absolute Time " + (midiEvent as TempoEvent).AbsoluteTime);
                }
            }
        }
    }

    // Switch to decimal from float.
    // decimal has 28-29 digits percision
    // while float has only 6-9
    // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types

    // Now we have only one collection of both non-note-off and tempo events
    // so we cannot be sure of the size of the time values array.
    // Just employ a List<float>
    List<decimal> eventsTimesArr = new List<decimal>();

    // Keep track of the last absolute time and last real time because
    // tempo events also can occur "between" events
    // which can cause incorrect times when calculated using AbsoluteTime
    decimal lastRealTime = 0m;
    decimal lastAbsoluteTime = 0m;

    // instead of keeping the tempo event itself, and
    // instead of multiplying every time, just keep
    // the current value for microseconds per tick
    decimal currentMicroSecondsPerTick = 0m;

    for (int i = 0; i < midiEvents.Count; i++)
    {
        MidiEvent midiEvent = midiEvents[i];
        TempoEvent tempoEvent = midiEvent as TempoEvent;

        // Just append to last real time the microseconds passed
        // since the last event (DeltaTime * MicroSecondsPerTick
        if (midiEvent.AbsoluteTime > lastAbsoluteTime)
        {
            lastRealTime += ((decimal)midiEvent.AbsoluteTime - lastAbsoluteTime) * currentMicroSecondsPerTick;
        }

        lastAbsoluteTime = midiEvent.AbsoluteTime;

        if (tempoEvent != null)
        {
            // Recalculate microseconds per tick
            currentMicroSecondsPerTick = (decimal)tempoEvent.MicrosecondsPerQuarterNote / (decimal)mf.DeltaTicksPerQuarterNote;

            // Remove the tempo event to make events and timings match - index-wise
            // Do not add to the eventTimes
            midiEvents.RemoveAt(i);
            i--;
            continue;
        }

        // Add the time to the collection.
        eventsTimesArr.Add(lastRealTime);

        Debug.WriteLine("Time for {0} is: {1}", midiEvent, lastRealTime / 1000000m);
    }
} 
...