Thêm âm thanh và nhạc vào trò chơi Flutter

1. Trước khi bắt đầu

Trò chơi là trải nghiệm nghe nhìn. Flutter là một công cụ tuyệt vời để tạo dựng hình ảnh đẹp mắt và giao diện người dùng ổn định, vì vậy, nó giúp bạn cải thiện khía cạnh trực quan của mọi thứ. Thành phần còn thiếu còn lại là âm thanh. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng trình bổ trợ flutter_soloud để giới thiệu âm thanh và nhạc có độ trễ thấp cho dự án của mình. Bạn nên bắt đầu với một scaffold (giàn giáo) cơ bản để có thể chuyển thẳng đến các phần thú vị.

Hình minh hoạ tai nghe vẽ tay.

Tất nhiên, bạn có thể sử dụng những gì bạn học được ở đây để thêm âm thanh vào ứng dụng của bạn, chứ không chỉ trò chơi. Nhưng mặc dù hầu hết các trò chơi đều yêu cầu âm thanh và âm nhạc, nhưng hầu hết ứng dụng thì không, vì vậy, lớp học lập trình này tập trung vào trò chơi.

Điều kiện tiên quyết

  • Hiểu biết cơ bản về Flutter.
  • Có kiến thức về cách chạy và gỡ lỗi ứng dụng Flutter.

Kiến thức bạn sẽ học được

  • Cách phát âm thanh một lần.
  • Cách phát và tuỳ chỉnh vòng lặp âm nhạc không ngắt quãng.
  • Cách nhỏ dần âm thanh vào và ra.
  • Cách áp dụng hiệu ứng môi trường cho âm thanh.
  • Cách xử lý các trường hợp ngoại lệ.
  • Cách kết hợp tất cả tính năng này vào một bộ điều khiển âm thanh duy nhất.

Bạn cần có

  • SDK Flutter
  • Trình soạn thảo mã do bạn chọn

2. Thiết lập

  1. Tải các tệp sau xuống. Nếu bạn có kết nối chậm, đừng lo lắng. Bạn cần các tệp thực sự sau này để có thể cho phép tải xuống khi làm việc.
  1. Tạo một dự án Flutter bằng tên do bạn chọn.
  1. Tạo một tệp lib/audio/audio_controller.dart trong dự án.
  2. Trong tệp đó, hãy nhập mã sau:

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
  }
}

Như bạn có thể thấy, đây chỉ là bộ khung cho chức năng trong tương lai. Chúng ta sẽ triển khai tất cả trong lớp học lập trình này.

  1. Tiếp theo, hãy mở tệp lib/main.dart rồi thay thế nội dung của tệp bằng đoạn mã sau:

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. Sau khi tải các tệp âm thanh xuống, hãy tạo một thư mục trong gốc của dự án có tên là assets.
  2. Trong thư mục assets, hãy tạo 2 thư mục con, một thư mục con tên là music và thư mục còn lại tên là sounds.
  3. Di chuyển các tệp đã tải xuống vào dự án của bạn sao cho tệp bài hát nằm trong tệp assets/music/looped-song.ogg và âm thanh thốt lên ở các tệp sau:
  • assets/sounds/pew1.mp3
  • assets/sounds/pew2.mp3
  • assets/sounds/pew3.mp3

Bây giờ, cấu trúc dự án của bạn sẽ có dạng như sau:

Chế độ xem dạng cây của dự án, có các thư mục như &quot;android&quot;, &quot;ios&quot;, các tệp như &quot;README.md&quot; và &quot;analysis_options.yaml&quot;. Trong số này, chúng ta có thể thấy thư mục &quot;tài sản&quot; có các thư mục con &quot;music&quot; và &quot;sounds&quot;, thư mục &quot;lib&quot; có thư mục &quot;main.dart&quot; và một thư mục con &quot;audio&quot; có thông số &quot;audio_controller.dart&quot; và tệp &quot;pubspec.yaml&quot;.  Các mũi tên chỉ vào các thư mục mới và những tệp mà bạn đã chạm đến cho đến thời điểm này.

