DEV Community

Cover image for SoLoud - Game Audio Engine ที่ใช้งานง่าย (มาก) และ opensource สำหรับ C++
Wutipong Wongsakuldej
Wutipong Wongsakuldej

Posted on

SoLoud - Game Audio Engine ที่ใช้งานง่าย (มาก) และ opensource สำหรับ C++

หลาย ๆ คนที่ยังเขียนเกมแบบเขียนเอนจินเอง น่าจะเคยเขียนส่วนที่เล่นเสียง ไม่ว่าจะใช้ DirectSound, XAudio, SDL2 แล้วรู้สึกว่า ทำไมมันช่างลำบากเหลือเกิน เล่นแค่ให้เสียงออกน่ะง่าย แต่ควบคุมให้มันทำงานได้ดังใจนี่มันช่างวุ่นวาย

ในทางกลับกัน บางคนก็อาจจะเคยได้จับ Engine เฉพาะ อย่าง AudioKinetic WWise หรือ FMOD มาก่อน แต่ก็สู้ราคาเอนจินไม่ไหว เพราะแพงจับใจ (ถึงไลเซนส์อินดี้จะฟรีก็ตาม)

วันนี้จะขอแนะนำ SoLoud มาเป็น Audio Engine อีกสักตัวนึง แต่ก่อนจะไปถึงขั้นนั้น มาดูกันก่อนว่า ทำไมเราถึงต้องมี Audio Engine หรือ Middleware หรือ Library หรืออะไรต่อมิอะไรที่เราเรียกกัน

ความจำเป็นของ Audio Engine

บางคนคงคิดว่า การเล่นเสียงมันไม่น่าจะยาก แค่สร้าง audio data แล้วเรียกฟังก์ชั่นของ OS มันก็เล่นให้ได้แล้ว ในความเป็นจริงคือ เวลาเราสร้าง audio device ขึ้นมาสักตัวนึง เราจะได้สตรีมของ audio มาสายนึง เวลาเราเล่นเสียง ตัว api ก็จะทำการเขียนข้อมูล PCM นี้ลงไปใน stream ที่ว่า

ถ้าเราเรียกฟังก์ชั่นที่ว่าหลาย ๆ ครั้งติด ๆ กัน ข้อมูลก่อนหน้าที่เคยเขียนลงไปก่อนหน้า จะถูกแทนที่ด้วยข้อมูลใหม่ที่ถูกเขียนลงไปทีหลัง นั่นหมายถึง เฉพาะเสียงสุดท้ายเท่านั้นที่จะถูกเล่นออกไป

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

ดังนั้น ฟังก์ชั่นหลักที่สำคัญที่สุดของ Audio Engine คือการผสมเสียงก็ว่าได้

ทีนี้ ด้วยความที่ว่า audio เป็นงานที่สัมพันธ์กับเวลา เราไม่สามารถเขียนข้อมูลลง stream เร็วไปหรือช้าไปได้ มันจะต่างกับงาน graphics ที่ เราสามารถวาดเฟรมใหม่ทันทีได้หลังจบเฟรม ถ้าเราทำแบบนั้นในงาน audio จะเกิด stutter คือเสียงมันถูกส่งมาเร็วเกินไปทำให้มีข้อมูลบางส่วนถูกข้ามไป หรืออาจจะส่งมาช้าเกินไปทำให้มีจังหวะที่เสียงเงียบได้อีกด้วย ดังนั้นงาน audio จะมีจังหวะของมันเองและไม่ขึ้นอยู่กับกราฟิค นอกจากนี้เสียงที่ถูกส่งเข้ามาเล่นอาจจะมีขนาดที่ใหญ่กว่าตัวบัฟเฟอร์ของสตรีมนั้นๆ ทำให้จะต้องเก็บข้อมูลบางส่วนเอาไว้ใช้ตอนเติม buffer ครั้งต่อ ๆ ไปหลังจากนั้นอีก

การเขียนโค๊ดที่ใช้ผสมเสียง หรือที่เรียกว่า mixer จึงเป็นงานที่ไม่ธรรมดา มีความซับซ้อนระดับหนึ่ง และยิ่งมีฟีเจอร์มากเท่าไหร่การสร้างก็จะยากขึ้นเท่านั้น

SoLoud คืออะไร ?

SoLoud เป็น Open Source, Cross-Platform Audio Engine ที่พัฒนาโดย Jari Komppa ตัวมันวางตัวยู่บน Audio Backend ต่าง ๆ เช่นพวก SDL2, ALSA, หรือ WinMM แล้วเราสามารถเล่นเสียงได้โดยง่าย และสามารถควบคุมโวลุ่ม ควบคุมการแพน หรือแม้กระทั่งเพิ่มเอฟเฟคอย่างรีเวิร์ปเข้าไปได้ด้วย

ทีนี้ ถ้าถามว่า ใช้ง่ายแค่ไหน ข้างล่างคือโค๊ดตัวอย่างครับ


int main() {
  SoLoud::SoLoud soloud;
  SoLoud::Wav sample;

  soloud.init();

  sample.load("sfx.wav");
  auto handle = soloud.play(sample);
  soloud.setVolume(handle, 0.5f);
  soloud.setPan(handle, -0.2f);
}
Enter fullscreen mode Exit fullscreen mode

