أضِف صوتًا وموسيقى إلى لعبتك على 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;sSounds&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، ثم استخدام طريقة 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);
    _musicHandle = await _soloud!.play(musicSource);
  }

...

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

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

  • لإظهار أنّ اللاعب في غرفة كبيرة، مثل كاتدرائية أو كهف، استخدِم الحقل 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 المقابل الذي يؤدي الإجراء نفسه.

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

  • تفعيل فلتر الفعل الحر بشكل عام.
  • اضبط المَعلمة Wet على 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. يميل باقي التعليمات البرمجية إلى أن يكون خاصًا بكل لعبة.
  • أنشئ لعبتك واطرَحها.

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