เทคนิคการเล่นไฟล์เพลงแบบ Streaming โดยการใช้บัฟเฟอร์คู่ (Double Buffering)

เทคนิคการใช้บัฟเฟอร์คู่ไม่ใช่เทคนิคแปลกใหม่อะไรครับ มีใช้กันมานานมากแล้วโดยเฉพาะกับการวาดกราฟิคขึ้นบนจอภาพ จุดประสงค์ของการทำบัฟเฟอร์คู่ก็คือลดการขาดช่วงของข้อมูลเสียง ซึ่งเป็นต้นเหตุของเสียงกระตุก ขาดช่วง หรือเสียงที่ไม่พึงประสงค์ (pop, crack เป็นต้น)

ก่อนจะไปถึงการใช้บัฟเฟอร์คู่ ขออธิบายคร่าว ๆ ก่อนว่าการทำ Streaming คืออะไร ตามปรกติการเล่นไฟล์เสียงนั้น เราจะทำการโหลดข้อมูลเสียงทั้งหมดลงไปใน memory ก่อน และเมื่อจะใช้เล่นก็จะก๊อปปี้เอาข้อมูลเสียงตรงนี้ส่งเข้าไปใน Device (เช่น Sound Card หรือ Audio Interface) แต่ว่า ข้อมูลเสียงนั้นจะมีขนาดค่อนข้างใหญ่ กินหน่วยความจำมาก (ราว ๆ 10 MB ต่อนาที ที่ PCM stereo 44.1 KHz 16bit) ดังนั้นการย้ายข้อมูลขนาดใหญ่ขนาดนี้ ย่อมใช้เวลา และการที่มีข้อมูลใหญ่ ๆ แบบนี้เก็บอยู่ในหน่วยความจำ มันก็ค่อนข้างสิ้นเปลืองอยู่เหมือนกัน

มีคนสังเกตว่า จริง ๆ แล้ว เมื่อเราเล่นเสียงไป ข้อมูลของเสียงที่เล่นไปแล้วจะไม่นำกลับมาใช้อีก … คิดถึงเพลงยาวสักสี่นาที เพมื่อเพลงเล่นไปจนถึงนาทีที่สอง เราจะไม่ใช้ข้อมูลเสียงตั้งแต่เริ่มต้นจนถึงนาทีที่สองแล้ว และยิ่งกว่านั้น ข้อมูลเสียงที่ยังมาไม่ถึง (เช่น ข้อมูลตั้งแต่นาทีที่สามขึ้นไป) จะยังไม่ถูกใช้งาน พูดกันง่าย ๆ ก็คือ เมื่อคุณฟังเพลง Art Of Life ที่ยาวสิบนาที ตลอดช่วงเวลาที่เล่นเพลงนี้ จะมีข้อมูลเกือบ ๆ 100MB ที่ถูกโหลดขึ้นมาแต่ไม่ได้ถูกใช้งาน และที่นาทีที่ 9 จะมีข้อมูลประมาณ 90MB ที่จะไม่ได้ใช้งานอีกแล้ว

ด้วยความหัวใส มีคนคิดอีกว่า แล้วทำไมเราไม่โหลดข้อมูลมาทีละก้อน เป็นก้อนเล็ก ๆ เก็บเอาไว้ในหน่วยความจำ พอใช้หมดแล้วก็โหลดก้อนใหม่ขึ้นมา ก้อนเก่าที่ใช้เสร็จแล้วก็ทิ้งไป เทคนิคนี้เรียกว่าการทำ Streaming ครับ

ทีนี้ แล้วกลับมาที่บัฟเฟอร์คู่ … ทำไมต้องใช้บัฟเฟอร์คู่ด้วย ผมอยากให้จินตนาการณ์ว่า ข้อมูลเสียงหรือไฟล์เพลงเป็นถังน้ำ บัฟเฟอร์คือแก้วน้ำ และการทำสตรีมมิ่งก็คือ การค่อย ๆ เทน้ำลงไปในท่อ สิ่งที่คุณพยายามจะทำก็คือ ใช้แก้วน้ำ ตักน้ำจากถังแล้วเทลงไปในท่อจนกว่าน้ำในถังจะหมด โดยเงื่อนไขคือน้ำในท่อจะต้องไหลอย่างสม่ำเสมอและต่อเนื่อง ตราบใดที่น้ำยังไม่หมดถัง ลองคิดดูนะครับว่าทำได้หรือไม่ ให้เวลาคิด 5 นาทีครับ

เอาล่ะ … หมดเวลา 5 นาทีแล้ว คิดว่าน่าจะได้คำตอบกันละว่าคงทำไม่ได้ คือ คุณพอจะตบตากรรมการโดยการเติมน้ำด้วยความเร็วสูง แต่นั่นก็ลำบากมากใช่มั้ยครับ ??

