Синтезатор - запись нажатой клавиатуры
Я должен построить синтезатор, и я использую C для программирования моего ATmega128A. Мне нужно записать нажатие клавиш и воспроизвести их через некоторое время. Для нажатия клавиш я использую опрос в main.c. Для игры на клавишах я использую Timer1. Каждый раз, когда истекает таймер, я сохраняю частоту клавиатуры и счетчик приращений для нее. Во время игры я сначала вычисляю длительность, а затем играю ее для этого интервала. Когда я хочу воспроизвести сохраненную песню, она тикает некоторое время и начинает издавать длинный звук.
Кроме того, я хочу сделать возможным нажимать, записывать и воспроизводить одновременные клавиатуры. Можете ли вы предложить какой-то алгоритм для этого?
main.c
#include <avr/io.h>
#include <avr/interrupt.h>
#include "keypad.h"
unsigned char temp; // to get keyboard input to play a note
unsigned char option; //to choose the embedded music to play
#define DELAY 1000
int main(void)
{
DDRG = 0xff; // To send sound to BUZ speakers (BUZ is connected to PG.4)
DDRD = 0x00; // Make it input, to get corresponding key to play a note
PORTD = 0xff; // All bits are 1s, so no button is pressed in the beginning
sei(); //Set Interrupt flag as enabled in SREG register
option = no_music; //No music is played on startup, this is default mode for free playing
// This loop keeps playing forever, so the main functionality
// of the program is below
DDRB = 0xff;
DDRD = 0x00; //ready for input
while(1)
{
temp = PIND; //store keyboard input for temporary variable
//PORTB = PIND;
switch(temp)
{
case 254: { // if 1st pin of PORTD is pressed
play_note(notes5[0]); // play corresponding note from octave 5 for 200ms
break;
}
case 253: { // if 2nd pin of PORTD is pressed
play_note(notes5[1]);
break;
}
case 251: { // if 3rd pin of PORTD is pressed
play_note(notes5[2]);
break;
}
case 247: { // if 4th pin of PORTD is pressed
play_note(notes5[3]);
break;
}
case 239: { // if 5th pin of PORTD is pressed
play_note(notes5[4]);
break;
}
case 223: { // if 6th pin of PORTD is pressed
play_note(notes5[5]);
break;
}
case 191: { // if 7th pin of PORTD is pressed
play_note(notes5[6]);
break;
}
case 127: {
if(isRecordingEnabled){
disableRecording();
//toggling LED as the sign of playing the record
toggleLED();
custom_delay_ms(DELAY);
toggleLED();
custom_delay_ms(DELAY);
custom_delay_ms(DELAY);
play_record();
}else{
//toggling LED as the sign of record start
toggleLED();
enableRecording();
}
}
}
}
return 0;
}
keypad.c
#include "structs.h"
#include "play.h"
#define F_CPU 16000000UL // 16 MHz
#include <util/delay.h>
#define BUFFER_SIZE 100
struct played_note buffer[BUFFER_SIZE];
int i = 0;
int8_t isRecordingEnabled = 0;
int8_t recordIndex = 0;
int8_t pressedNote;
int8_t isPressed = 0;
int8_t isPlaying = 0;
unsigned int ms_count = 0;
#define INTERVAL 100
#define DELAY_VALUE 0xFF
ISR(TIMER1_COMPA_vect){
// every time when timer0 reaches corresponding frequency,
// invert the output signal for BUZ, so it creates reflection, which leads to sound generation
//check whether the key was pressed because
//when the recording is enabled the interrupt is working make sound
if(isPressed || isPlaying)
PORTG = ~(PORTG);
if(isRecordingEnabled){
if(PIND == DELAY_VALUE)
pressedNote = DELAY_VALUE;
if(i == 0){
buffer[i].note = pressedNote;
buffer[i].counter = 0;
i++;
}else{
if(buffer[i - 1].note == pressedNote){
//the same note is being pressed
buffer[i - 1].counter++;
}else{
buffer[i++].note = pressedNote;
buffer[i].counter = 0;
}
}
}
}
void initTimer1(){
TIMSK = (1 << OCIE1A); //Timer1 Comparator Interrupt is enabled
TCCR1B |= (1 << WGM12) | (1 << CS12); //CTC mode, prescale = 256
}
void stopTimer1(){
TIMSK &= ~(1UL << OCIE1A);
TCCR1A = 0; //stop the timer1
TIFR = (1 << OCF1A); //Clear the timer1 Comparator Match flag
}
void enableRecording(){
isRecordingEnabled = 1;
i = 0;
ms_count = 0;
initTimer1();
}
void disableRecording(){
isRecordingEnabled = 0;
stopTimer1();
}
//Timer1A
void play_note_during(unsigned int note, unsigned int duration){
OCR1A = note;
pressedNote = note;
isPressed = 1;
initTimer1();
custom_delay_ms(duration);
stopTimer1();
isPressed = 0;
}
//Timer1A
void play_note(unsigned int note){
play_note_during(note, INTERVAL);
}
void play_record(){
isPlaying = 1;
recordIndex = 0;
int duration;
while(recordIndex < i){
PORTB = buffer[return].counter << 8;
duration = INTERVAL * buffer[recordIndex].counter;
if(buffer[recordIndex].note == DELAY_VALUE)
custom_delay_ms(duration);
else
play_note_during(buffer[recordIndex].note, duration);
recordIndex++;
}
isPlaying = 0;
}
Дополнительные ссылки можно найти в следующем репозитории github: https://github.com/bedilbek/music_simulation
1 ответ
На самом деле ваш вопрос о том, как записывать и воспроизводить нажатия клавиш, должен быть предрешен другим вопросом о том, как воспроизводить несколько звуков одновременно. Теперь вы используете только ШИМ-выход с переменной частотой. Но это позволяет генерировать только одну волну квадратной формы. Вы не можете играть две ноты (кроме использования другого таймера и другого выхода ШИМ). Вместо этого я предлагаю вам использовать ШИМ на самой высокой частоте и применять RC или LC-фильтры для сглаживания высокочастотного ШИМ-сигнала в виде сигнала, а затем применить этот сигнал к усилителю и динамику для создания звука. Используя этот подход, вы генерируете различные формы волны, смешиваете их вместе, делаете их громче или тише и даже применяете эффект "затухания", чтобы они звучали как пианино. Но ваш вопрос не в том, как генерировать подобные волновые формы, поэтому, если вы хотите знать, вам следует начать другой вопрос.
Итак, возвращаясь к вашему вопросу.
Вместо единой процедуры, которая начинается с примечания, удерживается пауза и только потом возвращается обратно; Я предлагаю вам пройти несколько процедур, одну play_note(note)
- который начнет играть ноту и сразу же вернется (пока нота продолжит играть). И, конечно же, stop_note(note)
- который остановит указанную ноту, если она сыграна. Также я предлагаю вам передать номер ноты, а не частоту или период таймера play
функция. Давайте предположим, что 0 - это самая низкая возможная нота (например, C2), и затем они идут последовательно по полутонам: 1 - C#2, 2 - D2, .... 11 - B2, 12 - C3 ... и т. Д.
Впервые вы можете переделать свои процедуры игры на одной ноте, чтобы соответствовать этому.
// #include <avr/pgmspace.h> to store tables in the flash memory
PROGMEM uint16_t const note_ocr_table[] = {
OCR1A_VALUE_FOR_C2, OCR1A_VALUE_FOR_C2_SHARP, ... etc
}; // a table to map note number into OCR1A value
#define NOTE_NONE 0xFF
static uint8_t current_note = NOTE_NONE;
void play_note(uint8_t note) {
if (note >= (sizeof(note_ocr_table) / sizeof(note_ocr_table[0])) return; // do nothing on the wrong parameter;
uint16_t ocr1a_val = pgm_read_word(¬e_ocr_table[note]);
TIMSK = (1 << OCIE1A); //Timer1 Comparator Interrupt is enabled // why you need this? May be you want to use just inverting OC1A output?
TCCR1B |= (1 << WGM12) | (1 << CS12); //CTC mode, prescale = 256 // you may want to use lesser prescalers and higher OCR1A values ?
OCR1A = ocr1a_val;
if (TCNT1 >= ocr1a_val) TCNT1 = 0; // do not miss the compare match when ORC1A is changed to lower values;
current_note = note;
}
void stop_note(uint8_t note) {
if (note == current_note) { // ignore stop for non-current note.
TIMSK &= ~(1UL << OCIE1A);
TCCR1A = 0; //stop the timer1
TIFR = (1 << OCF1A); //Clear the timer1 Comparator Match flag
current_note = NOTE_NONE; // No note is playing
}
}
Итак, теперь ваша задача очень проста: вам нужно просто периодически проверять состояние клавиш, скажем, 61 раз в секунду (на основе какого-то 8-битного таймера с переполнением прескалера 1:1024), и они должны заметить: какие ключи изменены их состояние. Если какая-то клавиша нажата, вы звоните play_note
начать ответную заметку. Если ключ отпущен, вы звоните stop_note
Также вы подсчитываете, сколько циклов таймера прошло с момента последнего события. При записи вы просто помещаете эти события в массив "клавиша X нажата", или "клавиша X отпущена", или "истекло количество циклов таймера X". При воспроизведении вы просто выполняете обратный процесс, сканируете массив и выполняете команды: вызов play_note
, stop_note
или ожидание точного количества циклов таймера, если это пауза.
Вместо того чтобы писать гигант switch
Заявление, вы также можете использовать таблицу для сканирования кнопок
// number of element in the port-state arrays
#define A 0
#define B 1
#define C 2
#define D 3
#define E 4
#define F 5
typedef struct {
port_index uint8_t;
mask uint8_t;
} KeyLocation;
PROGMEM KeyLocation const key_location[] = {
{ B, (1 << 1) }, // where C2 is located, e.g. PB1
{ E, (1 << 3) }, // where C#2 is located, e.g. PE3
...
}
uint16_t ticks_from_prev_event = 0;
uint8_t port_state_prev[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0XFF};
for (;;) { // main loop
wait_tick_timer_to_overflow();
// latching state of the pins
uint8_t port_state[6] = {PINA, PINB, PINC, PIND, PINE, PINF};
for (uint8_t i = 0 ; i < (sizeof(key_location) / sizeof(key_location[0])) ; i++) {
uint8_t port_idx = pgm_read_byte(&key_location[i].port_index);
uint8_t mask = pgm_read_byte(&key_location[i].mask);
if ((port_state[port_idx] & mask) != (port_state_prev[port_idx] & mask)) { // if pin state was changed
if (is_recording && (ticks_from_prev_event > 0)) {
put_into_record_pause(ticks_from_prev_event); // implement it on your own
}
if ((port_state[port_idx] & mask) == 0) { // key is pressed
play_note(i);
if (is_recording) {
put_into_record_play_note(i); // implement
}
} else { // key is released
stop_note(i);
if (is_recording) {
put_into_record_stop_note(i); // implement
}
}
}
}
// the current state of the pins now becomes a previous
for (uint8_t i = 0 ; i < (sizeof(port_state) / sizeof(port_state[0])) ; i++) {
port_state_prev[i] = port_state[i];
}
if (ticks_from_prev_event < 65535) ticks_from_prev_event++;
}
put_into_record_...
реализовать, как вы хотите.
Воспроизведение будет таким же простым (под шаблоном ниже вы можете указать из названия функции, что они должны делать)
while (has_more_data_in_the_recording()) {
if (next_is_play()) {
play_note(get_note_from_recording())
} else if (next_is_stop()) {
play_note(get_note_from_recording())
} else {
uint16_t pause = get_pause_value_from_recording();
while (pause > 0) {
pause--;
wait_tick_timer_to_overflow();
}
}
}
Этот подход дает вам два преимущества:
1) Неважно, сколько нот может воспроизводить воспроизводящий модуль, клавиши записываются, когда они нажаты и отпущены, поэтому все одновременные клавиши будут записаны и воспроизведены одновременно.
2) Неважно, сколько нот нажимается в один и тот же момент. Поскольку события паузы и клавиш записываются отдельно, во время воспроизведения все нажатия клавиш в одно и то же время будут воспроизводиться в одно и то же время без эффекта "арпеджио".