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

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

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

ภาพหูฟังที่วาดด้วยมือ

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

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

  • คุ้นเคยกับ 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),
      ),
      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
flutter pub add flutter_soloud logging

ตอนนี้ไฟล์ pubspec.yaml ควรมี Dependency เพิ่มเติมในแพ็กเกจ flutter_soloud และ logging

pubspec.yaml

...

dependencies:
  flutter:
    sdk: flutter

  flutter_soloud: ^3.1.10
  logging: ^1.3.0

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

10f0f751c9c47038.png

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:async';

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 ที่อยู่เบื้องหลังผ่านช่องนี้ และจะส่งต่อการเรียกทั้งหมดไปยังเครื่องยนต์

  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() อยู่แล้ว ซึ่งหมายความว่าการรีสตาร์ทโปรเจ็กต์จากการทำงานแบบ Hot จะเริ่มต้น 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,
    );
  }

...

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

แต่คุณมีปัญหาอยู่ 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

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

ภาพหูฟัง