แล้วถ้าเปลี่ยนใหม่เป็นมีแก้วน้ำสองใบล่ะ ??? เราตักน้ำไว้ก่อนใบหนึ่ง พอเริ่มเราก็เทน้ำในแก้วนี้ พร้อม ๆ กับตักน้ำใส่แก้วอีกใบนึงไว้ เมื่อน้ำใบที่เทอยู่หมดลง เราก็สลับ (ด้วยความเร็วสูง) เอาแก้วที่เพิ่งตักเมื่อกี้ค่อย ๆ เทลงไป แล้วก็เอาแก้วเปล่าไปตักน้ำต่อ ทำแบบนี้สลับไปมาเรื่อย ๆ จนกระทั่งน้ำหมดถัง

สองวิธีนี้แบบไหนง่ายกว่ากัน ผมคงไม่ต้องตอบ

การใช้บัฟเฟอร์คู่ ก็เหมือนกับการใช้แก้วสองใบ ก่อนจะเล่นเพลง เราก็โหลดข้อมูลก้อนเล็ก ๆ มาเก็บใส่บัฟเฟอร์แรกเอาไว้ เมื่อเริ่มเล่น เราก็เริ่มเล่นเสียงที่อยู่ในบัฟเฟอร์ไปพลาง โหลดข้อมูลก้อนต่อไปมาเก็บไปพลาง เมื่อเสียงในบัฟเฟอร์แรกหมด เราก็เล่นเสียงจากบัฟเฟอร์ที่สองต่อทันที และโหลดข้อมูลก้อนต่อไปมาเก็บใส่บัฟเฟอร์แรกไปในระหว่างที่เล่นเสียงจากบัฟเฟอร์ที่สอง ทำสลับกันไปเรื่อย ๆ จนกว่าจะจบครับ

แค่นี้เอง ไม่ยากกันจนเกินไปใช่มั้ยครับ 😉

เล่น file ogg แบบ stream ให้ loop เนียนไร้รอยต่อ (ผ่าน SDL)

คราวนี้ขอแปะแต่โค๊ดก่อนนะครับ อธิบายจะตามมาวันหลัง ง่วงแล้ว (ตีสองครึ่งแล้วนะ !!)

ที่ว่าแบบ stream ก็คือ จะไม่อ่านทีเดียวทั้งไฟล์ เก็บไว้ในเมมโมรี่ก่อน (มันเปลือง) แต่จะใช้วิธีค่อย ๆ บัฟเฟอร์ไฟล์มาเก็บไว้ แล้วค่อย ๆ เล่นทีละส่วน (ซึ่งเป็นเทคนิคทีเรียกว่าการทำ streaming)

หลักการคร่าว ๆ ก็คือ

  1. เติมข้อมูลทุก ๆ บัฟเฟอร์ เว้นบัฟเฟอร์สุดท้ายไว้ (จริง ๆ ไม่ต้องเว้นก็ได้ แต่ที่เว้นเพื่อความสะดวกในการเขียนโค้ด)
  2. เมื่อบัฟเฟอร์แรกถูกเล่น บัฟเฟอร์สุดท้ายจะถูกเติมข้อมูล
  3. เมื่อบัฟเฟอรฺ์แรกหมด จะมีการเรียก call back จากระบบมาบอกว่าเล่นบัฟเฟอร์หมดแล้ว ให้ก๊อปบัฟเฟอร์ที่สองใส่บัฟเฟอร์ระบบไป แล้วเติมบัฟเฟอร์แรกที่เล่นจบไปแล้ว
  4. ถ้าหากระหว่างเติมบัฟเฟอร์ ดันจบเพลงซะก่อน ก็ seek ไปยังตำแหน่งเริ่มต้น loop ใหม่ซะ แล้วบัฟเฟอร์ต่อจนเต็ม

เอาแค่นี้ก่อนละกัน เดี๋ยวค่อยมาแก้เพิ่มครับ 🙂

#include 
#include 

#include 
#include 

const int BUFFER_COUNT = 6;
const int NUM_SAMPLE = 4096*4;
const int BUFFER_SIZE = 2 * 2 * NUM_SAMPLE; //2 channels * 2byte per sample * NUM_SAMPLE

int current_buffer = 0;
char bgm_buffer[BUFFER_COUNT][BUFFER_SIZE];

const int loop_start = 291784;  // loop point in #sample, this is song-specific setting
								// and suits only for supplied ogg file.
								// Updates this value to test with other file.

SDL_mutex *mutex;
OggVorbis_File file;


// Macros for a lazy guy like me :P
#define if_error_display_then_exit(x, y)  
	if( (x) != 0 ) { std::cout<<(y)<

ต่อไปก็ Source Code กับตัวโปรแกรมนะครับ
Source
Binary

ตัวโปรแกรม (น่าจะ) ต้องใช้กับ VS 2008 Runtime นะครับ ถ้าไม่มีก็คงรันไม่ได้มั้ง ??? แต่ก็เอาโค๊ดไปคอมไพล์เองเป็นเวอร์ชั่นอื่นก็ได้ครับ