SoLoud - Game Audio Engine ที่ใช้งานง่าย (มาก) และ opensource สำหรับ C++
Tags: cpp, gamedev, gameaudio
Published: 2023-01-29 11:20:21

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

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

วันนี้จะขอแนะนำ [SoLoud](https://solhsa.com/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 แล้วเราสามารถเล่นเสียงได้โดยง่าย และสามารถควบคุมโวลุ่ม ควบคุมการแพน หรือแม้กระทั่งเพิ่มเอฟเฟคอย่างรีเวิร์ปเข้าไปได้ด้วย

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

```cpp

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);
}
```

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

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

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

## Demo Time!

อันนี้เป็นเดโมที่ผมเขียนไว้ ใช้ SDL2 กับ ImGUI ครับ สามารถไปดาวน์โหลดได้ที่ [Github](https://github.com/wutipong/soloud-sdl) [Release 0.1](https://github.com/wutipong/soloud-sdl/releases/tag/v0.1.0) มีแต่ Windows นะครับ

![Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/79ylh32v1uy8dxa5k6lv.png)

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

### SoLoud::Soloud

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

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

### SoLoud::AudioSource

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

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

```cpp
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);
```

* Wav เป็น Source ที่เก็บข้อมูลเสียง PCM จากไฟล์ทั้งไฟล์มาเก็บไว้ 
* WavStream ต่างกับ Wav ตรงที่ตัวมันเองจะอ่านข้อมูลแบสตรีมมิ่งจากไฟล์ระหว่างการเล่น ทำให้ใช้เมมโมรี่น้อยกว่า แต่ก็แลกมาด้วยจำนวน I/O Operation เพราะมันจะอ่านไปเล่นไป 
* Speech เป็นตัวสังเคราะห์เสียงพูดภาษาอังกฤษอย่างง่าย เอาไว้ทำเสียงพูดขำๆ
* SFXR ไลบราลีสำหรับสร้างซาวนด์เอฟเฟคด้วย synthesizer ตัว
* และอื่นๆ

### Bus

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

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

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

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

```cpp
soloud.setPause(sound_efx_bus_handle, true);
```

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

```cpp
soloud.setPause(sound_efx_bus_handle, false);
```

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

### Manipulation

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

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

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

```cpp
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.
```

### Filter

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

```cpp
source.setFilter(0, &lofi_filter);
                            
aBus.setFilter(0, &echo_filter);
```

## ข้อจำกัด

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

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

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

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

## ทิ้งท้าย

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

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

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