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

Пользователь

Отправить сообщение

Да, выше о нем уже упомянули. Постараюсь посмотреть исходники когда будет время.

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

Интересный проект, хотя мне не очень понятно как его можно использовать. Слишком уж сложно заставить микроконтроллер делать что-то еще помимо воспроизведения музыки.
К тому же, формат MOD слишком тяжелый для маленьких микроконтроллеров, хоть и намного более универсальный.
А вот если бы его на Arduino портировать, то могло бы быть вполне годно. Там и 32кб флеша и 16МГц кварц из коробки.

Я не уверен, что правильно понял вопрос. Скорее всего, вы имеете в виду что-то типа:


outSample = instrumentSample — (instrumentSample >> n);

Так не получится, так как:


  1. При сдвиге вправо значение уменьшается в 2 раза, а мы можем хотеть иметь громкость, например, 87%.
  2. Если упростить, то один сдвиг соответствует умножению на число, у которого только один бит равен 1, а остальные 0. Если нужно иметь возможность вычислять сэмпл для любого значения громкости, то надо будет делать до 8 сдвигов и мы получим то же умножение, только медленное.
  3. И вообще, в AVR нет операции сдвига на произвольное количество битов. Можно только на 1 бит влево или на 1 бит вправо.
  4. Запоминать уменьшеное значение семпла для уменьшения его позже тоже не получится. Wave table находится в програмной памяти и доступна только для чтения. И ее нужно было бы пересчитывать всю. И хранить отдельно для каждого канала.

Одну мелодию, там где много инструментов, я брал из своего старого проекта. Помню, что накидал данные в csv (строки — такты, столбцы — команды, подобно трекерной музыке) и потом перегнал в команды простым самописным конвертором. Конвертор уже где-то потерялся, но его не особо сложно написать заново.
Вторую мелодию позаимствовал из проэкта музкальной шкатулки, в ссылках есть. Опять же, наколеночный конвертор, показывать стыдно.
Коротенькие уведомления писал руками и тестировал в wavepot.


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

Проект на гитхабе, можете попробовать померять.
Укажите 3 канала вместо 5 и можно включить интерполяцию. Или использовать Arduino на 16МГц как самый простой вариант.


Как мне кажется, смысла хранить по таблице для каждой ноты нет. Можно попробовать хранить по таблице на октаву. В каждой следующей уменьшать количество гармоник вдвое путем выбрасывания половины сэмплов а потом интерполировать обратно до 256 сэмплов. Это как самый простой вариант.

Использую одну таблицу. Но можно сделать и несколько с той же производительностью. Когда начинает играть следующая нота, то вызывается функция инструмента и там можно выбрать необходимый массив в зависимости от заданой ноты.


Интерполяцию значений между семплами можно включить (директива INTERPOLATE_WAVE), но производительность падает. Решил просто использовать более длинную таблицу в 256 значений, а интерполяцию провести перед помещением данных в таблицу.

О такой возможности я писал во вступлении.

Картинка не вставилась.
Но идея хороша, можете попробовать :)
Надо написать драйвер (каталог microsound/devices) и там просто выводить значение в порт.

Решение со щелчками на поверхности, я его даже реализовывал, но оно все-таки просаживало производительность. В какой-то момент я делал рефакторинг кода и оно мешало, убрал. Обратно запиливать не стал. В планах все-таки попробовать плавно уменьшать громкость перед следующей нотой, но там пока не очень очевидно как это сделать хорошо.


По поводу PWM — да, проблема есть. Но в коде именно за вывод звука отвечает драйвер (каталог microsound/devices), можно что-то другое подключить. Covox, например, как советуют ниже.

Спасибо за комментарий. Возможно, стоит как-то переименовать заголовок, чтобы не смущать тех, кто в теме. Я старался упростить для тех, кто не в теме, и описал только то, что используется.

Из вики:


Wavetable synthesis is fundamentally based on periodic reproduction of an arbitrary, single-cycle waveform.

По сути, тут периодическая функция — это не совсем функция, а как раз таблица значений амплитуды, 256 значений на период. А еще она работает не сама по себе, а в паре с функцией громкости от времени, которая тоже яавляется таблицей значений.
Возможно, существует более подходящий термин, но я его не знаю.

Его типичная реализация не стабильна, но может быть таковой сделана (см. Здесь).

Так себе решение. Для достижения стабильности потребуется O(n) памяти и еще больше просадит производительность. Скорее всего получится хуже по всем параметрам, чем merge sort.
Идеального алгоритмя до сих пор нет (гарантированая сложность O(n * log n), константа (или хотя бы логарифм) по памяти, стабильность).

