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

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

เกมเป็นประสบการณ์ภาพและเสียง Flutter เป็นเครื่องมือที่ยอดเยี่ยมในการสร้างภาพที่สวยงามและ UI ที่มีประสิทธิภาพ ซึ่งจะช่วยให้คุณเห็นภาพของสิ่งต่างๆ ได้ชัดเจนยิ่งขึ้น ส่วนผสมสุดท้ายที่ขาดหายไปคือเสียง ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีใช้ปลั๊กอิน 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
  }
}

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

  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. เพิ่มการพึ่งพาแพ็กเกจ 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 แบบเดี่ยว คุณไม่สามารถสร้างอินสแตนซ์ 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 เล่นเสียงได้

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 Reload แล้วเลือกเล่นเสียง คุณควรได้ยินเสียงม้านั่งตลกๆ โปรดทราบดังต่อไปนี้
  • อาร์กิวเมนต์ 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 เพื่อให้คุณสามารถตรวจจับข้อผิดพลาดทั้งหมดที่เกี่ยวข้องกับฟังก์ชันการทำงานของเครื่องมือเสียงได้ ซึ่งจะเป็นประโยชน์อย่างยิ่งในกรณีที่การเล่นเสียงไม่สำคัญ ตัวอย่างเช่น เมื่อคุณไม่ต้องการให้เซสชันเกมของผู้เล่นขัดข้องเพียงเพราะไม่สามารถโหลดเสียงพิว-พิวได้

และคุณอาจคาดเดาได้ว่าเมธอด 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),
    );
  }

...

ค่อยๆ เบาลง

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

วิธีหนึ่งที่คุณสามารถใช้การเลือนเสียงคือให้มีฟังก์ชันบางอย่างที่เรียกใช้หลายครั้งต่อวินาที เช่น 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 มีเอฟเฟกต์เสียงต่างๆ มากมายที่คุณนำไปใช้กับเสียงได้

  • หากต้องการให้เสียงเหมือนผู้เล่นอยู่ในห้องขนาดใหญ่ เช่น มหาวิหารหรือถ้ำ ให้ใช้ช่อง SoLoud.filters ดังนี้

lib/audio/audio_controller.dart

...

  void applyFilter() {
    _soloud!.filters.freeverbFilter.activate();
    _soloud!.filters.freeverbFilter.wet.value = 0.2;
    _soloud!.filters.freeverbFilter.roomSize.value = 0.9;
  }

  void removeFilter() {
    _soloud!.filters.freeverbFilter.deactivate();
  }

...

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

หมายเหตุ: _soloud!.filters จะแสดงตัวกรองส่วนกลาง หากต้องการใช้ตัวกรองกับแหล่งที่มาแหล่งเดียว โปรดใช้ AudioSource.filters คู่กัน ซึ่งทํางานแบบเดียวกัน

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

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

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

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

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

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

ภาพหูฟัง