أضِف صوتًا وموسيقى إلى لعبتك على Flutter

1. قبل البدء

الألعاب هي تجارب مرئية وصوتية. ‫Flutter هي أداة رائعة لإنشاء مرئيات جميلة وواجهة مستخدم قوية، لذا فهي تساعدك كثيرًا في الجانب المرئي. العنصر المتبقّي هو الصوت. في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية استخدام المكوّن الإضافي 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، أنشئ دليلَين فرعيَّين، أحدهما باسم 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 الآن تبعيات إضافية على حِزم 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(). وهذا يعني أنّ إعادة تشغيل المشروع فورًا تؤدي إلى بدء SoLoud في الخلفية، ولكن لن تفيدك هذه العملية قبل تشغيل بعض الأصوات.

4. تشغيل أصوات قصيرة

تحميل مادة عرض وتشغيلها

بعد أن عرفت أنّه يتمّ إعداد 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. احفظ الملف وأعِد تحميله، ثم انقر على تشغيل الصوت. من المفترض أن تسمع صوتًا سخيفًا. يُرجى ملاحظة ما يلي:
  • الوسيطة assetKey المقدَّمة هي مثل assets/sounds/pew1.mp3، وهي السلسلة نفسها التي تقدّمها لأي واجهة برمجة تطبيقات أخرى من Flutter لتحميل مواد العرض، مثل التطبيق المصغّر 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!. يهدف كل ذلك إلى الإيجاز. يجب أن يتعامل رمز الإنتاج بشكلٍ سلس مع الموقف الذي يحاول فيه المطوّر تشغيل صوت قبل أن تحصل وحدة التحكّم في الصوت على فرصة لبدء التشغيل بالكامل.

التعامل مع الاستثناءات

ربما لاحظت أنّك تتجاهل مرة أخرى الاستثناءات المحتمَلة. لقد حان الوقت لحلّ هذه المشكلة في هذه الطريقة المحدّدة لأغراض التعلّم. (للاختصار، يعود الدرس التطبيقي إلى تجاهل الاستثناءات بعد هذا القسم).

  • للتعامل مع الاستثناءات في هذه الحالة، عليك لفّ السطرَين من طريقة 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. تسرد مستندات واجهة برمجة التطبيقات لكل طريقة أنواع الاستثناءات التي قد يتم طرحها.

توفّر SoLoud أيضًا فئة رئيسية لجميع استثناءاتها، وهو استثناء SoLoudException، حتى تتمكّن من رصد جميع الأخطاء المرتبطة بوظائف محرّك الصوت. ويُعدّ ذلك مفيدًا بشكل خاص في الحالات التي لا يكون فيها تشغيل الصوت أمرًا مهمًا. على سبيل المثال، عندما لا تريد تعطيل جلسة اللعب الخاصة باللاعب فقط لأنّه تعذّر تحميل أحد أصوات إطلاق النار.

كما هو متوقّع، يمكن أن تؤدي طريقة loadAsset() أيضًا إلى ظهور خطأ FlutterError إذا قدّمت مفتاح مادة عرض غير متوفّر. إنّ محاولة تحميل مواد عرض غير مضمّنة في اللعبة هي أمر يجب معالجته بشكل عام، لذلك يُعدّ خطأ.

تشغيل أصوات مختلفة

ربما لاحظت أنّك تشغّل ملف pew1.mp3 فقط، ولكن هناك نسختان أخريان من الصوت في دليل مواد العرض. غالبًا ما يبدو الصوت أكثر طبيعية عندما تتضمّن الألعاب عدة نُسخ من الصوت نفسه، وتشغّل النُسخ المختلفة بطريقة عشوائية أو بشكل دوار. ويمنع ذلك، على سبيل المثال، أن تبدو أصوات الخطوات وطلقات النار متماثلة جدًا وبالتالي زائفة.

  • كتدريب اختياري، يمكنك تعديل الرمز لتشغيل صوت مختلف كل مرة يتم فيها النقر على الزر.

صورة توضيحية

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). وهذا يعني أنّه يتم تحميل الملف فقط في أجزاء حسب الحاجة. بالنسبة إلى الملفات الصوتية التي تستغرق وقتًا أطول، من الأفضل بشكل عام تحميلها في وضع القرص. بالنسبة إلى المؤثرات الصوتية القصيرة، من المنطقي تحميلها وفك ضغطها في الذاكرة (قائمة LoadMode.memory الافتراضية).