Как мне кажется, для таких случаев стоит использовать какой-то параметр уверености. Например, рассматривать 2 наиболее вероятных варианта. И не парсить слово, если у них ошибки достаточно близки.
Но тут, конечно, от задачи зависит. Где-то может быть нужно распарсить любой ценой, а где-то неправильный или неоднозначный парсинг недопустим.

Идея интересная. У меня получился результат примерно на уровне моего последнего решения. Но в тут таки можно использовать память по полной и поместить 21 значения в long.
Из минусов такого подхода — надо подбирать размер массива для конкретного железа. У меня получился лучший результат при 2^12 интов, по 4 трехбитных слова. При использовании 2^9 или 2^15 производительность становится хуже. Так же она становится хуже при использовании массива байтов вместо инта. На Вашем железе может быть по-другому.
Алгоритм простой, сначала посчитать разность (см. предыдуйие код, 2 первые строчки), а потом посуммировать из массива. Если захотите проверить, то разберетесь как это сделать :)
Ну и я бы рекомендовал все-таки проверить мои 2 последних решения на Ваших данных и Вашем железе. Даже если по скорости он окажется таким же, то по памяти будет выигрыш. И мне тоже хотелось бы узнать результат :)

Ваша задача не отпускает :)
Обнаружил, что джава считает Long.bitCount очень быстро, по крайней мере на моей системе. Скорее всего задействует SSE инструкции или типа того.
А в связи с этим открываются новые возможности по оптимизации:


    public static long sumSqDiff(long l1, long l2) {
        long sameSign100 = (~(l1 ^ l2)) & BITS_100;

        // sum = l1 - l2
        long sum = ((l1 | BITS_100) - (l2 & BITS_011)) ^ sameSign100;

        // correct estimate: add cases for values +/-3
        return Long.bitCount(((sum ^ (sum << 1)) & BITS_100) & (sum << 2)) * 8 + estimate(l1, l2);
    }

    public static long estimate(long l1, long l2) {
        long xor = l1 ^ l2;
        long ones = xor & BITS_001;
        long res = Long.bitCount(ones);
        xor &= ~(ones * 7);
        long twos = xor & BITS_010;
        res += Long.bitCount(twos) * 4;
        xor &= ~(twos * 3);
        res += Long.bitCount(xor) * 16;
        return res;
    }

Это пока самый быстрый код, который я смог получить на своей системе. Не факт что на Вашей системе будет так же.
Метод estimate медленнее, чем у Вас, но более точный. Из плюсов, можно грубо посчитать грубую оценку, а потом при необходимости досчитать. Хотя в Вашем случае выигрыш от такого подхода будет совсем небольшой.

Я понял.
Переписал Ваш код на джаву и проверил производительность с помощью JMH.
Реализация с циклом примерно в 2 раза медленнее моей 4хбитной реализации. Трехбитная реализация была медленнее, но у меня уже есть решение с оптимизацией. Новое решение даже чуть обгоняет четырехбитное (правда, не понимаю почему), но в нем на 25% больше данных, так что в итоге должно быть быстрее и с меньшим потреблением памяти. Грубый фильтр примерно в 6 раз быстрее моей самой быстрой реализации (но это в пересчете на один long а не на количество реальных данных в нем).
Вот результаты тестов:


Benchmark            Mode  Cnt        Score       Error  Units
Jmh.bits3new        thrpt    5   171994.412 ±   925.414  ops/s
Jmh.bits3old        thrpt    5   117730.344 ± 10353.670  ops/s
Jmh.bits4           thrpt    5   163883.382 ±  2206.193  ops/s
Jmh.ref             thrpt    5    77606.405 ±   968.108  ops/s
Jmh.refEstimate     thrpt    5  1027086.177 ± 23775.351  ops/s

Ну и оптимизированый код:


    public static final long BITS_001 = 0b001001001001001001001001001001001001001001001001001001001001001L;
    public static final long BITS_010 = BITS_001 << 1;
    public static final long BITS_100 = BITS_001 << 2;
    public static final long BITS_011 = BITS_010 | BITS_001;

    public static final long BITS_GROUP_0N =0b000111000111000111000111000111000111000111000111000111000111000L;
    public static final long BITS_GROUP_0 = 0b111000111000111000111000111000111000111000111000111000111000111L;
    public static final long BITS_GROUP_1 = 0b111000000111111000000111111000000111111000000111111000000111111L;
    public static final long BITS_MUL     = 0b000000000000001000000000001000000000001000000000001000000000001L;

    public static long sumSqDiff(long l1, long l2) {
        // Calculate same sign places
        long sameSign100 = (~(l1 ^ l2)) & BITS_100;

        // sum = l1 - l2
        long sum = ((l1 | BITS_100) - (l2 & BITS_011)) ^ sameSign100;

        // sum = abs(sum)
        long sumNeg001 = (sum & BITS_100) >>> 2;
        sum = (sum ^ (sumNeg001 * 7)) + sumNeg001;

        // Calculate separate bits, prepare for multiplication
        long b0 = (sum & BITS_001) * 3; // Bit 0b001 -> 0b011
        long b1 = (sum & BITS_010) * 3; // Bit 0b010 -> 0b110

        // mul011 = l1 * l1 (low 2 bits only)
        long sumPart1 = sum & BITS_GROUP_0;
        long sumPart2 = sum & BITS_GROUP_0N;

        long res = ((sumPart1 << 1) & b1) + (sumPart1 & b0) +
                ((((sumPart2 << 1) & b1) + (sumPart2 & b0)) >>> 3);

        // Calculate special case for value 4 = 0b100
        long b2 = (sum & BITS_100) << 2;
        res += (b2 + (b2 >>> 3)) & BITS_GROUP_0N;

        // Combine alltogether
        res = (res & BITS_GROUP_1) + ((res >>> 6) & BITS_GROUP_1);
        res = (res * BITS_MUL) >>> 48;

        return res & 0xFFF;
    }