แค่นี้ก็เล่นเสียงจากไฟล์ sfx.wav ไปออกทางซ้าย 20% และเสียงดัง 50% จากต้นฉบับ เรียบร้อย

และถ้าเราเล่นไฟล์หลาย ๆ ไฟล์พร้อม ๆ กัน SoLoud จะผสมเสียงให้เอง เราไม่ต้องเขียนฟังก์ชั่นสำหรับผสมเสียงละ

ลักษณะเฉพาะนึงของ SoLoud คือ Library นี้จัดการเรื่อง lifetime ได้ดี ถ้าสังเกตคือตัว interface แทบจะไม่มี pointer เลย ทั้ง ๆ ที่เป็น library ที่ผู้ใช้สามารถขยายความสามารถได้ง่าย อันนี้เกิดจาก Library ออกแบบมาให้เราจัดการ state ทั้งหมดผ่าน stack variable ทำให้เกิด memory leak จากฝั่งคนใช้ได้ยากขึ้น

Demo Time!

อันนี้เป็นเดโมที่ผมเขียนไว้ ใช้ SDL2 กับ ImGUI ครับ สามารถไปดาวน์โหลดได้ที่ Github Release 0.1 มีแต่ Windows นะครับ

Screenshot

เนื่องด้วยจากความขี้เกียจของผม ผมจึงใส่เกือบทั้งหมดลงไปในใน main.cpp ยกเว้นส่วนที่เป็น file กับ joystick อันนี้ไม่ต้องไปทำความเข้าใจมากก็ได้ครับ อย่างตัว FileSystemFile นี่สามารถใช้ SoLoud::File หรือไปใช้พวกฟังก์ชั่นที่อ่านไฟล์ได้เองตรง ๆ ก็ได้ครับ ที่ผมเขียนคลาสนั้นขึ้นมานี่ เขียนมาเพื่อรอง std::filesystem::path แค่นั้นเลย

SoLoud::Soloud

คลาส Soloud นี้เป็นคลาสหลักของตัว Engine เป็นเหมือนตัว root level device ที่เวลาที่เราจะทำอะไรกับ SoLoud คลาสนี้แหละที่เราจะไปยุ่งด้วย

คือจะมองว่าเป็นตัวเล่นเสียง + ตัวผสมเสียงในตัวเดียวก็ได้ครับ

SoLoud::AudioSource

AudioSource ก็คือคลาสที่เป็นแหล่งกำเนิดเสียง ไม่ว่าจะเล่นจากไฟล์ หรือจะสังเคราะห์ขึ้นมาเอง

ตัว SoLoud มีคลาสชุด AudioSource ติดมาให้จำนวนหนึ่ง ข้างล่างนี้เป็นตัวอย่างบางส่วนครับ

SoLoud::Wav sample;
sample.load("sfx.wav");

SoLoud::WavStream bgm;
bgm.load("bgm01.mp3");

SoLoud::Speech speech;
speech.setText("Hello World", 11);

