เล่น file ogg แบบ stream ให้ loopได้ ผ่าน SDL_mixer

อันนี้เหมือนจะเป็นตอนต่อจาก entry ที่เขียนไว้เมื่อเกือบ ๆ 7 ปีที่แล้วครับ

คือโค๊ดอันนั้นเป็นการแสดงว่ามันลูปได้จริง ๆ นะ แต่ปัญหาคือ SDL นั้นไม่มี mixer ในตัวครับ และโปรแกรมนั่นเล่นได้แค่เสียงเดียวเท่านั้น เรียกได้ว่าแทบไม่มีประโยชน์อะไร ... เอาเป็นว่าช่างมันก่อนละกัน วันนี้จะเสนออีกวิธีนึงที่น่าจะใช้ประโยชน์ได้มากกว่า

แต่ก่อนหน้านั้นขออธิบายเรื่องการเล่นไฟล์เสียงแบบ streaming ก่อน

การเล่นเสียงแบบ Streaming

โดยทั่วไปเวลาเราจะเล่นไฟล์เสียง เราจะโหลดข้อมูลทั้งไฟล์ขึ้นไปในบัฟเฟอร์ ในรูปแบบที่ตัว hardware หรือ api สามารถเล่นได้ (มักจะเป็น PCM - Pulse-Code Modulatation) ซึ่งปัญหาคือถ้าไฟล์เสียงเรายาวมาก ๆ มันจะใช้พื้นที่ในหน่วยความจำค่อนมาก โดยทั่วไปก็ราว ๆ 60MB ต่อนาที (คิดที่ PCM 44.1KHz 16bit Stereo ครับ) การโหลดข้อมูลใหญ่ ๆ เข้าในเมโมรีทีเดียวทั้งหมดอาจจะเยอะเกินไป อาจจะทำให้โปรแกรมไม่ทำงานได้

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

พักตรงนี้ก่อน โค๊ดตัวอย่างนี้ใช้ไฟล์จาก Persona 4 OST แต่ผมคงไม่สามารถแปะเพลงนี้ได้ ถ้าสนใจอยากลองฟัง ก็ลองดูคลิปนี้ละกันครับ (คนละเวอร์ชันกันครับ)

ปัญหาเรื่องการเล่นไฟล์เพลงให้ลูปใน SDL_mixer

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

auto music = Mix_LoadMUS("./songs/2.04. Heartbeat, Heartbreak.ogg");
Mix_PlayMusic(music, -1);

ซึ่งสิ่งที่มันทำมันก็เหมือนกับที่ผมเล่าไปข้างบนนั่นแหละ เราเลยไม่ต้องมากังวลเรื่องการเติมบัฟเฟอร์ การสลับบัฟเฟอร์ หรืออะไรทำนองนั้น

ปัญหาคือ มันเล่นตั้งแต่ต้นจนจบไฟล์ แล้วเริ่มที่จุดเริ่มต้นใหม่ครับ เราไม่สามารถกำหนดได้ว่าจะลูปจากตรงไหน และลูปไปตรงไหน

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

ทางแก้ สำหรับคนที่ยังอยากให้ SDL_mixer

บอกก่อนว่าถ้าไม่อยากใช้ SDL_mixer ก็ทำได้ครับ แต่นั่นคือต้องเขียน mixer เอง เหนื่อยน่าดู อีกวิธีคือการเปลี่ยนไปใช้ API อื่นอย่าง OpenAL เลย (ผมเคยเล่าให้ฟังเมื่อ 7 ปีที่แล้วล่ะมั้ง ลืมละ)

โชคดีอย่าง SDL_mixer มีกลไกที่อนุญาตให้เราเติมบัฟเฟอร์ได้เอง ซึ่งเราจะไปถอดรหัสด้วยวิธีไหนมาจากอะไรก็ได้ แล้วแต่เราเลย ตรงนี้จะเป็นการใช้คำสั่ง Mix_HookMusic() แทนนั่นเอง ปัญหาคือใช้แล้วใช้คำสั่ง Play/Stop ไม่ได้นะครับ เรียกปุ๊บมันเล่นเลย เราต้องเขียนกลไกสั่งให้มันหยุดเอง แต่คำสั่ง Pause/Resume ยังใช้ได้นะ อันนี้ก็ต้องลองกันเอาเองดูนะครับ

ข้างล่างเป็นโค๊ดตัวอย่างครับ

OggVorbis_File vf;
ov_fopen("./songs/2.04. Heartbeat, Heartbreak.ogg", &vf);