Giờ đây, khi đã có các tệp này, bạn cần phải cho Flutter biết về chúng.

  1. Mở tệp pubspec.yaml rồi thay thế phần flutter: ở cuối tệp bằng đoạn mã sau:

pubspec.yaml

...

flutter:
  uses-material-design: true

  assets:
    - assets/music/
    - assets/sounds/
  1. Thêm phần phụ thuộc vào gói flutter_soloud và gói logging.

pubspec.yaml

...

dependencies:
  flutter:
    sdk: flutter

  flutter_soloud: ^2.0.0
  logging: ^1.2.0

...
  1. Chạy dự án. Chưa có ứng dụng nào hoạt động vì bạn đã thêm chức năng này vào các phần sau.

10f0f751c9c47038.pngS

/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ác mã này đến từ thư viện C++ SoLoud cơ bản. Chúng không ảnh hưởng đến chức năng nên bạn có thể bỏ qua một cách an toàn.

3. Khởi động và tắt

Để phát âm thanh, bạn hãy sử dụng trình bổ trợ flutter_soloud. Trình bổ trợ này dựa trên dự án SoLoud, một công cụ âm thanh C++ cho các trò chơi được Nintendo SNES Classic sử dụng cùng với các trò chơi khác.

7ce23849b6d0d09a.png.

Để khởi chạy công cụ âm thanh SoLoud, hãy làm theo các bước sau:

  1. Trong tệp audio_controller.dart, hãy nhập gói flutter_soloud và thêm một trường _soloud riêng tư vào lớp này.

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
  }

  ...

Bộ điều khiển âm thanh quản lý công cụ SoLoud cơ bản thông qua trường này và sẽ chuyển tiếp tất cả lệnh gọi đến đó.

  1. Trong phương thức initialize(), hãy nhập mã sau:

lib/audio/audio_controller.dart

...

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

...

Thao tác này sẽ điền trường _soloud và chờ khởi chạy. Xin lưu ý những điều sau:

  • SoLoud cung cấp trường instance singleton. Không có cách tạo thực thể cho nhiều thực thể SoLoud. Đây không phải là điều mà công cụ C++ cho phép, vì vậy, trình bổ trợ Dart cũng không cho phép điều này.
  • Quá trình khởi chạy trình bổ trợ không đồng bộ và kết thúc cho đến khi phương thức init() trả về.
  • Tóm lại, trong ví dụ này, bạn không phát hiện lỗi trong khối try/catch. Trong mã phát hành chính thức, bạn cần thực hiện việc này và báo cáo lỗi cho người dùng.
  1. Trong phương thức dispose(), hãy nhập mã sau:

lib/audio/audio_controller.dart

...

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

...

Bạn nên tắt SoLoud khi thoát khỏi ứng dụng, mặc dù mọi thứ sẽ hoạt động bình thường ngay cả khi bạn không làm vậy.

  1. Lưu ý rằng phương thức AudioController.initialize() đã được gọi từ hàm main(). Điều này có nghĩa là việc khởi động lại dự án sẽ khởi chạy SoLoud ở chế độ nền, nhưng sẽ không mang lại lợi ích gì cho bạn trước khi bạn thực sự phát một số âm thanh.

4. Phát âm thanh một lần

Tải và phát nội dung

Giờ đây, bạn đã biết SoLoud được khởi động khi khởi động, nên bạn có thể yêu cầu SoLoud phát âm thanh.

SoLoud phân biệt nguồn âm thanh, cụ thể là dữ liệu và siêu dữ liệu dùng để mô tả âm thanh và "thực thể âm thanh" (chính là những âm thanh thực sự được phát). Một ví dụ về nguồn âm thanh có thể là một tệp mp3 được tải vào bộ nhớ, sẵn sàng phát và được biểu thị bằng một thực thể của lớp AudioSource. Mỗi khi bạn phát nguồn âm thanh này, SoLoud sẽ tạo một "phiên bản âm thanh" được biểu thị bằng loại SoundHandle.

