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 hình ảnh đẹp mắt và giao diện người dùng vững chắc, nhờ đó, bạn có thể phát triển nhiều khía cạnh hình ảnh. Thành phần còn thiếu 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 sẽ bắt đầu với một khung 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.
  • 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 đoạn nhạc lặp lại không gián đoạn.
  • Cách làm âm thanh mờ dần và rõ dần.
  • 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 duy nhất.

Bạn cần có

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

2. Thiết lập

  1. Tải các tệp sau xuống. Đừng lo nếu bạn có kết nối chậm. 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 có tên tuỳ ý.
  1. Tạo 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à một 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 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),
      ),
      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 tệp âm thanh xuống, hãy tạo một thư mục có tên là assets ở gốc dự án.
  2. Trong thư mục assets, hãy tạo hai thư mục con, một thư mục có tên là music và thư mục còn lại có 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 nội dung 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.
flutter pub add flutter_soloud logging

Tệp pubspec.yaml của bạn hiện sẽ có các phần phụ thuộc bổ sung trên các gói flutter_soloudlogging.

pubspec.yaml

...

dependencies:
  flutter:
    sdk: flutter

  flutter_soloud: ^3.1.10
  logging: ^1.3.0

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

10f0f751c9c47038.png

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

Để phát âm thanh, bạ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 sử dụng (trong số nhiều công cụ khác) bởi Nintendo SNES Classic.

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 trường _soloud riêng tư vào lớp.

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
  }

  ...

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 một trường instance singleton. Không có cách nào để tạo bản sao cho một số 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ợ là không đồng bộ và chưa hoàn tất cho đến khi phương thức init() trả về.
  • Để ngắn gọn 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 nên làm như vậy và báo cáo mọi 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 nóng dự án sẽ khởi chạy SoLoud trong nền, nhưng sẽ không có tác dụng gì 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 một thành phần

Giờ đây, khi đã biết SoLoud được khởi chạy 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 "thực thể âm thanh" được biểu thị bằng loại SoundHandle.

Bạn có thể lấy 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ể chơi nhiều lần, thậm chí là cùng lúc.

Khi đã sử dụng xong nguồn âm thanh, bạn có thể loại bỏ nguồn đó 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 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ột âm thanh pew 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 — cùng một chuỗi mà bạn sẽ 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 tệp từ hệ thống tệp (phương thức loadFile()) và để tải qua mạng 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 đó, bạn có thể truyền handle này đến các phương thức SoLoud khác để thực hiện các thao tác như tạm dừng, dừng hoặc sửa đổi â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 ngoại (FFI) của Dart để gọi mã C trực tiếp và đồng bộ. Không tìm thấy thông báo qua lại thông thường giữa mã Dart và mã nền tảng đặ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 xác nhận rằng trường _soloud không phải là giá trị rỗng với _soloud!. Xin nhắc lại, điều này là để viết 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. Đã đến lúc khắc phục vấn đề đó cho phương thức cụ thể này cho mục đích học tập. (Để ngắn gọn, lớp học lập trình sẽ quay lại việc bỏ qua các ngoại lệ sau phần này.)

  • Để xử lý các trường hợp 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 có thể dự kiến, phương thức loadAsset() cũng có thể gửi lỗi FlutterError nếu bạn cung cấp khoá tài sản không tồn tại. Bạn thường phải giải quyết việc cố gắng tải các tài sản không đi kèm với trò chơi, vì vậy, đây là lỗi.

Phát nhiều âm thanh

Bạn có thể nhận thấy rằng bạn chỉ phát tệp pew1.mp3, nhưng có hai phiên bản âm thanh khác trong thư mục tài sản. Âm thanh thường nghe tự nhiên hơn khi trò chơi có nhiều phiên bản của cùng một âm thanh và phát các phiên bản khác nhau một 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 ghế ngồi khác nhau mỗi khi người dùng nhấn vào nút.