Статью писать я вряд ли буду, я не так много могу рассказать по этому поводу. Но могу порекомендовать отличную книгу по теме, называется Hacker's Delight или в русском переводе Алгоритмические трюки для программистов.

Странно, что нет повышения производительности. Возможно, что узкое место где-то не здесь. Попробуйте сравнить два (или три) решения без применения предварительного фильтра.
Я еще немного подчистил код для трехбитной реализации. Улучшение производительности в рамках погрешности, хотя вроде и есть.


    public static long sumSqDiff(long l1, long l2) {
        // l2 = -l2, inverse L2 to use sum instead of substraction
        long l2NotZero001 = (l2 | (l2 >>> 1)) & BITS_001;
        long l2Neg111 = (l2NotZero001 << 2) | (l2NotZero001 << 1) | l2NotZero001;
        l2 = (l2 ^ l2Neg111) + l2NotZero001;

        // Calculate where only one negative value is present (flip highest bit)
        long oneNeg100 = (l1 ^ l2) & BITS_100;

        // sum = l1 + l2
        long sum = ((l1 & BITS_011) + (l2 & BITS_011)) ^ oneNeg100;

        // sum = abs(sum)
        long sumNeg100 = sum & BITS_100;
        long sumNeg001 = sumNeg100 >>> 2;
        long sumNeg111 = sumNeg100 | (sumNeg001 << 1) | sumNeg001;
        sum = (sum ^ sumNeg111) + sumNeg001;

        // Calculate separate bits, prepare for multiplication
        long b0 = sum & BITS_001; // Bit 0b001
        b0 = b0 | (b0 << 1); // Bit 0b001 -> 0b011
        long b1 = sum & BITS_010; // Bit 0b010
        b1 = b1 | (b1 >>> 1); // Bit 0b010 -> 0b011
        long b2 = (sum & BITS_100) >>> 2; // Bit 0b100 -> 0b001

        // mul011 = l1 * l1 (low 2 bits only)
        long sumPart1 = sum & BITS_GROUP_0;
        long sumPart2 = sum & BITS_GROUP_0N;

        long res = ((sumPart1 & b1) << 1) + (sumPart1 & b0) +
                ((((sumPart2 & b1) << 1) + (sumPart2 & b0)) >>> 3);

        // Calculate special case for value 4 = 0b100
        b2 = (b2 + (b2 >>> 3)) & BITS_GROUP_0;

        // Combine alltogether
        res += b2 << 4;
        res = (res & BITS_GROUP_1) + ((res >>> 6) & BITS_GROUP_1);
        res = (res + (res >>> 12));
        res = (res + (res >>> 24));
        res = (res + (res >>> 48));

        return res & 0xFFF;
    }