Bạn nhận được một thực thể AudioSource bằng cách tải thực thể đó. Ví dụ: nếu có tệp mp3 trong thành phần, bạn có thể tải tệp đó để nhận AudioSource. Sau đó, bạn yêu cầu SoLoud phát AudioSource này. Bạn có thể phát đồng thời nhiều lần, thậm chí cùng lúc.

Khi dùng xong một nguồn âm thanh, bạn sẽ loại bỏ nguồn âm thanh đó bằng phương thức SoLoud.disposeSource().

Để tải và phát tài sản, hãy làm theo các bước sau:

  1. Trong phương thức playSound() của lớp AudioController, hãy nhập đoạn mã sau:

lib/audio/audio_controller.dart

  ...

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

  ...
  1. Lưu tệp, tải lại rồi chọn Phát âm thanh. Bạn sẽ nghe thấy âm thanh ngớ ngẩn. Xin lưu ý những điều sau:
  • Đối số assetKey được cung cấp có dạng assets/sounds/pew1.mp3 — cùng chuỗi mà bạn cung cấp cho bất kỳ API Flutter tải thành phần nào khác, chẳng hạn như tiện ích Image.asset().
  • Thực thể SoLoud cung cấp phương thức loadAsset() tải không đồng bộ tệp âm thanh từ các thành phần của dự án Flutter và trả về một thực thể của lớp AudioSource. Có các phương thức tương đương để tải một tệp từ hệ thống tệp (phương thức loadFile()) và tải qua mạng từ một URL (phương thức loadUrl()).
  • Sau đó, thực thể AudioSource mới thu nạp được chuyển đến phương thức play() của SoLoud. Phương thức này trả về một thực thể của loại SoundHandle đại diện cho âm thanh mới phát. Từ đó, tên người dùng này có thể được truyền đến các phương thức SoLoud khác để thực hiện những thao tác như tạm dừng, dừng hoặc chỉnh sửa âm lượng.
  • Mặc dù play() là phương thức không đồng bộ, nhưng về cơ bản, quá trình phát sẽ bắt đầu ngay lập tức. Gói flutter_soloud sử dụng giao diện hàm đối ngoại (FFI) của Dart để gọi mã C trực tiếp và đồng bộ. Thông thường, không tìm thấy thông báo qua lại giữa mã Dart và mã nền tảng, vốn thường thấy ở hầu hết các trình bổ trợ Flutter. Lý do duy nhất khiến một số phương thức không đồng bộ là một số mã của trình bổ trợ chạy trong một vùng riêng và hoạt động giao tiếp giữa các vùng phân tách Dart không đồng bộ.
  • Bạn chỉ cần xác nhận rằng trường _soloud không rỗng bằng _soloud!. Tôi xin nhắc lại một lần nữa để nói ngắn gọn. Mã sản xuất phải xử lý linh hoạt tình huống khi nhà phát triển cố gắng phát âm thanh trước khi bộ điều khiển âm thanh có cơ hội khởi chạy đầy đủ.

Xử lý các trường hợp ngoại lệ

Có thể bạn đã nhận thấy rằng một lần nữa, bạn đang bỏ qua các ngoại lệ có thể có. Hãy khắc phục vấn đề đó đối với phương pháp cụ thể này để học tập. (Tóm lại, lớp học lập trình sẽ quay lại phương thức bỏ qua các ngoại lệ sau phần này.)

  • Để xử lý các ngoại lệ trong trường hợp này, hãy gói hai dòng của phương thức playSound() trong một khối try/catch và chỉ phát hiện các thực thể của 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 gửi nhiều trường hợp ngoại lệ, chẳng hạn như ngoại lệ SoLoudNotInitializedException hoặc SoLoudTemporaryFolderFailedException. Tài liệu API của mỗi phương thức liệt kê các loại ngoại lệ có thể được gửi.

