เพิ่มเสียงและเพลงให้กับเกม Flutter

1. ก่อนเริ่มต้น

เกมคือประสบการณ์ภาพและเสียง Flutter เป็นเครื่องมือที่ยอดเยี่ยมในการสร้างภาพที่สวยงามและ UI ที่มีประสิทธิภาพ ซึ่งจะช่วยให้คุณเห็นภาพของสิ่งต่างๆ ได้ชัดเจนยิ่งขึ้น ส่วนประกอบที่หายไปทางซ้ายคือเสียง ใน Codelab นี้ คุณจะได้เรียนรู้วิธีใช้ปลั๊กอิน flutter_soloud เพื่อแนะนำเสียงและเพลงที่มีเวลาในการตอบสนองต่ำให้แก่โปรเจ็กต์ของคุณ คุณเริ่มต้นด้วยโครงข้อแข็งพื้นฐานเพื่อให้ข้ามไปยังส่วนที่น่าสนใจได้ทันที

ภาพหูฟังแบบวาดด้วยมือ

แน่นอนว่าคุณสามารถใช้สิ่งที่เรียนรู้ที่นี่เพื่อเพิ่มเสียงลงในแอป ไม่ใช่เฉพาะเกม แม้ว่าเกมเกือบทุกเกมจะต้องใช้เสียงและเพลง แต่แอปส่วนใหญ่ไม่ต้องใช้ ดังนั้น Codelab นี้จึงมุ่งเน้นที่เกม

ข้อกำหนดเบื้องต้น

  • มีความคุ้นเคยกับ Flutter ในระดับพื้นฐาน
  • ความรู้เกี่ยวกับวิธีเรียกใช้และแก้ไขข้อบกพร่องของแอป Flutter

สิ่งที่ได้เรียนรู้

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

สิ่งที่ต้องมี

  • Flutter SDK
  • ตัวแก้ไขโค้ดที่คุณต้องการ

2. ตั้งค่า

  1. ดาวน์โหลดไฟล์ต่อไปนี้ หากการเชื่อมต่อช้า ก็ไม่ต้องกังวล คุณต้องใช้ไฟล์จริงในภายหลังเพื่อให้ดาวน์โหลดได้ขณะทํางาน
  1. สร้างโปรเจ็กต์ Flutter โดยตั้งชื่อตามต้องการ
  1. สร้างไฟล์ lib/audio/audio_controller.dart ในโปรเจ็กต์
  2. ให้ป้อนรหัสต่อไปนี้ในไฟล์

lib/audio/audio_controller.dart

import 'dart:async';

import 'package:logging/logging.dart';

class AudioController {
  static final Logger _log = Logger('AudioController');

  Future<void> initialize() async {
    // TODO
  }

  void dispose() {
    // TODO
  }

  Future<void> playSound(String assetKey) async {
    _log.warning('Not implemented yet.');
  }

  Future<void> startMusic() async {
    _log.warning('Not implemented yet.');
  }

  void fadeOutMusic() {
    _log.warning('Not implemented yet.');
  }

  void applyFilter() {
    // TODO
  }

  void removeFilter() {
    // TODO
  }
}

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

  1. จากนั้นให้เปิดไฟล์ lib/main.dart และแทนที่เนื้อหาด้วยโค้ดต่อไปนี้

lib/main.dart

import 'dart:developer' as dev;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';

import 'audio/audio_controller.dart';