Не сомневаюсь, что ради этого и писалась статья :)
И, раз уж Вы так хотели уложить данные в 3 бита, то это тоже можно сделать. Правда, только 20 штук в long, а не 21, если без костылей просаживающих производительность.
Значения такие же как и в предыдущем примере, [0b110, 0b111, 0b000, 0b001, 0b010]. И все так же без веток и циклов:


    public static final long BITS_001 = 0b001001001001001001001001001001001001001001001001001001001001001L;
    public static final long BITS_010 = BITS_001 << 1;
    public static final long BITS_100 = BITS_001 << 2;
    public static final long BITS_011 = BITS_010 | BITS_001;

    public static final long BITS_GROUP_0N =0b000111000111000111000111000111000111000111000111000111000111000L;
    public static final long BITS_GROUP_0 = 0b111000111000111000111000111000111000111000111000111000111000111L;
    public static final long BITS_GROUP_1 = 0b111000000111111000000111111000000111111000000111111000000111111L;
    public static final long BITS_GROUP_2 = 0b000111111111111000000000000111111111111000000000000111111111111L;

    public static long sumSqDiff(long l1, long l2) {

        // l2 = -l2, inverse L2 to use sum instead of substraction
        long l2Neg001 = (l2 | (l2 >>> 1) | (l2 >>> 2)) & BITS_001;
        long l2Neg111 = (l2Neg001 << 2) | (l2Neg001 << 1) | l2Neg001;
        l2 = (l2 ^ l2Neg111) + l2Neg001;

        // Calculate where substraction is needed (flip highest bit)
        long oneNeg100 = (l1 ^ l2) & BITS_100;

        // sum = l1 + l2
        long sum = ((l1 & BITS_011) + (l2 & BITS_011)) ^ oneNeg100;

        // sum = abs(sum)
        long sumNeg001 = (sum & BITS_100) >>> 2;
        long sumNeg111 = (sumNeg001 << 2) | (sumNeg001 << 1) | sumNeg001;
        sum = (sum ^ sumNeg111) + sumNeg001;

        // Calculate separate bits, prepare for multiplication
        long b0 = sum & BITS_001; // Bit 0b001
        b0 = b0 | (b0 << 1); // Bit 0b001 -> 0b011
        long b1 = sum & BITS_010; // Bit 0b010
        b1 = b1 | (b1 >>> 1); // Bit 0b010 -> 0b011
        long b2 = (sum & BITS_100) >>> 2; // Bit 0b100 -> 0b001

        // mul011 = l1 * l1 (low 2 bits only)
        long sumPart1 = sum & BITS_GROUP_0;
        long sumPart2 = sum & BITS_GROUP_0N;

        long res = ((sumPart1 & b1) << 1) + (sumPart1 & b0) +
                ((((sumPart2 & b1) << 1) + (sumPart2 & b0)) >>> 3);

        // Calculate special case for value 4 = 0b100
        b2 = (b2 + (b2 >>> 3)) & BITS_GROUP_0;
        b2 = (b2 + (b2 >>> 6)) & BITS_GROUP_1;

        // Combine alltogether
        res = (res + (res >>> 6)) & BITS_GROUP_1;
        res += b2 << 4;
        res = (res + (res >>> 12)) & BITS_GROUP_2;
        res = (res + (res >>> 24));
        res = (res + (res >>> 48));

        return res & 0xFFF;
    }

Не знаю как будет по скорости. Наверно можно еще что-то соптимизировать, но я пока пас. Скорее всего примерно так же, как и в предыдущем примере. Но памяти меньше.
Буду ждать от Вас результатов тестов.

Можно сумму квадратов разности посчитать без циклов и ветвлений. Скорее всего будет быстрее, чем в приведенном примере.
В данном случае для значений [-2..2] я использовал по 4 бита на число: [0b0110, 0b0111, 0b0000, 0b0001, 0b0010].
Идея использовать побитовую магию для паралельного подсчета всех полубайтов лонга одновременно.
Код на джаве:


    public static long sumSqDiff(long l1, long l2) {
         // l1 = l1 - l2
        l1 |= 0x8888888888888888L;
        l1 -= l2;

        // l2 = negatives
        l2 = (l1 & 0x4444444444444444L) >>> 2;
        // l1 = abs(l1)
        l1 = (l1 ^ (l2 | (l2 << 1) | (l2 << 2))) + l2;

        // Calculate separate bits, prepare for multiplication
        long b0 = l1 & 0x1111111111111111L; // Bit 0b0001
        b0 = b0 | (b0 << 1); // Bit 0b0001 -> 0b0011
        long b1 = l1 & 0x2222222222222222L; // Bit 0b0010
        b1 = b1 | (b1 >>> 1); // Bit 0b0010 -> 0b0011
        long b2 = (l1 & 0x4444444444444444L) >>> 2; // Bit 0b0100 -> 0b0001

        // l1 = l1 * l1 (low 2 bits only)
        l1 = ((l1 & b1) << 1) + (l1 & b0);

        // Calculate special case for value 4 0b0100
        b2 = (b2 + (b2 >>> 4)) & 0x0F0F0F0F0F0F0F0FL;

        // Combine alltogether
        l1 = (l1 & 0x0F0F0F0F0F0F0F0FL) + ((l1 >>> 4) & 0x0F0F0F0F0F0F0F0FL) ;
        l1 += b2 << 4;
        l1 = (l1 + (l1 >>> 8)) & 0x00FF00FF00FF00FFL;
        l1 = (l1 + (l1 >>> 16));
        l1 = (l1 + (l1 >>> 32));

        return l1 & 0xFFFF;
    }

Надеюсь, у автора будет возможность проверить и написать результат.

Информация

В рейтинге
Не участвует
Откуда
Украина
Зарегистрирован
Активность