SoLoud cũng cung cấp một lớp mẹ cho tất cả các trường hợp ngoại lệ, đó là trường hợp ngoại lệ SoLoudException. Nhờ đó, bạn có thể phát hiện mọi lỗi liên quan đến chức năng của công cụ phát âm thanh. Điều này đặc biệt hữu ích trong những trường hợp việc phát âm thanh không quan trọng. Ví dụ: khi bạn không muốn làm hỏng phiên chơi của người chơi chỉ vì hệ thống không tải được âm thanh kêu.

Như bạn thường thấy, phương thức loadAsset() cũng có thể gửi ra lỗi FlutterError nếu bạn cung cấp khoá tài sản không tồn tại. Nhìn chung, bạn nên giải quyết việc cố gắng tải các thành phần không đi kèm với trò chơi, do đó đó là lỗi.

Phát nhiều âm thanh

Bạn có thể nhận thấy chỉ phát tệp pew1.mp3, nhưng có hai phiên bản âm thanh khác trong thư mục nội dung. Thường thì âm thanh sẽ tự nhiên hơn khi trò chơi có nhiều phiên bản âm thanh giống nhau và người dùng có thể phát các phiên bản khác nhau theo cách ngẫu nhiên hoặc luân phiên. Điều này giúp ngăn chặn (ví dụ: tiếng bước chân và tiếng súng) nghe có vẻ không đồng nhất và do đó là giả mạo.

  • Đây là một bài tập không bắt buộc, hãy sửa đổi mã để phát một âm thanh khác mỗi khi người dùng nhấn vào nút này.

Hình minh hoạ

5. Phát vòng lặp âm nhạc

Quản lý các âm thanh chạy lâu hơn

Một số âm thanh sẽ phát trong thời gian dài. Ví dụ rõ ràng là âm nhạc, nhưng nhiều trò chơi cũng chơi không khí, chẳng hạn như tiếng gió hú qua hành lang, tiếng các nhà sư từ xa, tiếng kim loại hàng trăm năm tuổi kêu lách cách hay tiếng ho từ xa của bệnh nhân.

Đây là những nguồn âm thanh có thời gian phát có thể đo lường bằng phút. Bạn cần theo dõi các quảng cáo đó để có thể tạm dừng hoặc dừng quảng cáo khi cần. Các tệp này cũng thường được các tệp lớn hỗ trợ và có thể tốn nhiều bộ nhớ. Vì vậy, một lý do khác để theo dõi tệp là để bạn có thể loại bỏ thực thể AudioSource khi không cần nữa.

Do đó, bạn sẽ giới thiệu một trường riêng tư mới cho AudioController. Đây là tên người dùng của bài hát đang phát, nếu có. Hãy thêm dòng lệnh sau đây:

lib/audio/audio_controller.dart

...

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

  SoLoud? _soloud;

  SoundHandle? _musicHandle;    // ← Add this.

  ...

Bắt đầu phát nhạc

Về cơ bản, việc phát nhạc không khác với phát âm thanh một lần. Trước tiên, bạn vẫn cần tải tệp assets/music/looped-song.ogg làm thực thể của lớp AudioSource, sau đó sử dụng phương thức play() của SoLoud để phát tệp đó.

Tuy nhiên, lần này bạn sẽ giữ thanh điều khiển âm thanh mà phương thức play() quay lại để điều khiển âm thanh trong khi phát.

  • Nếu muốn, hãy tự triển khai phương thức AudioController.startMusic(). Nếu bạn không nắm được một số thông tin chính xác thì cũng không sao. Điều quan trọng là nhạc sẽ bắt đầu khi bạn chọn Bắt đầu phát nhạc.

Dưới đây là cách triển khai tham chiếu:

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

...