لديك بعض المشاكل. أولاً، مستوى صوت الموسيقى مرتفع جدًا، ما يغطّي الأصوات. في معظم الألعاب، يتم تشغيل الموسيقى في الخلفية معظم الوقت، ما يمنح الصدارة للمحتوى الصوتي الأكثر إفادةً، مثل التعليقات الصوتية والتأثيرات الصوتية. هذا الإصلاح لاستخدام مَعلمة volume في طريقة التشغيل. على سبيل المثال، يمكنك محاولة _soloud!.play(musicSource, volume: 0.6) لتشغيل الأغنية بمستوى صوت% 60. بدلاً من ذلك، يمكنك ضبط مستوى الصوت في أي وقت لاحق باستخدام عبارة مثل _soloud!.setVolume(_musicHandle, 0.6).

المشكلة الثانية هي أنّ الأغنية تتوقف فجأة. ويعود السبب في ذلك إلى أنّه من المفترض تشغيل الأغنية بشكل متكرّر، ونقطة بدء التكرار ليست بداية ملف الصوت.

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 طرقًا سهلة الاستخدام لتنفيذ ذلك نيابةً عنك. في ما يلي كيفية تخفيف صوت الموسيقى على مدار خمس ثوانٍ ثم إيقافه كي لا يستهلك موارد وحدة المعالجة المركزية (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. تطبيق التأثيرات

من المزايا الكبيرة لاستخدام محرّك صوتي مناسب أنّه يمكنك إجراء معالجة صوتية، مثل توجيه بعض الأصوات من خلال صدى أو معادل صوت أو فلتر مرور منخفض.

في الألعاب، يمكن استخدام هذا الإجراء للتمييز بين المواقع الجغرافية من خلال الأصوات. على سبيل المثال، يختلف صوت التصفيق في الغابة عن صوته في ملجأ خرساني. في حين أنّ الغابة تساعد في تبديد الصوت وامتصاصه، فإنّ الجدران العارية في الملجأ تعكس موجات الصوت، ما يؤدي إلى حدوث صدى. وبالمثل، تبدو أصوات الأشخاص مختلفة عند سماعها من خلال جدار. وتزداد حدة الترددات العالية لهذه الأصوات أثناء انتقالها عبر الوسيط الصلب، ما يؤدي إلى تأثير فلتر تمرير منخفض.

صورة توضيحية لشخصَين يتحدثان في غرفة لا تنتقل موجات الصوت من شخص إلى آخر مباشرةً فقط، بل ترتدّ أيضًا عن الجدران والسقف.

يوفّر 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 بشكل عام.
  • اضبط المَعلمة التأثير على 0.2، ما يعني أنّ الصوت الناتج سيكون بنسبة% 80 صوتًا أصليًا و% 20 من تأثير الصدى. في حال ضبط هذه المَعلمة على 1.0، سيكون الأمر أشبه بسماع موجات الصوت التي ترتدّ إليك من الجدران البعيدة في الغرفة فقط بدون سماع أي صوت أصلي.
  • اضبط المَعلمة Room Size (حجم الغرفة) على 0.9. يمكنك تعديل هذه المَعلمة على النحو الذي تفضّله أو حتى تغييرها ديناميكيًا. 1.0 هو كهف ضخم بينما 0.0 هو حمام.
  • إذا أردت، يمكنك تغيير الرمز وتطبيق أحد الفلاتر التالية أو مجموعة من الفلاتر التالية:
  • biquadFilter (يمكن استخدامه كفلتر مرور منخفض)
  • pitchShiftFilter
  • equalizerFilter
  • echoFilter
  • lofiFilter
  • flangerFilter
  • bassboostFilter
  • waveShaperFilter
  • robotizeFilter

7. تهانينا

نفّذت وحدة تحكّم في الصوت تشغّل الأصوات وتكرّر الموسيقى وتطبّق المؤثرات.

مزيد من المعلومات

  • يمكنك الاستفادة من عناصر التحكّم في الصوت بشكل أكبر من خلال ميزات مثل التحميل المُسبَق للأصوات عند بدء التشغيل أو تشغيل الأغاني تسلسليًا أو تطبيق فلتر تدريجيًا بمرور الوقت.
  • اطّلِع على مستندات حِزم flutter_soloud.
  • راجِع الصفحة الرئيسية لمكتبة C++ الأساسية.
  • اطّلِع على مزيد من المعلومات عن Dart FFI، وهي التكنولوجيا المستخدَمة للتفاعل مع مكتبة C++.
  • يمكنك مشاهدة محادثة "غي سومبرغ" حول برمجة الصوت في الألعاب للحصول على أفكار. (يتوفر أيضًا تقرير أطول). عندما يتحدث "غي" عن "الوسيط"، يعني ذلك مكتبات مثل SoLoud وFMOD. ويكون باقي الرمز مخصّصًا لكل لعبة.
  • أنشئ لعبتك واطرَحها.

صورة توضيحية لسماعات الرأس