ปัญหา:
การตรวจจับโน้ตดนตรีจากสัญญาณเสียงบน Arduino เป็นเรื่องที่ทำได้ยากเพราะหน่วยความจำและพลังประมวลผลมีจำกัด ปกติแล้วโน้ตดนตรีไม่ได้เป็นคลื่นไซน์บริสุทธิ์ (pure sine wave) ทำให้การตรวจจับมันยากขึ้นไปอีก ถ้าเราทำการแปลงสัญญาณด้วย FFT (Frequency Transform) ของเครื่องดนตรีหลากหลายชนิด มันอาจจะมีฮาร์มอนิก (harmonics) หลายตัวผสมกันขึ้นอยู่กับโน้ตที่เล่น เครื่องดนตรีแต่ละชนิดมีลายเซ็นเฉพาะตัวของฮาร์มอนิกที่ผสมกันต่างกันไป ในโค้ดนี้ พี่ลองเขียนโปรแกรมที่พยายามครอบคลุมเครื่องดนตรีให้ได้มากที่สุด น้องสามารถดูวิดีโอที่พี่แนบมาด้วยได้เลย พี่ลองทดสอบกับเครื่องดนตรีหลายแบบ ทั้งโทนต่างๆ ที่สร้างจากคีย์บอร์ด และแม้แต่เสียงร้องเพลงเลย ความแม่นยำในการตรวจจับก็ต่างกันไปตามเครื่องดนตรี บางเครื่อง (เช่น เปียโน) ในช่วงที่จำกัดก็แม่นยำดี ในขณะที่บางเครื่อง (เช่น หีบเพลงปาก) ความแม่นยำก็ต่ำลง
โค้ดนี้ใช้ FFT code ที่พัฒนามาก่อนหน้านี้ชื่อ EasyFFT
อัลกอริทึมสำหรับการตรวจจับโน้ต
อย่างที่บอกไปในขั้นตอนก่อนหน้า การตรวจจับทำได้ยากเพราะในตัวอย่างเสียงมีหลายความถี่ (multiple frequencies) ปนกัน
โปรแกรมทำงานตามขั้นตอนต่อไปนี้:
1. การเก็บข้อมูล (Data acquisition):
- ส่วนนี้จะเก็บตัวอย่าง (samples) จำนวน 128 ตัวอย่างจากข้อมูลเสียง ช่วงห่างระหว่างสองตัวอย่าง (ความถี่ในการสุ่มตัวอย่าง หรือ sampling frequency) ขึ้นอยู่กับความถี่ที่เราสนใจ ในกรณีนี้ ช่วงห่างระหว่างตัวอย่างสองตัวจะถูกใช้เพื่อประยุกต์ใช้ฟังก์ชันหน้าต่างฮานน์ (Hann window function) รวมถึงการคำนวณค่าแอมพลิจูด/ค่า RMS ด้วย โค้ดนี้ยังทำการปรับศูนย์คร่าวๆ โดยการลบค่า 500 ออกจากค่าที่อ่านได้จาก analogRead ค่านี้สามารถเปลี่ยนได้ถ้าจำเป็น สำหรับกรณีทั่วไป ค่าเหล่านี้ทำงานได้ดี นอกจากนี้ ต้องเพิ่มดีเลย์ (delay) บางส่วนเพื่อให้ได้ความถี่ในการสุ่มตัวอย่างประมาณ 1200Hz ในกรณีที่ความถี่สุ่มตัวอย่างเป็น 1200Hz เราสามารถตรวจจับความถี่สูงสุดได้ที่ 600 Hz (ตามทฤษฎีของ Nyquist)
for(int i=0;i<128;i++)
{
a=analogRead(Mic_pin)-500; //rough zero shift
sum1=sum1+a; //to average value
sum2=sum2+a*a; // to RMS value
a=a*(sin(i*3.14/128)*sin(i*3.14/128)); // Hann window
in[i]=4*a; // scaling for float to int conversion
delayMicroseconds(195); // based on operation frequency range
}
2. FFT: เมื่อข้อมูลพร้อมแล้ว ก็จะทำการแปลง FFT โดยใช้ EasyFFT ฟังก์ชัน EasyFFT นี้ถูกปรับแก้ให้ทำ FFT สำหรับ 128 ตัวอย่างโดยเฉพาะ โค้ดยังถูกปรับเพื่อลดการใช้หน่วยความจำอีกด้วย ฟังก์ชัน EasyFFT ต้นฉบับถูกออกแบบให้รองรับตัวอย่างได้สูงสุดถึง 1028 ตัวอย่าง (กับบอร์ดที่รองรับ) ในขณะที่เราต้องการแค่ 128 ตัวอย่าง โค้ดนี้ช่วยลดการใช้หน่วยความจำลงได้ประมาณ 20% เมื่อเทียบกับฟังก์ชัน EasyFFT ต้นฉบับ
เมื่อทำ FFT เสร็จแล้ว โค้ดจะคืนค่าความถี่ที่โดดเด่นที่สุด 5 อันดับแรก (Top 5 peaks) มาให้เราวิเคราะห์ต่อ โดยความถี่เหล่านี้จะถูกเรียงลำดับจากแอมพลิจูดสูงไปต่ำ
3. การตรวจจับโน้ต (Note detection): สำหรับแต่ละพีค (peak) โค้ดจะพยายามตรวจจับว่าโน้ตอะไรที่อาจจะเกี่ยวข้องกับความถี่นั้น โค้ดนี้จะสแกนความถี่ไปจนถึงแค่ 1200 Hz นะตัว ไม่จำเป็นว่าโน้ตที่เจอจะต้องเป็นความถี่ที่มีแอมพลิจูดสูงสุดเสมอไป
ความถี่ทั้งหมดจะถูกแมป (map) ค่าอยู่ระหว่าง 0 ถึง 255 ตรงนี้เราจะตรวจจับแค่ช่วงแรก (first octave) ตัวอย่างเช่น 65.4 Hz ถึง 130.8 Hz จะแทนหนึ่งอ็อกเทฟ, 130.8 Hz ถึง 261.6 Hz ก็จะเป็นอีกอ็อกเทฟนึง ในแต่ละอ็อกเทฟ เราจะแมปค่าความถี่จาก 0 ถึง 255 โดยเริ่มแมปจากโน้ต C ไปถึง C' (C ตัวสูง)
if(f_peaks[i]>1040){f_peaks[i]=0;}
if(f_peaks[i]>=65.4 && f_peaks[i]<=130.8) {f_peaks[i]=255*((f_peaks[i]/65.4)-1);}
if(f_peaks[i]>=130.8 && f_peaks[i]<=261.6) {f_peaks[i]=255*((f_peaks[i]/130.8)-1);}
if(f_peaks[i]>=261.6 && f_peaks[i]<=523.25){f_peaks[i]=255*((f_peaks[i]/261.6)-1);}
if(f_peaks[i]>=523.25 && f_peaks[i]<=1046) {f_peaks[i]=255*((f_peaks[i]/523.25)-1);}
if(f_peaks[i]>=1046 && f_peaks[i]<=2093) {f_peaks[i]=255*((f_peaks[i]/1046)-1);}
อาเรย์ NoteV จะถูกใช้เพื่อกำหนดโน้ตให้กับความถี่ที่ตรวจจับได้
byte NoteV[13]={8,23,40,57,76,96,116,138,162,187,213,241,255};
a. การตรวจจับโน้ต (ต่อ): 4. หลังจากคำนวณหาโน้ตสำหรับแต่ละความถี่แล้ว มันอาจจะมีกรณีที่ความถี่หลายค่าชี้ไปที่โน้ตตัวเดียวกัน เพื่อให้ผลลัพธ์แม่นยำขึ้น โค้ดก็จะพิจารณาการซ้ำ (repetitions) ด้วย โดยจะรวมค่าความถี่ทั้งหมดเข้าด้วยกันโดยอิงตามลำดับแอมพลิจูดและการซ้ำของโน้ต แล้วเลือกโน้ตที่มีแอมพลิจูดรวมสูงที่สุดมาแสดงผล
B: การตรวจจับคอร์ด (Chord detection):
for (int i=0;i<12;i++)
{
in[20+i]=in[i]*in[i+4]*in[i+7];
in[32+i]=in[i]*in[i+3]*in[i+7]; //all chord check
}
ส่วนนี้จะตรวจสอบคอร์ดทั้งหมดโดยการคูณค่าของโน้ตเข้าด้วยกันตามรูปแบบของคอร์ดเมเจอร์และไมเนอร์ ยังใช้ Input array เดิมสำหรับเก็บข้อมูลต่อนะ หลังจากนั้น คอร์ดที่มีความเป็นไปได้สูงสุด (ผลคูณมากที่สุด) ก็จะถูกเลือกมาแสดงผล
มาแกะ Fast Fourier Transform (FFT) กันดีกว่า
หัวใจของโปรเจคนี้คือ Fast Fourier Transform (FFT) อัลกอริทึมสุดทรงพลังที่แปลงสัญญาณจากโดเมนเวลา (แอมพลิจูด vs เวลา) ไปเป็นโดเมนความถี่ (แอมพลิจูด vs ความถี่) อันนี้สำคัญมากเพราะว่าโน้ตดนตรีถูกนิยามด้วยความถี่พื้นฐาน (fundamental frequency) ของมัน กระบวนการ FFT เกี่ยวข้องกับ 3 ขั้นตอนหลักที่ใช้กับข้อมูลเสียงที่เราสุ่มมา (sampled audio data):
- การทำวินโดว์ (Windowing): ใช้ฟังก์ชันวินโดว์ (เช่น Hann window ที่ใช้ในโค้ด) เพื่อลด spectral leakage ซึ่งเป็นสิ่งรบกวนที่ทำให้พีคความถี่จริงๆ มัวเบลอได้
- การแปลง (Transformation): ทำการคำนวณ FFT ทางคณิตศาสตร์บนข้อมูลที่ผ่านวินโดว์แล้ว เพื่อแยกย่อยมันออกเป็นองค์ประกอบความถี่ต่างๆ
- การคำนวณขนาด (Magnitude Calculation): แปลงผลลัพธ์เชิงซ้อน (complex output) จาก FFT ให้เป็นค่าขนาด (magnitude) ซึ่งแสดงถึงความแรง (แอมพลิจูด) ของแต่ละองค์ประกอบความถี่ที่มีอยู่ในสัญญาณดั้งเดิม
จากนั้นโค้ดก็จะระบุพีคความถี่ที่โดดเด่นที่สุดจากสเปกตรัมของ magnitude นี้เพื่อนำไปวิเคราะห์หาโน้ตต่อไป จัดไปวัยรุ่น!
การขยายสัญญาณไมโครโฟนให้สุดพลัง
เพื่อการตรวจจับที่แม่นยำและเสถียร สัญญาณเสียงที่สะอาดและแรงเป็นสิ่งจำเป็น แนะนำให้ใช้โมดูลอย่าง MAX4466 electret microphone amplifier ตัวนี้มันมีเกนปรับได้ ช่วยให้เราขยายสัญญาณเบาๆ จากเครื่องดนตรีหรือเสียงร้องได้โดยไม่เพิ่มเสียงรบกวนมากนัก การตั้งค่าให้เหมาะสมจะทำให้สัญญาณอนาล็อกที่ส่งไปยังขา ADC ของ Arduino ใช้ช่วงไดนามิกเรนจ์ได้เต็มที่ ช่วยเพิ่มสัญญาณต่อสัญญาณรบกวน (SNR) และทำให้การวิเคราะห์ FFT ในขั้นตอนต่อไปแม่นยำขึ้น

