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 để đưa âm thanh và nhạc có độ trễ thấp vào dự án của mình. Bạ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 được vẽ tay.

Tất nhiên, bạn có thể sử dụng những kiến thức học được tại đây để thêm âm thanh vào ứng dụng, chứ không chỉ trò chơi. Tuy hầu hết các trò chơi đều cần có âm thanh và nhạc, nhưng hầu hết các ứng dụng thì không. Vì vậy, lớp học lập trình này sẽ 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 các vòng lặp nhạc không gián đoạn.
  • 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 đóng gói tất cả các tính năng này vào một bộ điều khiển âm thanh.

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 sẽ cần các tệp thực tế sau này, vì vậy, bạn có thể tải các tệp đó xuống trong 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 để tệp bài hát nằm trong tệp assets/music/looped-song.ogg và âm thanh của hàng ghế ngồi nằm trong các tệp sau:
  • assets/sounds/pew1.mp3
  • assets/sounds/pew2.mp3
  • assets/sounds/pew3.mp3

Lúc này, 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, với các thư mục như `android`, `ios`, các tệp như `README.md` và `analysis_options.yaml`. Trong số này, chúng ta có thể thấy thư mục `assets` có các thư mục con `music` và `sounds`, thư mục `lib` có `main.dart` và thư mục con `audio` có `audio_controller.dart` và tệp `pubspec.yaml`.  Các mũi tên trỏ đến các thư mục mới và các tệp mà bạn đã chạm vào cho đến thời điểm này.

Giờ đây, khi các tệp đã có sẵn, bạn cần cho Flutter biết về các tệp đó.

  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.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ác hàm này đến từ thư viện C++ SoLoud cơ bản. Các lỗi này không ảnh hưởng đến chức năng và bạn có thể bỏ qua.

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

Để phát âm thanh, bạn cần 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
  }

  ...

Trình đ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 công cụ đó.

  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 sẵ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, do đó, 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 ứng dụng, mặc dù mọi thứ vẫn hoạt động tốt ngay cả khi bạn quên làm điều nà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, khi đã biết SoLoud được khởi động khi khởi động, bạn có thể yêu cầu SoLoud phát âm thanh.

SoLoud phân biệt giữa nguồn âm thanh (là dữ liệu và siêu dữ liệu dùng để mô tả âm thanh) và "thực thể âm thanh" (là âm thanh thực sự được phát). Ví dụ về nguồn âm thanh có thể là 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 tài sản, bạn có thể tải tệp đó để lấy 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 một thành phầ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 nhanh 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 như assets/sounds/pew1.mp3 – chính là chuỗi mà bạn cung cấp cho bất kỳ API Flutter tải tài sản nào khác, chẳng hạn như tiện ích Image.asset().
  • Thực thể SoLoud cung cấp một phương thức loadAsset() tải không đồng bộ tệp âm thanh từ các tài sả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 được sẽ đượ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. Sau đó, 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 của âm thanh.
  • Mặc dù play() là một phương thức không đồng bộ, nhưng quá trình phát về cơ bản 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 còn tìm thấy thông báo qua lại giữa mã Dart và mã nền tảng, vốn đặc trưng cho 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 vùng chứa riêng và hoạt động giao tiếp giữa các vùng chứa Dart là 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!. Xin nhắc lại, điều này là để ngắn gọn. Mã phát hành chính thức phải xử lý linh hoạt trường hợp nhà phát triển cố gắng phát âm thanh trước khi trình đ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ệ

Bạn có thể nhận thấy rằng một lần nữa, bạn đang bỏ qua các ngoại lệ có thể xảy ra. Hãy khắc phục vấn đề đó cho phương thức cụ thể này cho mục đích 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 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 ngoại lệ, ngoại lệ SoLoudException, để bạn có thể phát hiện tất cả lỗi liên quan đến chức năng của công cụ âm thanh. Điều này đặc biệt hữu ích trong trường hợp việc phát âm thanh không quan trọng. Ví dụ: khi bạn không muốn phiên chơi của người chơi gặp sự cố chỉ vì một trong những âm thanh pew-pew không tải được.

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. Việc này thường nghe 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. Ví dụ: điều này giúp tiếng bước chân và tiếng súng không quá đồng nhất và giả tạ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ý âm thanh chạy trong thời gian dài hơn

Một số âm thanh được thiết kế để 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 lượng phát có thể đo lường được theo phút. Bạn cần theo dõi các phiên phát để có thể tạm dừng hoặc dừng phát 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 gì việc phát mộ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ữ handle âm thanh mà phương thức play() trả về để thao tác với âm thanh trong khi âm thanh đang phát.

  • Nếu muốn, hãy tự triển khai phương thức AudioController.startMusic(). Bạn không cần phải lo lắng nếu có một số chi tiết chưa chính xác. Đ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 chỉ 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á to, lấn át các âm thanh khác. 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.png

Đâ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, SoLoud cung cấp các cách để phát âm thanh lặp lại. Phương thức play() nhận một 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, tham số này sẽ mặc định là Duration.zero (tức là đầu 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 cần.

  • Để đảm bảo 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ý được 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),
    );
  }

...

Làm mờ âm thanh

Vấn đề tiếp theo là 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.

Rất may, SoLoud cung cấp các phương thức bắn và quên thuận tiện để thực hiện việc này cho bạn. Sau đây là cách bạn có thể làm giảm âm lượng nhạc trong vòng 5 giây, sau đó dừng thực thể âm thanh để không tiêu tốn 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 có 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 hoặc bộ lọc thông thấp.

Trong trò chơi, bạn có thể dùng tính năng này để phân biệt vị trí bằng âm thanh. Ví dụ: tiếng vỗ tay trong rừng sẽ 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ự, giọng nói của mọi người sẽ khác khi nghe 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 đang 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.

  • Để nghe như thể người chơi đ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 trường 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();
  }

...

Trường SoLoud.filters cho phép bạn truy cập vào tất cả các loại bộ lọc và tham số của chúng. Mọi tham số cũng có các chức năng tích hợp sẵn như làm mờ dần và dao động.

Lưu ý: _soloud!.filters hiển thị các bộ lọc chung. Nếu bạn muốn áp dụng các bộ lọc cho một nguồn, vui lòng sử dụng phần tử tương ứng AudioSource.filters. Thao tác này cũng tương tự như trên.

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

  • Bật bộ lọc freeverb trên toàn cục.
  • Đặt thông số Wet (Âm ướt) thành 0.2, nghĩa là âm thanh thu được sẽ là 80% âm thanh gốc và 20% là đầ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:
  • biquadFilter (có thể dùng làm bộ lọc thông báo thấp)
  • pitchShiftFilter
  • equalizerFilter
  • echoFilter
  • lofiFilter
  • flangerFilter
  • bassboostFilter
  • waveShaperFilter
  • robotizeFilter

7. Xin chúc mừng

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

Tìm hiểu thêm

  • Hãy thử sử dụng bộ điều khiển âm thanh với các tính năng như tải trước âm thanh khi khởi động, phát các bài hát theo trình tự hoặc áp dụng bộ lọc dần dần theo thời gian.
  • Đọc tài liệu 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 của Guy Somberg về lập 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 nói về "phần mềm trung gian", Guy muốn nói đến các thư viện như SoLoud và FMOD. Phần còn lại của mã thườ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