Lưu ý rằng bạn tải tệp nhạc ở chế độ đĩa (enum LoadMode.disk). Điều này đơn giản có nghĩa là tệp chỉ được tải theo từng phần khi cần. Để có âm thanh chạy lâu hơn, thông thường, tốt nhất bạn nên tải ở chế độ đĩa. Đối với các hiệu ứng âm thanh ngắn, bạn nên tải và giải nén các hiệu ứng đó vào bộ nhớ (enum LoadMode.memory mặc định).

Tuy nhiên, bạn có một vài vấn đề. Thứ nhất, nhạc quá lớn, tràn ngập âm thanh. Trong hầu hết các trò chơi, nhạc thường phát ở chế độ nền, qua đó giúp âm thanh giàu thông tin hơn như lời nói và hiệu ứng âm thanh. Bạn có thể dễ dàng khắc phục vấn đề này bằng cách sử dụng tham số âm lượng của phương thức phát. Ví dụ: bạn có thể thử _soloud!.play(musicSource, volume: 0.6) để phát bài hát ở mức âm lượng 60%. Ngoài ra, bạn có thể đặt âm lượng vào bất cứ lúc nào sau này bằng cách dùng _soloud!.setVolume(_musicHandle, 0.6).

Vấn đề thứ hai là bài hát dừng đột ngột. Lý do là vì đây là một bài hát cần phát lặp lại và điểm bắt đầu của vòng lặp không phải là điểm bắt đầu của tệp âm thanh.

88d2c57fffdfe996.pngS

Đây là một lựa chọn phổ biến cho nhạc trò chơi vì bài hát bắt đầu bằng một đoạn mở đầu tự nhiên, sau đó có thể phát lâu dài khi cần mà không có một điểm lặp rõ ràng. Khi trò chơi cần chuyển sang trạng thái khác của bài hát đang phát, bài hát đó chỉ tắt dần.

Rất may là SoLoud cung cấp nhiều cách phát âm thanh lặp lại. Phương thức play() lấy giá trị boolean cho tham số looping, đồng thời nhận giá trị cho điểm bắt đầu của vòng lặp làm tham số loopingStartAt. Mã kết quả có dạng như sau:

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),
);

...

Nếu bạn không đặt tham số loopingStartAt, thì theo mặc định, tham số này sẽ là Duration.zero (nói cách khác, là điểm bắt đầu của tệp âm thanh). Nếu bạn có một bản nhạc là một vòng lặp hoàn hảo mà không cần có phần giới thiệu, thì đây chính là điều bạn muốn làm.

  • Để đảm bảo rằng nguồn âm thanh được xử lý đúng cách sau khi phát xong, hãy nghe luồng allInstancesFinished mà mỗi nguồn âm thanh cung cấp. Với các lệnh gọi nhật ký đã thêm, phương thức startMusic() sẽ có dạng như sau:

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

...

Âm thanh nhỏ dần

Vấn đề tiếp theo là âm nhạc không bao giờ kết thúc. Hãy triển khai hiệu ứng làm mờ.

Bạn có thể triển khai hiệu ứng làm mờ bằng cách dùng một loại hàm được gọi vài lần trong một giây, chẳng hạn như Ticker hoặc Timer.periodic, sau đó giảm âm lượng của bản nhạc xuống những lần giảm nhỏ. Như vậy có được, nhưng mất rất nhiều công sức.

May mắn thay, SoLoud cung cấp các phương pháp "quên quên" thuận tiện để giúp bạn làm việc này. Dưới đây là cách bạn có thể làm mờ nhạc trong khoảng thời gian năm giây và sau đó dừng thực thể âm thanh để nó không tiêu thụ tài nguyên CPU một cách không cần thiết. Thay thế phương thức fadeOutMusic() bằng mã sau:

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. Áp dụng hiệu ứng

Một lợi thế lớn khi sử dụng một công cụ âm thanh phù hợp là bạn có thể xử lý âm thanh, chẳng hạn như định tuyến một số âm thanh thông qua âm vang, bộ cân bằng âm thanh hoặc bộ lọc thông thấp.