วิธีใช้งาน
โค้ดนี้ใช้ง่ายมาก แต่ก็มีข้อจำกัดหลายอย่างที่ต้องจำไว้เวลาลองใช้ สามารถคัดลอกโค้ดไปใช้ตรวจจับโน้ตได้เลย แต่อย่าลืมดูกฎเหล็กด้านล่างนี้ด้วยนะ
1. กำหนดขา (Pin Assignment): ต้องแก้ไขตามการต่อขาในวงจรของตัวเอง สำหรับการทดลองของพี่ ใช้ขา Analog pin 7
void setup()
{Serial.begin(250000);
Mic_pin = A7;
}
2. ความไวไมโครโฟน (Microphone sensitivity): ต้องปรับความไวของไมโครโฟนให้ได้รูปคลื่นที่มีแอมพลิจูดสวยๆ ส่วนใหญ่โมดูลไมโครโฟนจะมีปุ่มปรับความไว ให้เลือกค่าที่เหมาะสม อย่าให้สัญญาณเบาจนเกินไป หรือแรงจนคลื่นถูกตัด (clip) เพราะแอมพลิจูดสูงเกิน
3. ค่าเกณฑ์แอมพลิจูด (Amplitude threshold): โค้ดนี้จะทำงานก็ต่อเมื่อแอมพลิจูดของสัญญาณสูงพอ ค่านี้ผู้ใช้ต้องตั้งเอง มันขึ้นอยู่กับความไวไมโครโฟนและงานที่ทำ
if(sum2-sum1>5){
.
.
ในโค้ดด้านบน sum2 ให้ค่า RMS ส่วน sum1 ให้ค่าเฉลี่ย ผลต่างของสองค่านี้คือแอมพลิจูดของสัญญาณเสียง สำหรับพี่ มันทำงานดีที่ค่าประมาณ 5
4. โดยค่าเริ่มต้น โค้ดนี้จะพิมพ์โน้ตที่ตรวจจับได้ออกมา แต่ถ้าน้องจะเอาโน้ตไปใช้ทำอย่างอื่น ให้ใช้ตัวเลขที่กำหนดแทน เช่น C=0; C#=1, D=2, D#=3 ไปเรื่อยๆ
5. ถ้าเครื่องดนตรีมีความถี่สูงเกิน โค้ดอาจให้ผลผิดพลาดได้ ความถี่สูงสุดถูกจำกัดโดยความถี่ในการสุ่มตัวอย่าง (sampling frequency) น้องอาจต้องลองปรับค่าดีเลย์ด้านล่างเพื่อให้ได้ผลลัพธ์ที่ดีที่สุด ในโค้ดด้านล่างใช้ดีเลย์ 195 ไมโครวินาที ซึ่งอาจต้องปรับแต่งเพื่อให้ได้ผลลัพธ์เหมาะสม การปรับนี้จะส่งผลต่อเวลาการทำงานทั้งหมดของโปรแกรม
{ a=analogRead(Mic_pin)-500; //rough zero shift
sum1=sum1+a; //to average value
sum2=sum2+a*a; // to RMS value
a=a*(sin(i*3.14/128)*sin(i*3.14/128)); // Hann window
in[i]=4*a; // scaling for float to int conversion
delayMicroseconds(195); // based on operation frequency range
}
6. โค้ดนี้จะทำงานได้จนถึงความถี่ 2000Hz เท่านั้น หากต้องการความถี่สูงกว่านั้น ต้องกำจัดดีเลย์ระหว่างการสุ่มตัวอย่างออกไป ซึ่งจะทำให้ได้ความถี่ในการสุ่มตัวอย่างประมาณ 3-4 kHz
ข้อควรระวัง:
- ตามที่บอกไว้ในบทสอน EasyFFT, การคำนวณ FFT กินหน่วยความจำของ Arduino เยอะมาก ดังนั้นถ้าโปรแกรมของน้องต้องเก็บค่าอะไรบางอย่าง แนะนำให้ใช้บอร์ดที่มีหน่วยความจำสูงกว่านะ
- โค้ดนี้อาจทำงานดีกับเครื่องดนตรีหรือนักร้องคนหนึ่ง แต่กลับไม่ดีกับอีกคน การตรวจจับที่แม่นยำแบบเรียลไทม์เป็นไปไม่ได้เลย เนื่องจากข้อจำกัดด้านการคำนวณของบอร์ด รู้ไว้ใช้ว่า อย่าคาดหวังสูงลิ่ว!
สรุป
การตรวจจับโน้ตดนตรีเป็นงานที่ใช้พลังประมวลผลโคตรๆ การจะได้ผลลัพธ์แบบเรียลไทม์เนี่ยยากสุดๆ โดยเฉพาะบน Arduino โค้ดตัวนี้สามารถให้ผลลัพธ์ได้ประมาณ 6.6 ตัวอย่าง / วินาที (เมื่อเพิ่มดีเลย์ 195 ไมโครวินาที) มันทำงานได้ดีกับเปียโนและเครื่องดนตรีอื่นๆ อีกบางชนิด
หวังว่าโค้ดและบทสอนนี้จะมีประโยชน์กับโปรเจคเกี่ยวกับดนตรีของน้องๆ นะจ๊ะ ถ้ามีข้อสงสัยหรือข้อเสนอแนะอะไร ก็จัดมาได้เต็มที่เลย