c - 合成器 - 记录按下的键盘

标签 c audio avr atmega synthesizer

我必须构建合成器,并且我正在使用 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;
}

键盘.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

最佳答案

实际上,您关于如何录制和重放按键的问题应该由另一个关于如何同时播放多个声音的问题来解决。 现在您只使用可变频率的 PWM 输出。但这只允许您生成单个方形波形。您不能演奏两个音符(除非使用另一个计时器和另一个 PWM 输出)。 相反,我建议您使用最高频率的 PWM,并应用 RC 或 LC 滤波器将高频 PWM 信号平滑为波形,然后将该波形应用于放大器和扬声器以发出声音。 通过这种方法,您可以生成不同的波形,将它们混合在一起,使它们更响亮或更安静,甚至应用“淡出”效果使它们听起来像钢琴。 但你的问题不是关于如何生成那种波形,所以如果你想知道你应该开始另一个问题。

那么,回到你的问题。

而不是有一个单一的过程,即开始音符,暂停,然后才返回;我建议你有几个程序,一个 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 次(基于一些具有 1:1024 预分频器溢出的 8 位定时器),并且需要注意:哪些键改变了状态。如果按下某个键,则调用 play_note 开始相应的音符。如果松开按键,您将调用 stop_note,同时您还会计算自上次事件以来经过了多少个计时器周期。 录制时,您只需将这些事件插入数组“按下X键”或“释放X键”或“X定时器周期到期”。 播放时,您只需执行向后过程,扫描数组并执行命令:调用 play_notestop_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) 在同一时刻按下多少个音符并不重要。由于暂停和按键事件是分开记录的,因此在重放期间,所有同时按下的按键都会同时重放,不会产生“琶音”效果

关于c - 合成器 - 记录按下的键盘,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50075635/

相关文章:

c - 使用 C 中的 Do..while 循环登录

c - 不同的字符串如何具有相同的地址

c++ - AVR ATMega169P 液晶显示器使用 C++

c - c中指向数组的指针

c - 将结构指针或数组作为函数参数传递

android - 从 Android 上的 OPUS 帧创建可播放的音频文件。

audio - C++ FFmpeg 转换音频时声音失真

python-3.x - audioop.rms(fragment, width)中的fragment和width是什么意思

c - C 中是否有一个标准函数允许在 nSec 中设置特定延迟?

c - 定时器比较匹配的 ISR 未运行