Hình minh hoạ

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

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. Âm nhạc là ví dụ rõ ràng nhất, nhưng nhiều trò chơi cũng phát âm thanh môi trường xung quanh, chẳng hạn như tiếng gió hú qua hành lang, tiếng tụng kinh của các nhà sư ở xa, tiếng kim loại kẽo kẹt đã hàng thế kỷ hoặc tiếng ho của bệnh nhân ở xa.

Đâ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 ứng dụng đó để có thể tạm dừng hoặc dừng hẳn khi cần. Các tệp này cũng thường được sao lưu bằng các tệp lớn và có thể tiêu tốn nhiều bộ nhớ. Vì vậy, một lý do khác để theo dõi các tệp này là để bạn có thể loại bỏ thực thể AudioSource khi không cần nữa.

Vì lý 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 thanh một lần. Trước tiên, bạn vẫn cần tải tệp assets/music/looped-song.ogg dưới dạng một 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ữ tay cầm âm thanh mà phương thức play() trả về để thao tác với âm thanh trong khi 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 phát 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,
    );
  }

...

Lưu ý rằng bạn tải tệp nhạc ở chế độ đĩa (enum LoadMode.disk). Điều này có nghĩa là tệp chỉ được tải theo từng phần khi cần. Đối với âm thanh chạy lâu hơn, 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, âm nhạc thường ở chế độ nền, nhường sân khấu trung tâm cho âm thanh mang tính thông tin hơn, chẳng hạn như lời nói và hiệu ứng âm thanh. Đây là cách khắc phục 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 tại bất kỳ thời điểm nào sau đó bằng một lệnh như _soloud!.setVolume(_musicHandle, 0.6).

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

88d2c57fffdfe996.png

Đây là lựa chọn phổ biến cho nhạc trò chơi vì có nghĩa là bài hát bắt đầu bằng một phần mở đầu tự nhiên, sau đó phát trong thời gian cần thiết mà không có điểm lặp rõ ràng. Khi cần chuyển đổi khỏi bài hát đang phát, trò chơi sẽ làm mờ bài hát.

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ả sẽ 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ặp lại hoàn hảo mà không có phần giới thiệu, thì đây chính là lựa chọn bạn cần.

  • Để xác minh 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ý đượ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. Đã đến lúc triển khai hiệu ứng mờ.

Một cách để bạn có thể triển khai hiệu ứng mờ là có một số loại hàm được gọi vài lần mỗi giây, chẳng hạn như Ticker hoặc Timer.periodic, và giảm âm lượng của nhạc theo mức giảm nhỏ. Cách này sẽ hoạt động, nhưng bạn sẽ phải làm nhiều việ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. Dưới đây là cách bạn có thể làm mờ 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 giúp tản và hấp thụ âm thanh, thì các bức tường trần của hầm phản xạ 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 đó bị suy giảm nhiều hơn khi chúng đi qua môi trường 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 phòng. Sóng âm không chỉ truyền trực tiếp từ người này sang người khác mà còn dội vào 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.

  • Để tạo cảm giác như 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 các bộ lọc đó. 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 bộ lọc cho một nguồn duy nhất, hãy sử dụng AudioSource.filters đối ứng có hành động tương tự.

Với mã trước đó, bạn thực hiện 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ì bạn sẽ chỉ nghe thấy các sóng âm thanh phản hồi từ những bức tường xa trong phòng mà không nghe thấy âm thanh ban đầu.
  • Đặt tham số Room Size (Diện tích phòng) thành 0.9. Bạn có thể điều chỉnh thông số này theo ý thích hoặc thậm chí thay đổi thông số này một cách linh động. 1.0 là một hang động khổng lồ còn 0.0 là một phòng tắm.
  • Nếu bạn muốn, 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 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 trình đ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 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 của Guy Somberg về lập trình âm thanh trò chơi để tìm cảm hứng. (Cũng có một phiên bản 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 và phát hành trò chơi.

Hình minh hoạ tai nghe