SoLoud::Sfxr sfxr;
sfxr.loadPreset(SoLoud::Sfxr::COIN, 654'321);

soloud.play(sample);
soloud.play(bgm);
soloud.play(speech);
soloud.play(sfxr);
Enter fullscreen mode Exit fullscreen mode
  • Wav เป็น Source ที่เก็บข้อมูลเสียง PCM จากไฟล์ทั้งไฟล์มาเก็บไว้
  • WavStream ต่างกับ Wav ตรงที่ตัวมันเองจะอ่านข้อมูลแบสตรีมมิ่งจากไฟล์ระหว่างการเล่น ทำให้ใช้เมมโมรี่น้อยกว่า แต่ก็แลกมาด้วยจำนวน I/O Operation เพราะมันจะอ่านไปเล่นไป
  • Speech เป็นตัวสังเคราะห์เสียงพูดภาษาอังกฤษอย่างง่าย เอาไว้ทำเสียงพูดขำๆ
  • SFXR ไลบราลีสำหรับสร้างซาวนด์เอฟเฟคด้วย synthesizer ตัว
  • และอื่นๆ

Bus

Bus เป็น Audio Source ที่พิเศษตรงที่ตัวมันเองจะรับเสียงจาก Audio Source อื่น ๆ ก่อนจะส่งไปที่ตัว Soloud object

Bus ทำตัวเป็น submix ที่เราสามารถนำเสียงที่อยู่ในกลุ่มเดียวกันมารวมกันในจุดเดียว เพื่อที่จะควบคุมเสียงพร้อ ๆ กันได้ในทีเดียว

soloud.play(aBus);
for(int i = 0; i< sources.size(); i++){
    aBus.play3d(sources[i], positions[i].x, positions[i].y, positions[i].z);
}
Enter fullscreen mode Exit fullscreen mode

ยกตัวอย่างเช่น เกมอย่าง Rockman/Megaman X จะมีเสียงซาวนด์เอฟเฟคต่างๆ และเสียงเพลง background music เมื่อเรากด pause เสียงซาวนด์เอฟเฟคทั้งหมดจะหยุดชั่วคราว เสียง background music จะเบาลง และเมนูจะแสดงขึ้นมา เมื่อเรากดปุ่ม ซาวนด์เอฟเฟคในหน้าเมนูก็จะดังตามปรกติ

soloud.setPause(sound_efx_bus_handle, true);
Enter fullscreen mode Exit fullscreen mode

เมื่อเรากดปิดเมนู เสียงซาวนด์เอฟคเดิมจะเล่นต่อจากจุดที่หยุดชั่วคราว เสียง background music จะกลับมาดังเหมือนเดิม และเสียงเมนูก็จะหายไป

soloud.setPause(sound_efx_bus_handle, false);
Enter fullscreen mode Exit fullscreen mode

ตรงเราสามารถทำได้โดยการให้ sound effect ในเกมรันอยู่บน bus หนึ่ง แล้วเรา play/pause ตัวบัสนี้เมื่อกด pause เข้าเมนู/ออกจากเมนู โดยที่เราไม่ต้องไปหยุด sound source ย่อย ๆ ทีละตัว

Manipulation

สำหรับ Soloud เราสามารถสั่งเล่น audio source เดียวกันหลาย ๆ ครั้งพร้อม ๆ กันได้ ยกตัวอย่างคือเสียงปืน ที่มักจะเป็นเสียงเดียวกัน และอาจจะเล่นพร้อม ๆ กันหรือเล่นต่อ ๆ กันได้

ทีนี้ แล้วเราจะควบคุมเสียงที่เล่นออกมาแล้วได้อย่างไร? เวลาที่เราเล่นเสียง ฟังก์ชั่น play() จะคืนค่า integer ออกมาตัวหนึ่ง ที่เรียกว่า handle

เราสามารถใช้ handle ไปควบคุมเสียงที่เล่นออกมาแล้วได้ เช่นอาจจะแพนซ้ายขวา หรือตั้งค่าความดัง เป็นต้นครับ

SoLoud::Wav wav;
wav.load("gunshot.wav");

auto handle = soloud.play(wav);
soloud.setVolume(handle, 0.7f);
soloud.setPan(handle, 0.2f); // 20% to the right.
Enter fullscreen mode Exit fullscreen mode

Filter

filter เป็นเหมือนเอฟเฟคที่ทำงานคู่กับ audio source เช่น เราอาจจะอยากให้เสียงพูดมีลักษณะเหมือนเสียงหุ่นยนต์ เราอาจจะอยากให้เสียงเพลงมีลัษณะเหมือน lofi เราอาจจะอยากให้เสียงที่วิ่งผ่านบัสซาวนด์เอฟเฟคมีเสียงรีเวิร์ป ตรงนี้เราสามารถทำได้หมดเลย และง่ายมากด้วย

source.setFilter(0, &lofi_filter);

aBus.setFilter(0, &echo_filter);
Enter fullscreen mode Exit fullscreen mode

ข้อจำกัด

SoLoud ติดต่อกับ backend ด้วยโค๊ดที่เลเวลล่างมาก ตัวมันเองคำนวนตำแหน่งสามมิติด้วยตัวมันเอง แทนที่จะให้ backend คำนวนให้ ทำให้ไม่สามารถใช้ศักยภาพของแพลตฟอร์มได้ดีนัก อย่างพวก 3D Binaural Audio ด้วยการใช้ฮาร์ดแวร์นี่ทำไม่ได้ และตัวมันเองก็ยังไม่รองรับ HRTF ด้วย

อุปสรรคในการใช้งาน

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

ทั้งนี้เค้ามีก็ premake script ให้แหละ แต่กับคนที่ไม่ใช้ premake ก็ไม่มีประโยชน์อะไรเท่าไหร่

ทิ้งท้าย

สำหรับคนที่ยังเขียน C++ อยู่ และอยากจะ modernize โค๊ดตัวเอง เอาโค๊ดของตัวเองที่อาจจะดูและไม่ดีนักออก แล้วเอาโค๊ดที่ดูแลดีกว่าของคนอื่น (ที่เป็นโปรเจคที่ร่วมกันทำงานหลายคน และร่วมกันเป็นเจ้าของ) มาใช้แทน สำหรับภาค Game Audio ผมว่า SoLoud นี่ก็ถือว่าตอบโจทย์ได้ดีทีเดียวครับ ใช้งานง่าย ลูกเล่นเยอะ แถมทำงานได้หลายแพลตฟอร์มอีก

ส่วนตัวผมรู้จัก SoLoud จากการไปลองศึกษา The Forge ที่เป็นเหมือนเฟรมเวิร์คสำหรับเขียนเกม ถูกใช้ในเกมที่วางขายแล้วจำนวนหนึ่ง รวมทั้ง AAA อย่าง Starfield และ No Man Sky คิดว่า โค๊ดที่ถูกใช้ในโครงการระดับนี้ก็น่าจะถูกคัดกรองมาดีระดับหนึ่งแล้วครับ

อันนี้ใครสนใจก็ลองหามาเล่นดูได้ครับผม

Latest comments (0)