Arduinos chỉ cung cấp đầu ra kỹ thuật số: đầu ra là bật (+ 5V) hoặc tắt (0V). Hàm tone()
, mà tôi cho rằng bạn đã chạy vào thời điểm này, tạo ra một làn sóng vuông ở tần số được chỉ định.
Giả sử bạn muốn có một giai điệu 100Hz. 100Hz nghĩa là đầu ra lặp lại mỗi 1/100 giây, hoặc 10ms. Vì vậy, tone(PIN,100)
sẽ đặt gián đoạn hẹn giờ được gọi là mỗi 5ms. Lần đầu tiên ngắt được gọi, nó đặt đầu ra thấp và trả về bất kỳ chương trình nào của bạn đang làm. Lần sau nó được gọi là nó đặt đầu ra cao. Vì vậy, mỗi 5ms đầu ra thay đổi, và bạn nhận được một làn sóng vuông ở chu kỳ nhiệm vụ 50%, có nghĩa là đầu ra là cho một nửa thời gian chính xác.
Đây là tất cả đều rất tốt, nhưng hầu hết các dạng sóng âm thanh không phải là sóng vuông. Nếu bạn muốn chơi hai tông màu vuông đồng thời, hoặc thậm chí để điều khiển âm lượng của một âm thanh sóng vuông đơn, bạn cần có khả năng xuất nhiều giá trị hơn chỉ là "bật" và "tắt".
Tin tốt là có một mẹo bạn có thể sử dụng được gọi là điều chế độ rộng xung (thường được viết tắt là PWM). Ý tưởng là bạn chỉ có thể đặt đầu ra của mình thành một trong hai giá trị, nhưng bạn có thể thực hiện nhanh như vậy. Con người có thể nghe thấy tần số âm thanh lên đến khoảng 20kHz. Nếu bạn lừa phỉnh đầu ra của bạn nhanh hơn mà, nói tại 200kHz (cũng trong khả năng của Arduino, mà là ở tốc độ 16MHz), bạn không nghe thấy các hiệu ứng chuyển tiếp sản lượng cá nhân, nhưng trung bình giá trị trong khoảng thời gian lâu hơn.
Hãy tưởng tượng tạo âm 200 kHz với tone()
. Đó là cách quá cao để nghe, nhưng giá trị trung bình là nửa chừng giữa và tắt (chu kỳ nhiệm vụ 50%, nhớ không?). Vì vậy, chúng tôi hiện có ba giá trị đầu ra có thể có: bật, tắt và nửa chừng. Đây là đủ để cho phép chúng tôi chơi hai sóng vuông đồng thời:
âm thanh chất lượng cao đòi hỏi nhiều giá trị hơn này. CD lưu trữ âm thanh 16 bit, có nghĩa là có 65536 giá trị có thể.Và trong khi chúng tôi sẽ không nhận được âm thanh chất lượng CD ra khỏi Arduino, chúng tôi có thể nhận được nhiều giá trị đầu ra hơn bằng cách chọn một chu kỳ nhiệm vụ khác hơn 50%. Trong thực tế, Arduino có phần cứng để điều này cho chúng tôi.
Gặp gỡ analogWrite()
. Điều này tạo ra các mức đầu ra khác nhau bằng cách sử dụng phần cứng PWM tích hợp của Arduino. Tin xấu là tần số PWM thường là 500Hz, điều này là tốt cho việc làm mờ một đèn LED nhưng cách quá thấp cho âm thanh. Vì vậy, chúng tôi phải lập trình phần cứng đăng ký ourself.
Secrets of Arduino PWM có thêm một số thông tin và đây là detailed reference về cách triển khai DAC PWM trên Arduino.
Tôi chọn độ phân giải 7 bit, có nghĩa là đầu ra là sóng vuông 16MHz/128 = 125kHz với 128 chu kỳ nhiệm vụ có thể.
Tất nhiên, một khi bạn đã có đầu ra PWM, hoạt động vui nhộn chỉ mới bắt đầu. Với nhiều giọng nói bạn không thể dựa vào các ngắt để đặt tần số dạng sóng của bạn, bạn phải tự kéo dài chúng. Kiến thức về xử lý tín hiệu kỹ thuật số cơ bản (DSP) sẽ rất tiện dụng. Bạn sẽ cần mã chặt chẽ để tạo ra dữ liệu âm thanh từ bên trong một trình xử lý ngắt, và sau đó bạn sẽ cần một playroutine để kích hoạt các ghi chú thích hợp vào đúng thời điểm. Bầu trời là giới hạn!
Dù sao, đây là một số mã:
#define PIN 9
/* these magic constants were generated by the following perl script:
#!/usr/bin/perl -lw
my $freq = 16000000/256;
my $A4 = 440;
print int(128*$freq/$A4*exp(-log(2)*$_/12)) for (-9..2);
*/
const uint16_t frtab[] = {
30578, 28861, 27241, 25712,
24269, 22907, 21621, 20408,
19262, 18181, 17161, 16198
};
#define VOICES 4
struct voice {
uint16_t freq;
int16_t frac;
uint8_t octave;
uint8_t off;
int8_t vol;
const uint8_t *waveform;
} voice[VOICES];
#define PITCH 50 /* global pitch adjustment */
/* some waveforms. 16 samples each */
const uint8_t square_50[] = {
0, 0, 0, 0, 0, 0, 0, 0,15,15,15,15,15,15,15,15
};
const uint8_t square_25[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,15,15,15,15
};
const uint8_t square_12[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,15,15
};
const uint8_t square_6[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,15
};
const uint8_t sawtooth[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15
};
const uint8_t triangle[] = {
0, 2, 4, 6, 8,10,12,14,15,13,11, 9, 7, 5, 3, 1
};
const uint8_t nicebass[] = {
0, 8,14,18,22,23,24,25,26,25,24,23,22,18,14, 8
};
void setup() {
/* TIMER0 is used by the Arduino environment for millis() etc.
So we use TIMER1.
*/
pinMode(PIN, OUTPUT);
/* fast PWM, no prescaler */
TCCR1A = 0x80;
TCCR1B = 0x11;
/* 7-bit precision => 125kHz PWM frequency */
ICR1H = 0;
ICR1L = 0x7f;
/* enable interrupts on TIMER1 overflow */
TIMSK1 = 1;
OCR1AH = 0; /* hi-byte is unused */
for (uint8_t i=0; i<VOICES; i++)
clear_voice(i);
}
void set_voice(uint8_t v, uint8_t note, uint8_t volume, const uint8_t *waveform) {
note += PITCH;
voice[v].octave = note/12;
voice[v].freq = frtab[note%12];
voice[v].frac = 0;
voice[v].off = 0;
voice[v].waveform = waveform;
voice[v].vol = volume;
}
void clear_voice (uint8_t v) {
voice[v].freq = 0;
}
uint8_t s = 0;
ISR(TIMER1_OVF_vect) {
/* Calculate new data every 4 pulses, i.e. at 125/4 = 31.25kHz.
Being interrupted unnecessarily is kinda wasteful, but using another timer is messy.
*/
if (s++ & 3)
return;
int8_t i;
int8_t out = 0;
for (i=0; i<VOICES; i++) {
if (voice[i].freq) {
voice[i].frac -= 128<<voice[i].octave;
if (voice[i].frac < 0) { /* overflow */
voice[i].frac += voice[i].freq;
voice[i].off++;
}
/* warning: vol isn't a real volume control, only for square waves */
out += (voice[i].waveform[voice[i].off & 15]) & voice[i].vol;
}
}
/* out is in the range 0..127. With 4-bit samples this gives us headroom for 8 voices.
Or we could use more than 4-bit samples (see nicebass).
*/
OCR1AL = out;
}
/* tune data */
const uint8_t bass[8][4] = {
{ 12, 19, 23, 24 },
{ 5, 12, 19, 21 },
{ 12, 19, 23, 24 },
{ 5, 12, 19, 21 },
{ 14, 16, 17, 21 },
{ 7, 19, 14, 19 },
{ 14, 16, 17, 21 },
{ 7, 19, 14, 19 }
};
const uint8_t melody[2][8][16] = {
{/* first voice */
{31, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 0,28,26,24 },
{ 0, 0, 0, 0, 0, 1, 2, 3,53,54,53,54, 0, 1, 2, 3 },
{31, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 5,28, 5,26 },
{ 5,28,24, 0, 0, 1, 2, 3,53,54,56,54, 0, 1, 2, 3 },
{29, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 5, 0,28, 5 },
{28, 5, 0,26, 0, 1, 2, 3,54,56,58,56, 0, 1, 2, 3 },
{29, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 5, 0,28, 5 },
{28, 5, 0,26, 0, 1, 2, 3, 0,19,21,23,24,26,28,29 },
},
{/* second voice */
{24, 0, 0, 0, 0, 1, 2, 3,24,24,24,24, 0,24,24,21 },
{ 0, 0, 0, 0, 0, 1, 2, 3,49,51,49,51, 0, 1, 2, 3 },
{24, 0, 0, 0, 0, 1, 2, 3,24,24,24,24, 5,24, 5,24 },
{ 5,23,21, 0, 0, 1, 2, 3,49,51,53,51, 0, 1, 2, 3 },
{26, 0, 0, 0, 0, 1, 2, 3,24,26,24,24, 5, 0,24, 5 },
{24, 5, 0,24, 0, 0, 0, 0,51,51,54,54, 0, 1, 2, 3 },
{26, 0, 0, 0, 0, 1, 2, 3,24,26,24,24, 5, 0,24, 5 },
{24, 5, 0,23, 0, 1, 2, 3, 0, 5, 0,19,21,23,24,26 },
}
};
void loop() {
uint8_t pos, i, j;
for (pos=0; pos<8; pos++) {
for (i=0; i<16; i++) {
/* melody: voices 0 and 1 */
for (j=0; j<=1; j++) {
uint8_t m = melody[j][pos][i];
if (m>10) {
/* new note */
if (m > 40) /* hack: new note, keep volume */
set_voice(j, m-30, voice[j].vol, square_50);
else /* new note, full volume */
set_voice(j, m, 15, square_50);
} else {
voice[j].vol--; /* fade existing note */
switch(m) { /* apply effect */
case 1: voice[j].waveform = square_25; break;
case 2: voice[j].waveform = square_12; break;
case 3: voice[j].waveform = square_6; break;
case 4: clear_voice(j); break; /* unused */
case 5: voice[j].vol -= 8; break;
}
if (voice[j].vol < 0)
voice[j].vol = 0; /* just in case */
}
}
/* bass: voices 2 and 3 */
set_voice(2, bass[pos][i%4], 31, nicebass);
set_voice(3, bass[pos][0]-12, 15-i, sawtooth);
delay(120); /* time per event */
}
}
}
này đóng một giai điệu bốn-thoại. Tôi chỉ có một Arduino Leonardo (tốt, Pro Micro) để kiểm tra nó, vì vậy bạn có thể cần phải thay đổi PIN
theo đó pin được nối với TIMER1A (nếu tôi đọc chính xác nó là pin 9 trên Uno và pin 11 trên một Mega). Bạn không nhận được sự lựa chọn pin nào để sử dụng, thật đáng buồn.
Tôi cũng đã chỉ thử nghiệm nó với tai nghe, vì vậy tôi không có ý tưởng làm thế nào nó sẽ âm thanh trên một còi piezo ...
Hy vọng rằng nó cung cấp cho bạn một số ý tưởng của possiblities, và một điểm khởi đầu tiềm năng cho giai điệu của riêng bạn. Vui lòng giải thích bất cứ điều gì không rõ ràng, và cũng cảm ơn bạn đã cho tôi lý do để viết điều này :)
Tôi không hiểu - bạn có thể đăng một số mã ví dụ giải thích cách sử dụng thư viện Tone của bên thứ ba mà bạn tham chiếu để phát đồng thời hay không âm trên một loa? –