синтезатор - запись нажатой клавиатуры - PullRequest
0 голосов
/ 28 апреля 2018

Мне нужно собрать синтезатор, и я использую 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 Ответ

0 голосов
/ 03 мая 2018

На самом деле ваш вопрос о том, как записывать и воспроизводить нажатия клавиш, должен ожидаться другим вопросом о том, как воспроизводить несколько звуков одновременно. Теперь вы используете только ШИМ-выход с переменной частотой. Но это позволяет генерировать только одну волну квадратной формы. Вы не можете играть две ноты (кроме использования другого таймера и другого выхода ШИМ). Вместо этого я предлагаю вам использовать ШИМ на самой высокой частоте и применять 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(&note_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) Неважно, сколько нот нажимается в один и тот же момент. Так как события паузы и клавиш записываются отдельно, во время воспроизведения все нажатия клавиш в одно и то же время будут воспроизводиться в одно и то же время без эффекта «арпеджио»

...