Mix_HookMusic([](void *udata, Uint8 *stream, int len) {
  OggVorbis_File *pvf = (OggVorbis_File *) udata;
  const long int start_loop = 447151L;
  const long int end_loop = 3410688L;
  int bitstream;
  int read_total = 0;
  while (true) {
    if (read_total >= len) break;
    auto pos = ov_pcm_tell(pvf);

    if (pos > end_loop)
      ov_pcm_seek(pvf, start_loop);

    int read = ov_read(pvf, (char *) stream + read_total,
                       len - read_total, 0, 2, 1, &bitstream);
    read_total += read;
  }

}, &vf);

จะเห็นว่าผมมี lambda ตัวหนึ่งในพารามิเตอร์ของ Mix_HookMusic() ฟังก์ชันนี้เป็น callback โดยทุกครั้งที่ตัว SDL_Mixer ต้องการข้อมูล มันจะเรียกฟังก์ชันนี้โดยให้บัฟเฟอร์เรามา เรามีหน้าที่ต้องเติมให้เต็มบัฟเฟอร์ครับ (อันนี้ต่างกับบาง API ที่เราสามารถเติมให้ไม่เต็มได้ แค่บอกว่าเราเติมไปเท่าไหร่) ส่วนเรื่องที่ว่ามันจะเอาบัฟเฟอร์ไปทำอะไรเป็นหน้าที่ของ SDL_Mixer เราไม่ต้องยุ่งเลย

ผมใช้ตำแหน่งแบบ pcm ซึ่งจะนับจำนวน sample จากจุดเริ่มต้นจนถึงตำแหน่งที่เรากำหนดไว้ ซึ่งจะแม่นยำกว่าการใช้ตำแหน่งแบบเวลา (เช่น วินาที) แต่การจะได้ตำแหน่งนี้มานั้นคงต้องคุยกับ sound design ดี ๆ ล่ะครับ เราสามารถหาตำแหน่งของลูปได้จากโปรแกรมแก้ไขไฟล์เสียง แล้วก็โปรแกรมพวก DAW ครับ (ไฟล์นี้ผมใช้ Sound Forge Audio Studio 10 ที่แถมมากับแลปท็อปผมครับ ... น่าเสียดายที่ Sony ขาย Vaio ไปแล้วจริง ๆ )

2016-01-12 05_13_34-Greenshot

วิธีนี้จะใช้วิธีการเช็คตำแหน่งที่เราอยู่ในปัจจุบันก่อนที่จะสั่ง decode โดยถ้าเราเล่นเกินตำแหน่งลูปไปแล้ว ก็ให้กรอกลับไปยังตำแหน่งเริ่มต้นใหม่ วิธีนี้ยังค่อนข้างหยาบอยู่ เพราะว่าเราเล่น "เกิน" กว่าตำแหน่งลูปก่อนจะวนกลับไป (ตรงนี้จำเป็นเพราะว่าเราถอดรหัสไฟล์ออกมาเป็นข้อมูลก้อนใหญ่ ๆ ก้อนหนึ่งครับ ไม่ใช่ว่าเราโหลดราย sample เลย ดังนั้นมันจะโหลดมาเกินกว่าที่เราตั้งไว้) เป็นสาเหตุว่าทำไมในหลาย ๆ lib ถึงไม่การันตีว่ามันจะลูปที่ตำแหน่งที่เราตั้งไว้ด้วยครับ เพราะมันถอดมาเกินนี่ล่ะ แต่เท่าที่ผมลองฟังดูผมไม่รู้สึกว่ามันมีรอยต่อครับ อาจจะเพราะว่าเลือกจุด loop point ได้ดีด้วย (เป็นจุดที่ amplitude ค่อนข้างต่ำครับ)

อ้อ ถ้าเกิดว่ามันลูปมาไม่เนียน เราสามารถปรับตำแหน่ง loop in/out ได้ครับ โค๊ดตรงนี้ทำงานค่อนข้างเหมือนเดิมในแต่ละครั้ง ถ้าเรามั่วค่าได้ดีรอยต่อมันจะหายไปเอง

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

สุดท้ายคือ วิธีนี้ใช้ custom callback ในการโหลดข้อมูลเข้าบัฟเฟอร์ การที่จะรองรับ file format ต่าง ๆ นั้นเราต้องทำเองหมดเลย (ต่างกับการเล่นไฟล์ด้วยกลไกของ SDL_mixer เอง) ถ้าเราต้องการรองรับหลาย ๆ ฟอร์แมทนี่อาจจะเหนื่อยหน่อยนะครับ :-) ส่วนตัวผมแนะนำว่าให้ใช้แค่ฟอร์แมทเดียวกับทั้งโปรเจคดีกว่า จัดการง่ายกว่าครับ

สุดท้ายทิ้งท้ายด้วยไฟล์โค๊ดตัวอย่าง ลองเข้าไปดูเล่น ๆ นะครับ

Wutipong Wongsakuldej

Programmer, interested in frontend applications, music and multimedia.

Latest posts by Wutipong Wongsakuldej (see all)

Leave a Reply

Your email address will not be published. Required fields are marked *