Как стать автором
Обновить

Комментарии 13

Я так понимаю, ваша задача состоит в получении длительности ноты из midi-файла? Тогда нормализация рационального числа это наименьшая из всех проблем. Длительности типа 133/400 обычно получаются не потому, что музыка авангардная, а потому, что длительности скорректированы для эффекта стаккато/легато/арпеджио/свинг/ и т.д. Стартовые позиции ноты тоже могут быть смещены в том числе и для эффекта "гуманизации", для создания иллюзии живой игры. Это если не рассматривать вариант, когда midi изначально вживую записывается.

Я так понимаю, ваша задача состоит в получении длительности ноты из midi-файла?

Преобразования любого произвольного отрезка времени из MIDI-формата в человеческий. Длительность нот, в частности. MIDI-файлом задача не ограничивается.

Длительности типа 133/400 обычно получаются не потому, что музыка авангардная, а потому, что длительности скорректированы для эффекта стаккато/легато/арпеджио/свинг/ и т.д. Стартовые позиции ноты тоже могут быть смещены в том числе и для эффекта "гуманизации", для создания иллюзии живой игры.

Это всё, разумеется, имеет место быть, сам много раз занимался гуманизацией программных дорожек. Я скорее про запись живой игры, да. И про специально сделанные "сложности" тоже, если всё-таки про авангард говорить. А ваша мысль дополняет мою, про эти моменты тоже стоило написать, согласен.

Если чуть подробнее, моя библиотека DryWetMIDI предоставляет API для преобразования времени в разные форматы. Например, в описанный в данной статье. У пользователя на руках может быть просто нота или аккорд или отдельное MIDI-событие, или произвольный отрезок в тиках, и он хочет знать, а сколько это в дробном выражении (или метрическом, или в тактах и долях и т.д.). Например, для ноты выглядеть будет так:

var tempoMap = midiFile.GetTempoMap(); // or TempoMap.Create(...) or ...
// ...
var musicalTime = note.TimeAs<MusicalTimeSpan>(tempoMap);
var musicalLength = note.LengthAs<MusicalTimeSpan>(tempoMap);

Тогда нормализация рационального числа это наименьшая из всех проблем.

А проблем, собственно, и нет, статья крохотной получилась. Этот формат самый простой для реализации. Разве что меня занесло с диофантовыми уравнениями в своё время :-)

Я видел вашу библиотеку ещё давно, но мне она показалась слегка переусложнённой. Ну или я не допонял её истинного назначения.

Если будет нужна помощь с ней, обращайтесь. Наоборот, API построен так, чтобы позволять решать сложные задачи в пару строк. Не лишая при этом возможности залезть в дебри, разумеется.

Спасибо, но вопрос с MIDI у меня уже решён. Вы сами-то для каких целей используете свою библиотеку?

На данный момент для получения удовольствия от помощи людям с их проблемами, связанными с MIDI :-) В целом, библиотека давно уже развивается благодаря обращениям пользователей. Изначально большинство нужных функций заложил я, впоследствие идеи приходили от других людей. Рассказывал подробно в статье Из Open Source с любовью.

Посмотрел вашу библиотеку ещё раз. По-прежнему не понимаю, зачем всё оформлять в стилу Linq вместо перегрузки индексатора, чтобы иметь возможность писать типа

track[(SamplePosition)10000] = new NoteOn("ми6");

или

track[(TimePosition)"0:0:10"] = new Chord("Am", Duration.ToNextChord, ChordStyle.Guitar);

Видимо, дело вкуса. Но суть не в этом. Ваша реализация приближения действительного числа рациональным содержит ошибку, в результате чего итератор может входить в бесконечный цикл, например при значениях number=0.77777777 и eps = 0.0000001. Кроме того, особенно в контексте задачи, точность приближения удобнее ограничивать значением знаменателя. Кроме того, в контексте задачи, имеет смысл выделять дроби 3/2, 7/4, 15/8 ... , которые в стандартной нотной записи соответствуют точкам справа от ноты.

Видимо, дело вкуса.

Пусть будет дело вкуса.

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

var musicalTimeSpan = MusicalTimeSpan.FromDouble(
    0.77777777,
    new DoubleToMusicalTimeSpanSettings
    {
        FractionalPartEpsilon = 0.0000001
    });

За пару миллисекунд получил ответ 7/9. Никакого бесконечного цикла быть тут не может. Если вы посмотрите в настройки, там, помимо эпсилона на дробную часть и ограничения по точности приближения, есть ещё ограничение по количеству итераций. Это тот рубильник, который и призван спасти от каких-либо циклов и зависаний. Всё же лучше сперва проверять перед тем, как "находить ошибку" ;-)

Ну так я и проверил копированием и выполнением части кода с заявленной функциональностью. Ограничение количества итераций, да ещё и где-то снаружи - это (на мой взгляд) очевидный костыль, поскольку количество приближений должно быть ограничено по определению. Но не воспринимайте это за критику, пожалуйста.

Покажите, пожалуйста, ваш код, как вы проверяли. Если вы говорите про приближение действительного числа рациональным, речь же про метод FromDouble? Если так, то там невозможно получить зависание, как я уже упомянул выше. Более того, вам не нужно ничего делать с количеством итераций, просто не указывайте и всё будет работать с настройками по умолчанию :-)

Я к багам отношусь всегда серьёзно, поэтому, если ошибка есть, буду рад исправить. Но для этого мне нужно знать, как её поймать.

    class Program
    {
        static void Main(string[] args)
        {
            double num = 0.77777777;
            double eps = 0.0000001;
            foreach (var z in GetRationalizations(num, eps))
            {
                Console.WriteLine(z);
            }
        }

        public static IEnumerable<RationalNumber> GetRationalizations(double number, double epsilon)
        {
            var intPart = (int)Math.Floor(number);
            yield return new RationalNumber(intPart, 1);

            var fractionPart = number - intPart;
            if (fractionPart < epsilon) yield break;

            foreach (var rationalNumber in GetRationalizations(1 / fractionPart, epsilon))
            {
                yield return new RationalNumber(rationalNumber.Denominator + intPart * rationalNumber.Numerator, rationalNumber.Numerator);
            }
        }
    }

Даже при искусственном ограничении итераций после первых 6-и приближений идёт мусор, включая отрицательные значения.

Теперь стало понятно, вы про внутренний метод в библиотеке. Да, тут коррекции возможны. Но публичный метод, который доступен пользователям, использует этот код с доработками, поэтому зависаний не будет.

Так в этом и ценность открытого кода - что помимо публичных методов пользователю доступна и реализация, которую можно изучать, корректировать, и использовать в личных и немного других целях.

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

Программисту параметр epsilon не нужен - в том числе и потому, что совершенно непонятно, из каких критериев его выбирать. Даже интуитивно достаточного в половину точности оказалось недостаточно, ну а сам факт возможности бесконечного цикла категорически недопустим. Мой самый любимый нотный редактор на некоторых миди-шках крашится - откуда мне знать, может именно поэтому?

Правильное решение здесь - отказаться от внешнего параметра epsilon в принципе. После каждого нового полученного приближения нужно получить double взад делением числителя на знаменатель и посчитать abs разницы с оригинальным числом. Как только этот abs перестал уменьшаться (по сравнению с предыдуще посчитанным) - пора выходить из цикла. Только так можно гарантировать корректность перечисления с учётом всех погрешностей вычислений.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории