Пост опять про музыку, но на этот раз без экзотики, играть будем по нотам.
За основу взят проект "Подмосковные вечера", и для понимания данного, стоит сначала изучить базовый вариант. Цель работы - реализовать полифонию, а также сделать музыкальный инструмент поинтереснее чем "телефонный гудок" (меандр). Для этих целей во-первых нужно иметь возможность не просто давать попеременно 0 и 1 с частотой звука,а плавно менять уровень сигнала. Это уже умеет делать модуль "ЦАП" из моего прошлого поста, преобразующий 8-битный уровень сигнала в ШИМ:
Как изменить частоту на один тон? Надо всё делать быстрее в 2^(1/12) раз. В проекте "Подмосковные вечера" водитель ритма посылает импульс один раз на полуцикл звуковой частоты, но нам для усложненного сэмплера нужно в 200 раз больше отсчетов, но если мы просто разделим константы отсюда:
...
// 8: begin factor = 6378; note_leds = 8'b00010000; end //G
// 9: begin factor = 6017; note_leds = 8'b00110000; end //G#
// 10: begin factor = 5682; note_leds = 8'b00100000; end //A
// 11: begin factor = 5364; note_leds = 8'b01100000; end //A#
...
на 200, то получим слишком маленькие числа, а значит высокую погрешность. Поэтому механизм водителя ритма немного изменен:
...
8: begin factor = 2055; end //G
// 9: begin factor = 2177; end //G#
10: begin factor = 2307; end //A 440(Hz)*200(отсчетов на сэмпл)*2^15(cnt[])*2(нот)/5000000
// 11: begin factor = 2888; end //A# A* 2^(1/12)
...
cnt[n_queue] <= cnt[n_queue] + factor;
...
assign sampler_clock[0] = cnt[0][15];
assign sampler_clock[1] = cnt[1][15];
...
В результате реализовано два канала, в первом "инструментом" остался меандр, во втором стала синусоида.
Для экономии ресурсов используется разделение по времени, n_queue указывает, для какого из каналов производятся вычисления. В результате получилось добавить "вечерам" басовый инструмент, вот только динамик плохо справлялся, поэтому я повысил частоту на октаву.
Замечания:
- В схеме один выход перенаправлен по сравнению с первоначальным проектом на другой пин (F1->F2).
- Добавлен reset по включению питания (нужен для синуса).
- Ресурсов не хватало, поэтому всё не очень нужное повыкидовано, на более ёмкой ПЛИС-е можно создать более интересные инструменты, сделать больше каналов и увеличить играемый фрагмент.
Взять весь проект можно здесь:
И еще, по просьбе читателей, дополнение: про генератор синуса.
Берем единичный вектор на плоскости и начинаем равномерно вращать:
X(0)= 0
Y(0)= 1
X(t+1)= X(t) + alpha*Y(t)
Y(t+1)= Y(t) - alpha*X(t)
X и Y тут дают синус и косинус, вторые части выражений - приращения компонент за единицу времени и они зависят от угла, на который поворачиваем за шаг. Угол выбираем вида 2^(-С) так, чтобы умножение на него превращалось в знаковый сдвиг вправо на С. Отсюда взялась константа 200: 2*Pi/200 ~= 1/32.
На верилоге это будет записано как-то так:
if ( reset ) begin
y <= (1<<<Rm)-1; /* Rm-размер дробной части числа с фиксированной точкой */
x <= 0;
end
else begin
x <= ( x + (y>>>N) );
y <= ( y - (x>>>N) );
end
Реальный код немного сложнее, из-за борьбы с накапливающейся погрешностью:
if ( reset ) begin
y <= (1<<<Rm)-1;
x <= 0;
end
else begin
if ( y[R:Rm]== 2'b01 ) begin
/*После каждого поворота переинициализируем, чтобы не накапливалась погрешность*/
y <= (1<<<Rm)-1;
x <= 0;
end
else begin
x <= ( x + (y>>>N) - (x>>>(2*N+1)) ); /*Для большей точности, учитываем вторую производную*/
y <= ( y - (x>>>N) - (y>>>(2*N+1)) );
end
end
Обратите внимание, что этот простой способ годится только для получения последовательных значений синуса, для вычисления sin от произвольного х, придётся использовать алгоритм посложнее, например метод цифра за цифрой (CORDIC).
Подробнее...