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

1. قبل البدء

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

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

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

المتطلبات الأساسية

  • لديهم إلمام أساسي ببرنامج Flutter.
  • معرفة كيفية تشغيل تطبيقات Flutter وتصحيح الأخطاء فيها

المعلومات التي تطّلع عليها

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

ما تحتاج إليه

  • حزمة تطوير البرامج (SDK) مع Flutter
  • أداة تعديل الرموز التي تختارها

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),
        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. بعد تنزيل الملفات الصوتية، أنشِئ دليلاً في جذر مشروعك باسم 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.

pubspec.yaml

...

dependencies:
  flutter:
    sdk: flutter

  flutter_soloud: ^2.0.0
  logging: ^1.2.0

...
  1. شغِّل المشروع. لم تعمل أي أداة حتى الآن لأنّك أضفت الوظائف في الأقسام التالية.

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];

وتأتي هذه النتائج من مكتبة SoLoud C++ الأساسية. وليس لها أي تأثير على الوظائف ويمكن تجاهلها بأمان.

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

  ...

وتدير وحدة التحكّم في الصوت محرّك 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 API أخرى تحمِّل مواد عرض، مثل التطبيق المصغَّر Image.asset().
  • يوفّر مثيل SoLoud طريقة loadAsset() لتحميل ملف صوتي من مواد عرض مشروع Flutter بشكل غير متزامن مع عرض مثيل من الفئة AudioSource. هناك طرق مماثلة لتحميل ملف من نظام الملفات (الطريقة loadFile())، وتحميله عبر الشبكة من عنوان URL (الطريقة loadUrl()).
  • بعد ذلك، يتم تمرير مثيل AudioSource الذي تم الحصول عليه حديثًا إلى طريقة play() في SoLoud. تعرض هذه الطريقة مثيلاً من النوع SoundHandle يمثّل الصوت الذي يتم تشغيله حديثًا. بدوره، يمكن تمرير هذا الاسم المعرِّف إلى طرق SoLoud أخرى لتنفيذ إجراءات مثل إيقاف الصوت مؤقتًا أو إيقافه أو تعديل مستوى صوته.
  • على الرغم من أنّ play() هي طريقة غير متزامنة، يبدأ التشغيل بشكل فوري. تستخدم الحزمة flutter_soloud واجهة الدالة الخارجية (FFI) لـ Dart لاستدعاء الرمز C مباشرةً وبشكل متزامن. فالرسائل المعتادة بين رمز Dart ورمز النظام الأساسي الخاص بمعظم مكوّنات Flutter الإضافية لا تتوفّر في أي مكان. السبب الوحيد الذي يجعل بعض الأساليب غير متزامنة هو أن بعض التعليمات البرمجية للمكون الإضافي تعمل بعزلة خاصة وأن الاتصال بين عناصر عزل Dart غير متزامن.
  • أنت تؤكد ببساطة أن الحقل _soloud ليس فارغًا مع _soloud!. يعد هذا مرة أخرى للإيجاز. يجب أن يتعامل رمز الإنتاج بسلاسة مع الموقف الذي يحاول فيه المطور تشغيل صوت قبل أن تتاح الفرصة لإعداده بشكل كامل لوحدة التحكم في الصوت.

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

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

  • للتعامل مع الاستثناءات في هذه الحالة، عليك إحاطة سطرَي الإجراء 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، ثم استخدام طريقة SoLoud لـ play() لتشغيله.

أما هذه المرة، فستمسك بمقبض الصوت الذي تعرضه طريقة 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);
    _musicHandle = await _soloud!.play(musicSource);
  }

...

لاحِظ أنّك حمّلت ملف الموسيقى في وضع القرص (التعداد LoadMode.disk). هذا يعني ببساطة أنه يتم تحميل الملف فقط في أجزاء حسب الحاجة. بالنسبة إلى المقاطع الصوتية التي تعمل لمدة أطول، يُفضّل عمومًا تحميلها في وضع القرص. بالنسبة إلى المؤثرات الصوتية القصيرة، من الأسهل تحميلها وفك ضغطها في الذاكرة (التعداد التلقائي لـ LoadMode.memory).

لكن لديك مشكلتان. أولاً، تُصدر الموسيقى صوتًا عاليًا جدًا، وتتجاوز هذه الأصوات الأصوات. في معظم الألعاب، يتم تشغيل الموسيقى في الخلفية في معظم الأوقات، ما يمنح الأولوية للمحتوى الصوتي الأكثر ثراءً بالمعلومات، مثل الكلام والمؤثرات الصوتية. ويمكن إصلاح هذا بسهولة باستخدام معلَمة مستوى الصوت في طريقة التشغيل. يمكنك مثلاً تجربة _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، وخفض مستوى صوت الموسيقى بفواصل زمنية صغيرة. سيعمل ذلك، ولكنه يتطلب الكثير من العمل.

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

  • لإظهار أنّ اللاعب في غرفة كبيرة، مثل كاتدرائية أو كهف، استخدِم تعداد 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);
  }

...

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

باستخدام الرمز السابق، يمكنك إجراء ما يلي:

  • تفعيل فلتر الفعل الحر بشكل عام أو على مزيج الصوت بأكمله، وليس صوتًا واحدًا فقط.
  • يجب ضبط المعلَمة Wet على 0.2، ما يعني أنّ الصوت الناتج سيكون أصليًا بنسبة 80% ومخرجات تأثير الارتداد بنسبة 20%. إذا ضبطت هذه المَعلمة على 1.0، سيكون الأمر بمثابة سماع الموجات الصوتية فقط التي ترجع إليك من الجدران البعيدة للغرفة وليس الصوت الأصلي.
  • اضبط مَعلمة حجم الغرفة على 0.9. ويمكنك تعديل هذه المَعلمة كما تشاء أو حتى تغييرها ديناميكيًا. ويشكّل 1.0 كهفًا ضخمًا، بينما يضمّ 0.0 حمّامًا.
  • إذا أردت ذلك، فغيّر الرمز وطبّق أحد الفلاتر التالية أو مجموعة من الفلاتر التالية:
  • FilterType.biquadResonantFilter (يمكن استخدامه كفلتر للتذاكر المنخفضة)
  • FilterType.eqFilter
  • FilterType.echoFilter
  • FilterType.lofiFilter
  • FilterType.flangerFilter
  • FilterType.bassboostFilter
  • FilterType.waveShaperFilter
  • FilterType.robotizeFilter
  • FilterType.freeverbFilter

7. تهانينا

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

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

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

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