void main() async {
  // The `flutter_soloud` package logs everything
  // (from severe warnings to fine debug messages)
  // using the standard `package:logging`.
  // You can listen to the logs as shown below.
  Logger.root.level = kDebugMode ? Level.FINE : Level.INFO;
  Logger.root.onRecord.listen((record) {
    dev.log(
      record.message,
      time: record.time,
      level: record.level.value,
      name: record.loggerName,
      zone: record.zone,
      error: record.error,
      stackTrace: record.stackTrace,
    );
  });

  WidgetsFlutterBinding.ensureInitialized();

  final audioController = AudioController();
  await audioController.initialize();

  runApp(
    MyApp(audioController: audioController),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({required this.audioController, super.key});

  final AudioController audioController;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter SoLoud Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.brown),
        useMaterial3: true,
      ),
      home: MyHomePage(audioController: audioController),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.audioController});

  final AudioController audioController;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const _gap = SizedBox(height: 16);

  bool filterApplied = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter SoLoud Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            OutlinedButton(
              onPressed: () {
                widget.audioController.playSound('assets/sounds/pew1.mp3');
              },
              child: const Text('Play Sound'),
            ),
            _gap,
            OutlinedButton(
              onPressed: () {
                widget.audioController.startMusic();
              },
              child: const Text('Start Music'),
            ),
            _gap,
            OutlinedButton(
              onPressed: () {
                widget.audioController.fadeOutMusic();
              },
              child: const Text('Fade Out Music'),
            ),
            _gap,
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Text('Apply Filter'),
                Checkbox(
                  value: filterApplied,
                  onChanged: (value) {
                    setState(() {
                      filterApplied = value!;
                    });
                    if (filterApplied) {
                      widget.audioController.applyFilter();
                    } else {
                      widget.audioController.removeFilter();
                    }
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
  1. หลังจากดาวน์โหลดไฟล์เสียงแล้ว ให้สร้างไดเรกทอรีในรูทของโปรเจ็กต์ชื่อ assets
  2. ในไดเรกทอรี assets ให้สร้างไดเรกทอรีย่อย 2 รายการ โดยรายการหนึ่งชื่อ music และอีกรายการเรียกว่า sounds
  3. ย้ายไฟล์ที่ดาวน์โหลดไว้ไปยังโปรเจ็กต์เพื่อให้ไฟล์เพลงอยู่ในไฟล์ assets/music/looped-song.ogg และมีเสียงที่ดูต่อในไฟล์ต่อไปนี้
  • assets/sounds/pew1.mp3
  • assets/sounds/pew2.mp3
  • assets/sounds/pew3.mp3

โครงสร้างโปรเจ็กต์ควรมีลักษณะดังนี้

มุมมองแบบโครงสร้างของโปรเจ็กต์ โดยมีโฟลเดอร์ต่างๆ เช่น &quot;android&quot;, &quot;ios&quot; รวมถึงไฟล์ เช่น &quot;README.md&quot; และ &quot;analysis_options.yaml&quot; เราจะเห็นไดเรกทอรี &quot;assets&quot; ที่มีไดเรกทอรีย่อย &quot;music&quot; และ &quot;sounds&quot; ไดเรกทอรี &quot;lib&quot; ที่มี &quot;main.dart&quot; และไดเรกทอรีย่อย &quot;audio&quot; ที่มี &quot;audio_controller.dart&quot; และไฟล์ &quot;pubspec.yaml&quot;  ลูกศรจะชี้ไปที่ไดเรกทอรีใหม่และไฟล์ที่คุณแตะ

เมื่อนำไฟล์ต่างๆ ออกแล้ว คุณจะต้องแจ้งให้ Flutter ทราบเกี่ยวกับไฟล์เหล่านั้น

  1. เปิดไฟล์ pubspec.yaml แล้วแทนที่ส่วน flutter: ที่ด้านล่างของไฟล์ด้วยข้อมูลต่อไปนี้

pubspec.yaml

...

flutter:
  uses-material-design: true

  assets:
    - assets/music/
    - assets/sounds/
  1. เพิ่มทรัพยากร Dependency ในแพ็กเกจ flutter_soloud และแพ็กเกจ logging

pubspec.yaml

...

dependencies:
  flutter:
    sdk: flutter

  flutter_soloud: ^2.0.0
  logging: ^1.2.0

...
  1. เรียกใช้โปรเจ็กต์ ยังไม่มีสิ่งใดใช้งานได้เนื่องจากคุณเพิ่มฟังก์ชันในส่วนต่อไปนี้

10f0f751c9c47038.png

/flutter_soloud/src/filters/filters.cpp:21:24: warning: implicit conversion loses integer precision: 'decltype(__x.base() - __y.base())' (aka 'long') to 'int' [-Wshorten-64-to-32];

รหัสเหล่านี้มาจากไลบรารี C++ ที่สำคัญของ SoLoud โดยไม่ส่งผลต่อฟังก์ชันการทำงานและเพิกเฉยได้อย่างปลอดภัย

3. เริ่มต้นและปิด

คุณต้องใช้ปลั๊กอิน flutter_soloud ในการเล่นเสียง ปลั๊กอินนี้อิงตามโปรเจ็กต์ SoLoud ซึ่งเป็นเครื่องมือเสียง C++ สำหรับเกมที่ใช้และรวมถึงเกมอื่นๆ จาก Nintendo SNES Classic

7ce23849b6d0d09a.png

หากต้องการเริ่มต้นเครื่องมือเสียง SoLoud ให้ทำตามขั้นตอนต่อไปนี้

  1. ในไฟล์ audio_controller.dart ให้นำเข้าแพ็กเกจ flutter_soloud และเพิ่มช่อง _soloud ส่วนตัวในชั้นเรียน

lib/audio/audio_controller.dart

import 'dart:ui';

import 'package:flutter_soloud/flutter_soloud.dart';  // ← Add this...
import 'package:logging/logging.dart';

class AudioController {
  static final Logger _log = Logger('AudioController');

  SoLoud? _soloud;                                    // ← ... and this.

  Future<void> initialize() async {
    // TODO
  }

  ...

ตัวควบคุมเสียงจะจัดการเครื่องมือ SoLoud ที่สำคัญผ่านช่องนี้และจะโอนสายทั้งหมดไปยังเครื่องมือ SoLoud ดังกล่าว

  1. ให้ป้อนรหัสต่อไปนี้ในเมธอด initialize()

lib/audio/audio_controller.dart

...

  Future<void> initialize() async {
    _soloud = SoLoud.instance;
    await _soloud!.init();
  }

...

ระบบจะเติมข้อมูลในช่อง _soloud และรอการเริ่มต้น โปรดทราบดังต่อไปนี้

  • SoLoud มีฟิลด์ instance แบบ Singleton คุณไม่สามารถสร้างอินสแตนซ์ SoLoud หลายรายการได้ นี่ไม่ใช่สิ่งที่เครื่องมือ C++ อนุญาต ปลั๊กอินของ Dart จึงไม่อนุญาตเช่นกัน
  • การเริ่มต้นปลั๊กอินเป็นแบบไม่พร้อมกันและจะไม่เสร็จสิ้นจนกว่าเมธอด init() จะกลับมา
  • เพื่อความกระชับในตัวอย่างนี้ คุณไม่ตรวจพบข้อผิดพลาดในบล็อก try/catch ในโค้ดเวอร์ชันที่ใช้งานจริง คุณควรดำเนินการและรายงานข้อผิดพลาดให้ผู้ใช้ทราบ
  1. ให้ป้อนรหัสต่อไปนี้ในเมธอด dispose()

lib/audio/audio_controller.dart

...

  void dispose() {
    _soloud?.deinit();
  }

...

การปิด SoLoud ขณะออกจากแอปถือเป็นแนวทางปฏิบัติที่ดี แม้ว่าทุกอย่างควรทำงานได้ตามปกติ แม้ว่าคุณจะละเลยก็ตาม

  1. โปรดสังเกตว่ามีการเรียกเมธอด AudioController.initialize() จากฟังก์ชัน main() แล้ว ซึ่งหมายความว่าการรีสตาร์ทโปรเจ็กต์ร้อนจะเป็นการเริ่มต้น SoLoud ในเบื้องหลัง แต่จะไม่ส่งผลใดๆ ก่อนที่จะเล่นเสียงบางอย่างจริง

4. เปิดเสียงแบบช็อตเดียว

โหลดเนื้อหาและเล่น

เมื่อทราบว่า SoLoud เริ่มทำงานเมื่อเริ่มต้นใช้งานแล้ว คุณก็ขอให้เล่นเสียงได้

SoLoud จะแยกความแตกต่างระหว่างแหล่งที่มาของเสียงซึ่งเป็นข้อมูลและข้อมูลเมตาที่ใช้อธิบายเสียงกับ "อินสแตนซ์เสียง" ซึ่งเป็นเสียงที่เล่นจริงๆ ตัวอย่างแหล่งที่มาของเสียงอาจเป็นไฟล์ mp3 ที่โหลดลงในหน่วยความจำ พร้อมเล่น และแสดงโดยอินสแตนซ์ของคลาส AudioSource ทุกครั้งที่คุณเล่นแหล่งที่มาของเสียงนี้ SoLoud จะสร้าง "อินสแตนซ์เสียง" ขึ้นมา ซึ่งจะแสดงด้วยประเภท SoundHandle

คุณจะได้รับอินสแตนซ์ AudioSource โดยการโหลดอินสแตนซ์ดังกล่าว เช่น หากคุณมีไฟล์ mp3 ในเนื้อหา คุณสามารถโหลดไฟล์เพื่อรับ AudioSource ได้ จากนั้นบอกให้ SoLoud เปิด AudioSource นี้ โดยคุณจะเล่นได้หลายครั้ง แม้จะพร้อมกัน

เมื่อใช้แหล่งที่มาของเสียงเสร็จแล้ว ให้กำจัดแหล่งที่มาโดยใช้เมธอด SoLoud.disposeSource()

โปรดทำตามขั้นตอนต่อไปนี้เพื่อโหลดเนื้อหาและเล่น

  1. ในเมธอด playSound() ของชั้นเรียน AudioController ให้ป้อนรหัสต่อไปนี้

lib/audio/audio_controller.dart

  ...

  Future<void> playSound(String assetKey) async {
    final source = await _soloud!.loadAsset(assetKey);
    await _soloud!.play(source);
  }

  ...
  1. บันทึกไฟล์ แล้ว Hot โหลดซ้ำ จากนั้นเลือกเล่นเสียง คุณควรได้ยินเสียงม้านั่งตลกๆ โปรดทราบดังต่อไปนี้
  • อาร์กิวเมนต์ assetKey ที่ระบุมีลักษณะอย่างเช่น assets/sounds/pew1.mp3 ซึ่งเป็นสตริงเดียวกับที่คุณระบุให้กับ Flutter API อื่นๆ ที่โหลดเนื้อหา เช่น วิดเจ็ต Image.asset()
  • อินสแตนซ์ SoLoud ให้เมธอด loadAsset() ที่โหลดไฟล์เสียงจากชิ้นงานของโปรเจ็กต์ Flutter แบบไม่พร้อมกันและแสดงผลอินสแตนซ์ของคลาส AudioSource มีวิธีที่เหมือนกันในการโหลดไฟล์จากระบบไฟล์ (เมธอด loadFile()) และการโหลดผ่านเครือข่ายจาก URL (เมธอด loadUrl())
  • จากนั้นระบบจะส่งอินสแตนซ์ AudioSource ที่เพิ่งได้รับไปยังเมธอด play() ของ SoLoud เมธอดนี้แสดงผลอินสแตนซ์ของประเภท SoundHandle ที่แสดงถึงเสียงที่กำลังเล่นใหม่ ในทางกลับกัน แฮนเดิลนี้สามารถส่งผ่านไปยังวิธีการ SoLoud อื่นๆ เพื่อทำสิ่งต่างๆ เช่น หยุดชั่วคราว หยุด หรือแก้ไขระดับเสียงได้
  • แม้ว่า play() จะเป็นเมธอดแบบไม่พร้อมกัน แต่โดยทั่วไปการเล่นจะเริ่มต้นทันที แพ็กเกจ flutter_soloud ใช้อินเทอร์เฟซฟังก์ชันต่างชาติ (FFI) ของ Dart เพื่อเรียกโค้ด C โดยตรงและพร้อมกัน การรับส่งข้อความตามปกติระหว่างโค้ด Dart กับโค้ดแพลตฟอร์มซึ่งเป็นลักษณะเฉพาะของปลั๊กอิน Flutter ส่วนใหญ่นั้นไม่พบเลย เหตุผลเดียวที่บางเมธอดเป็นแบบอะซิงโครนัสคือโค้ดของปลั๊กอินบางอย่างทำงานในโหมดแยกของตัวเองและการสื่อสารระหว่างการแยกของ Dart จะเป็นแบบไม่พร้อมกัน
  • คุณเพียงแค่ยืนยันว่าฟิลด์ _soloud เป็นค่าว่างด้วย _soloud! เพื่อความกระชับ โค้ดเวอร์ชันที่ใช้งานจริงควรจัดการกับสถานการณ์นี้ได้อย่างราบรื่นเมื่อนักพัฒนาซอฟต์แวร์พยายามเล่นเสียงก่อนที่ตัวควบคุมเสียงจะมีโอกาสเริ่มต้นอย่างสมบูรณ์

จัดการกับข้อยกเว้น

คุณอาจสังเกตเห็นว่าคุณไม่สนใจข้อยกเว้นที่เป็นไปได้อีกแล้ว มาลองแก้ไขวิธีนี้เพื่อวัตถุประสงค์ในการเรียนรู้กัน (เพื่อความกระชับ Codelab จะกลับไปละเว้นข้อยกเว้นหลังจากส่วนนี้)

  • หากต้องการจัดการข้อยกเว้นในกรณีนี้ ให้รวม 2 บรรทัดของเมธอด playSound() ไว้ในบล็อก try/catch และตรวจจับเฉพาะอินสแตนซ์ของ SoLoudException

lib/audio/audio_controller.dart

  ...

  Future<void> playSound(String assetKey) async {
    try {
      final source = await _soloud!.loadAsset(assetKey);
      await _soloud!.play(source);
    } on SoLoudException catch (e) {
      _log.severe("Cannot play sound '$assetKey'. Ignoring.", e);
    }
  }

  ...

SoLoud จะแสดงข้อยกเว้นหลายรายการ เช่น ข้อยกเว้น SoLoudNotInitializedException หรือ SoLoudTemporaryFolderFailedException เอกสาร API ของแต่ละเมธอดจะแสดงประเภทของข้อยกเว้นที่อาจมี

SoLoud ยังมีคลาสระดับบนสุดสำหรับข้อยกเว้นทั้งหมดของคลาส SoLoudException เพื่อให้คุณสามารถตรวจจับข้อผิดพลาดทั้งหมดที่เกี่ยวข้องกับฟังก์ชันการทำงานของเครื่องมือเสียงได้ ซึ่งจะเป็นประโยชน์อย่างยิ่งในกรณีที่การเล่นเสียงไม่สำคัญ ตัวอย่างเช่น เมื่อคุณไม่ต้องการให้เซสชันเกมของผู้เล่นขัดข้องเพียงเพราะไม่สามารถโหลดเสียงพิว-พิวได้ 1 เสียง

ตามที่คาดหวังไว้ เมธอด loadAsset() อาจแสดงข้อผิดพลาด FlutterError ได้หากคุณระบุคีย์ของชิ้นงานที่ไม่มีอยู่ โดยทั่วไปแล้ว การพยายามโหลดเนื้อหาที่ไม่ได้พ่วงกับเกมคือสิ่งที่คุณควรแก้ไข ดังนั้นกรณีนี้จึงเป็นข้อผิดพลาด

เล่นเสียงอื่น

คุณอาจสังเกตเห็นว่าคุณเล่นเฉพาะไฟล์ pew1.mp3 เท่านั้น แต่มีเสียงอื่นๆ อีก 2 เวอร์ชันในไดเรกทอรีเนื้อหา ซึ่งมักฟังดูเป็นธรรมชาติมากขึ้นเมื่อเกมมีเสียงเดียวกันหลายเวอร์ชัน และเล่นหลายๆ เวอร์ชันแบบสุ่มหรือเล่นวนไปเรื่อยๆ เพื่อป้องกันไม่ให้เสียงฝีเท้าและกระสุนปืนไม่ดูสม่ำเสมอเกินไปจนถือเป็นรูปแบบปลอม

  • สำหรับแบบฝึกหัดที่ไม่บังคับ ให้คุณแก้ไขโค้ดเพื่อให้เล่นเสียงที่แตกต่างกันทุกครั้งที่มีการแตะปุ่ม

ภาพประกอบของ

5. เล่นวนซ้ำเพลง

จัดการเสียงที่ทำงานนานขึ้น

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

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

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

lib/audio/audio_controller.dart

...

class AudioController {
  static final Logger _log = Logger('AudioController');

  SoLoud? _soloud;

  SoundHandle? _musicHandle;    // ← Add this.

  ...

เริ่มเล่นเพลง

ตามหลักแล้ว การเล่นเพลงไม่แตกต่างจากการเล่นเสียงแบบช็อตเดียว คุณยังต้องโหลดไฟล์ assets/music/looped-song.ogg เป็นอินสแตนซ์ของคลาส AudioSource ก่อน จากนั้นจึงใช้เมธอด play() ของ SoLoud เพื่อเล่น

แต่คราวนี้คุณจะจับแฮนเดิลเสียงที่เมธอด play() กลับมาใช้เพื่อจัดการเสียงขณะที่กำลังเล่นอยู่ได้

  • คุณใช้เมธอด AudioController.startMusic() ด้วยตนเองได้หากต้องการ ไม่ต้องกังวลหากรายละเอียดบางอย่างไม่ถูกต้อง โปรดทราบว่าเพลงจะเริ่มต้นเมื่อคุณเลือกเริ่มเพลง

ต่อไปนี้คือการใช้งานข้อมูลอ้างอิง

lib/audio/audio_controller.dart

...

  Future<void> startMusic() async {
    if (_musicHandle != null) {
      if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
        _log.info('Music is already playing. Stopping first.');
        await _soloud!.stop(_musicHandle!);
      }
    }
    final musicSource = await _soloud!
        .loadAsset('assets/music/looped-song.ogg', mode: LoadMode.disk);
    _musicHandle = await _soloud!.play(musicSource);
  }

...

โปรดทราบว่าคุณโหลดไฟล์เพลงในโหมดดิสก์ (enum ของ LoadMode.disk) ซึ่งหมายความว่าระบบจะโหลดไฟล์เป็นส่วนๆ ตามที่ต้องการเท่านั้น สำหรับเสียงที่เล่นได้ยาวนานขึ้น โดยทั่วไปแล้วมักจะโหลดในโหมดดิสก์ สำหรับเอฟเฟกต์เสียงสั้นๆ การโหลดและบีบอัดลงในหน่วยความจำจะเหมาะสมกว่า (enum ของ LoadMode.memory เริ่มต้น)

แต่คุณมีปัญหาอยู่ 2 ข้อ ข้อแรก เพลงดังเกินไป ทำให้เสียงดังเกินไป ในเกมส่วนใหญ่ เพลงมักจะเล่นเป็นแบ็กกราวน์เกือบตลอดเวลา ทำให้ได้เสียงที่เป็นสาระสำคัญมากขึ้นอย่างเสียงพูดและเอฟเฟกต์เสียง ในเวทีสำคัญ ซึ่งแก้ไขได้ง่ายๆ โดยใช้พารามิเตอร์ระดับเสียงของวิธีการเล่น เช่น คุณลอง _soloud!.play(musicSource, volume: 0.6) เพื่อเปิดเพลงที่ระดับเสียง 60% ได้ หรือจะตั้งค่าระดับเสียงภายหลังโดยใช้ข้อความอย่าง _soloud!.setVolume(_musicHandle, 0.6) ก็ได้

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

88d2c57fffdfe996.png

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

โชคดีที่ SoLoud มีวิธีเล่นเสียงวนซ้ำ เมธอด play() จะใช้ค่าบูลีนสำหรับพารามิเตอร์ looping และค่าสำหรับจุดเริ่มต้นของการวนซ้ำเป็นพารามิเตอร์ loopingStartAt โค้ดที่ได้จะมีลักษณะดังนี้

lib/audio/audio_controller.dart

...

_musicHandle = await _soloud!.play(
  musicSource,
  volume: 0.6,
  looping: true,
  // ↓ The exact timestamp of the start of the loop.
  loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
);

...

หากคุณไม่ได้ตั้งค่าพารามิเตอร์ loopingStartAt พารามิเตอร์จะมีค่าเริ่มต้นเป็น Duration.zero (กล่าวคือ จุดเริ่มต้นของไฟล์เสียง) ถ้าคุณมีแทร็กเพลงที่เล่นวนซ้ำแบบแนบเนียนโดยไม่มีบทนำ นี่คือสิ่งที่คุณต้องใช้

  • ฟังสตรีม allInstancesFinished ที่แต่ละแหล่งเสียงมีให้ เพื่อให้แน่ใจว่ามีการกำจัดแหล่งที่มาของเสียงอย่างเหมาะสมเมื่อเล่นจบแล้ว เมื่อเพิ่มการโทรในบันทึก เมธอด startMusic() จะมีลักษณะดังนี้

lib/audio/audio_controller.dart

...

  Future<void> startMusic() async {
    if (_musicHandle != null) {
      if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
        _log.info('Music is already playing. Stopping first.');
        await _soloud!.stop(_musicHandle!);
      }
    }
    _log.info('Loading music');
    final musicSource = await _soloud!
        .loadAsset('assets/music/looped-song.ogg', mode: LoadMode.disk);
    musicSource.allInstancesFinished.first.then((_) {
      _soloud!.disposeSource(musicSource);
      _log.info('Music source disposed');
      _musicHandle = null;
    });

    _log.info('Playing music');
    _musicHandle = await _soloud!.play(
      musicSource,
      volume: 0.6,
      looping: true,
      loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
    );
  }

...

เสียงเบาลง

ปัญหาต่อไปคือเพลงไม่รู้จบ มาใช้การเฟดกัน

วิธีหนึ่งที่คุณสามารถใช้การค่อยๆ ดังคือการใช้ฟังก์ชันบางอย่างที่จะเรียกหลายครั้งในวินาทีละ 1 ครั้ง เช่น Ticker หรือ Timer.periodic จากนั้นลดเสียงเพลงลงเล็กน้อย วิธีนี้ได้ผล แต่ก็เป็นงานหนัก

โชคดีที่ SoLoud ให้วิธีการดับเพลิงแล้วทำให้เสร็จได้โดยสะดวกซึ่งทำหน้าที่นี้ให้คุณ ต่อไปนี้คือวิธีที่ทำให้เพลงค่อยๆ เบาลงภายในเวลา 5 วินาที แล้วหยุดอินสแตนซ์เสียง เพื่อให้ไม่ต้องใช้ทรัพยากร CPU โดยไม่จำเป็น แทนที่เมธอด fadeOutMusic() ด้วยโค้ดนี้

lib/audio/audio_controller.dart

...

  void fadeOutMusic() {
    if (_musicHandle == null) {
      _log.info('Nothing to fade out');
      return;
    }
    const length = Duration(seconds: 5);
    _soloud!.fadeVolume(_musicHandle!, 0, length);
    _soloud!.scheduleStop(_musicHandle!, length);
  }

...

6. ใช้เอฟเฟกต์

ข้อดีที่สำคัญอย่างหนึ่งของการมีเครื่องมือเสียงที่เหมาะสมคือคุณสามารถประมวลผลเสียง เช่น กำหนดเส้นทางเสียงบางส่วนผ่านเสียงก้อง อีควอไลเซอร์ หรือตัวกรองโลว์พาส

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

ภาพคน 2 คนกำลังพูดคุยกันในห้อง คลื่นเสียงไม่เพียงกระจายจากบุคคลหนึ่งไปยังอีกคนหนึ่งโดยตรง แต่ยังกระทบกระแทกผนังและเพดานด้วย

SoLoud มีเอฟเฟกต์เสียงต่างๆ มากมายที่คุณนำไปใช้กับเสียงได้

  • หากต้องการให้ดูเหมือนว่าโปรแกรมเล่นอยู่ในห้องขนาดใหญ่ เช่น มหาวิหารหรือถ้ำ ให้ใช้ enum ของ FilterType.freeverbFilter ดังนี้

lib/audio/audio_controller.dart

...

  void applyFilter() {
    _soloud!.addGlobalFilter(FilterType.freeverbFilter);
    _soloud!.setFilterParameter(FilterType.freeverbFilter, 0, 0.2);
    _soloud!.setFilterParameter(FilterType.freeverbFilter, 2, 0.9);
  }

  void removeFilter() {
    _soloud!.removeGlobalFilter(FilterType.freeverbFilter);
  }

...

จะเห็นได้ว่าเมื่อใช้ตัวกรองเพื่อเจาะลึกพื้นที่ในระดับต่ำ การตั้งค่าพารามิเตอร์ตัวกรองจะทำกับดัชนีของพารามิเตอร์ เช่น พารามิเตอร์ Wet ของ Freeverb มีดัชนี 0 และพารามิเตอร์ขนาดห้องจะมีดัชนี 2

ด้วยโค้ดก่อนหน้า คุณสามารถทำสิ่งต่อไปนี้

  • เปิดใช้ตัวกรอง Freeverb ทั่วโลกหรือใช้งานได้กับมิกซ์เสียงทั้งหมด ไม่ใช่แค่เสียงเดียว
  • ตั้งค่าพารามิเตอร์ Wet เป็น 0.2 ซึ่งหมายความว่าเสียงที่ได้จะเป็นต้นฉบับ 80% และเอาต์พุตของเอฟเฟกต์ Reverb เป็น 20% หากคุณตั้งค่าพารามิเตอร์นี้เป็น 1.0 ก็คงจะเหมือนได้ยินเฉพาะคลื่นเสียงที่กลับมาหาคุณจากผนังที่ไกลๆ ของห้อง โดยไม่มีเสียงต้นฉบับใดๆ เลย
  • ตั้งค่าพารามิเตอร์ขนาดห้องเป็น 0.9 คุณสามารถปรับแต่งพารามิเตอร์นี้ได้ตามต้องการหรือแม้แต่ปรับเปลี่ยนแบบไดนามิก 1.0 เป็นถ้ำขนาดใหญ่ ส่วน 0.0 เป็นห้องน้ำ
  • เปลี่ยนโค้ดและใช้ตัวกรองรายการใดรายการหนึ่งต่อไปนี้ หรือใช้ตัวกรองต่อไปนี้ผสมกัน
  • FilterType.biquadResonantFilter (ใช้เป็นตัวกรองโลว์พาสได้)
  • FilterType.eqFilter
  • FilterType.echoFilter
  • FilterType.lofiFilter
  • FilterType.flangerFilter
  • FilterType.bassboostFilter
  • FilterType.waveShaperFilter
  • FilterType.robotizeFilter
  • FilterType.freeverbFilter

7. ขอแสดงความยินดี

คุณใช้ตัวควบคุมเสียงที่จะเล่นเสียง เล่นเพลงวนซ้ำ และใช้เอฟเฟกต์

ดูข้อมูลเพิ่มเติม

  • ลองเพิ่มประสิทธิภาพให้ตัวควบคุมเสียงด้วยฟีเจอร์ต่างๆ เช่น เสียงการโหลดล่วงหน้าเมื่อเริ่มต้น การเล่นเพลงตามลำดับ หรือการค่อยๆ ใช้ตัวกรองเมื่อเวลาผ่านไป
  • อ่านเอกสารประกอบเกี่ยวกับแพ็กเกจของ flutter_soloud
  • อ่านหน้าแรกของไลบรารี C++ ที่เกี่ยวข้อง
  • อ่านเพิ่มเติมเกี่ยวกับ Dart FFI ซึ่งเป็นเทคโนโลยีที่ใช้ในการเชื่อมต่อกับไลบรารี C++
  • ดูการบรรยายของ Guy Somberg เกี่ยวกับการจัดโปรแกรมเสียงในเกมเพื่อหาแรงบันดาลใจ (หรืออีกวิธีแบบยาว) เมื่อ Guy พูดถึง "มิดเดิลแวร์" เขาหมายถึงไลบรารีต่างๆ เช่น SoLoud และ FMOD ส่วนที่เหลือของโค้ดมักจะเฉพาะเจาะจงสำหรับแต่ละเกม
  • สร้างเกมแล้วเผยแพร่

ภาพหูฟัง