Trong trò chơi, vị trí có thể được dùng để phân biệt thính giác. Ví dụ: tiếng vỗ tay trong rừng khác với tiếng vỗ tay trong hầm bê tông. Trong khi rừng cây giúp phân tán và hấp thụ âm thanh, thì các bức tường trần của hầm trú ẩn phản chiếu sóng âm trở lại, dẫn đến âm vang. Tương tự như vậy, giọng nói của mọi người cũng khác khi nghe thấy qua tường. Tần số cao hơn của những âm thanh đó dễ bị suy giảm hơn khi chúng đi qua môi trường chất rắn, dẫn đến hiệu ứng bộ lọc thông thấp.

Hình minh hoạ hai người nói chuyện trong một căn phòng. Sóng âm thanh không chỉ truyền trực tiếp từ người này sang người khác mà còn bật ra khỏi tường và trần nhà.

SoLoud cung cấp một số hiệu ứng âm thanh mà bạn có thể áp dụng cho âm thanh.

  • Để người chơi nghe như đang ở trong một căn phòng lớn, chẳng hạn như nhà thờ hoặc hang động, hãy sử dụng 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);
  }

...

Như bạn có thể thấy, bằng bộ lọc, bạn có thể tìm hiểu kỹ hơn về lãnh thổ cấp thấp hơn. Việc đặt tham số bộ lọc được thực hiện thông qua chỉ mục của tham số đó. Ví dụ: tham số Wet của động từ tự do có chỉ mục là 0 và tham số Room size có chỉ mục là 2.

Với mã trước đó, bạn làm như sau:

  • Bật bộ lọc động từ tự do trên toàn bộ hoặc cho toàn bộ danh sách kết hợp âm thanh chứ không chỉ một âm thanh duy nhất.
  • Đặt thông số Wet thành 0.2, tức là âm thanh thu được sẽ có 80% âm thanh gốc và 20% đầu ra của hiệu ứng âm vang. Nếu bạn đặt tham số này thành 1.0, thì việc này sẽ giống như chỉ nghe thấy sóng âm thanh quay lại bạn từ những bức tường xa của phòng và không có âm thanh gốc.
  • Đặt thông số Room size (Kích thước phòng) thành 0.9. Bạn có thể điều chỉnh tham số này theo ý thích hoặc thậm chí thay đổi linh động. 1.0 là một hang động lớn trong khi 0.0 là một phòng tắm.
  • Nếu bạn thích, hãy thay đổi mã và áp dụng một trong các bộ lọc sau hoặc kết hợp các bộ lọc sau:
  • FilterType.biquadResonantFilter (có thể dùng làm bộ lọc thông báo thấp)
  • FilterType.eqFilter
  • FilterType.echoFilter
  • FilterType.lofiFilter
  • FilterType.flangerFilter
  • FilterType.bassboostFilter
  • FilterType.waveShaperFilter
  • FilterType.robotizeFilter
  • FilterType.freeverbFilter

7. Xin chúc mừng

Bạn đã triển khai một bộ điều khiển âm thanh phát âm thanh, lặp lại nhạc và áp dụng hiệu ứng.

Tìm hiểu thêm

  • Hãy thử dùng bộ điều khiển âm thanh hơn nữa bằng các tính năng như tải trước âm thanh khi khởi động, phát bài hát theo trình tự hoặc áp dụng bộ lọc dần theo thời gian.
  • Đọc tài liệu về gói của flutter_soloud.
  • Đọc trang chủ của thư viện C++ cơ bản.
  • Đọc thêm về Dart FFI, công nghệ dùng để giao tiếp với thư viện C++.
  • Xem bài nói chuyện của Guy Somberg về chương trình âm thanh trò chơi để tìm cảm hứng. (Ngoài ra còn có một URL dài hơn.) Khi Guy nói về "phần mềm trung gian", anh ấy muốn nói đến các thư viện như SoLoud và FMOD. Phần còn lại của mã có xu hướng dành riêng cho từng trò chơi.
  • Tạo bản dựng và phát hành trò chơi.

Hình minh hoạ tai nghe