الجوال والشاشات

النداء المسموع (الصوت والنطق)

تطبيق نداء الطلاب يُعلن اسم الطفل حين يصل وليّ أمره إلى البوّابة، وإعداداته تعرض على المدرسة أربعة أصوات: جرسٌ لطيف، والاسم منطوقاً بصوت صناعي، وتسجيلٌ سجّلته المدرسة، وتسجيلٌ سجّله وليّ الأمر. في المتصفّح كانت الأربعة تعمل. وعلى السطحين اللذين يهمّان — الشاشة على الحائط وهاتف الموظّف — كان يعمل الجرس وحده، لأن سطح الصوت في المضيف كان Okta.playSound(name) كلّه: مفردات مغلقة من ثلاث نغمات نظام. ثلاثة أوضاع من أربعة لم يكن لها طريق أصلاً.

العقد 23 يفتح الطريق بأربعة نداءات.

الأربعة، وقدرتان لا واحدة

النداء ما يفعله ماذا تعني false
Future<bool> Okta.playAudio(String url) تشغيل ملف صوتي من رابط يخدمه خادمك أنت لم يبدأ التشغيل: رابط غير صالح، أو ملف لا يُبلَغ، أو مكبّر صوت لا يستجيب
void Okta.stopAudio() إسكات ما يُسمَع الآن — الملف والنطق معاً — (لا تعيد شيئاً، ولا تفشل)
Future<bool> Okta.speak(String text) نطق نصّ بصوت الجهاز لم يبدأ النطق: لا محرّك نطق، أو لا صوت للّغة المطلوبة
Future<bool> Okta.canSpeak() هل يستطيع هذا الجهاز النطق؟ لا يستطيع — وهذا جواب سليم لا عطل

وقدرتان لا واحدة، لأن الأوضاع أشياء مختلفة: التسجيلان — تسجيل المدرسة وتسجيل وليّ الأمر — يحتاجان ملفاً يُشغَّل (playAudio)، والصوت الصناعي وحده يحتاج نطقاً (speak). فتطبيقٌ لا يعرض إلا التسجيلات لا يحتاج محرّك نطق أن يوجد على الجهاز إطلاقاً، ولا يجوز أن يسأل عنه ولا أن يعطّل نفسه لغيابه.

اسأل canSpeak() قبل أن تعرض «النداء بالاسم» — وهذا هو بيت القصيد

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

false حالةٌ عادية على جهاز سليم. ليست عطلاً، وليست شيئاً يُعاد المحاولة عليه — إعادة السؤال بعد ثانيتين تُجيب false نفسها إلى أن يثبّت أحدٌ صوتاً على الجهاز. الجواب الصحيح نزولُ درجة: التسجيل، ثم الجرس.

واسأله في موضعين: عند رسم إعدادات تطبيقك كي لا تعرض على مدرسةٍ خياراً لا يعمل على شاشتها، وعند النداء نفسه كي لا تفترض أن ما كان صحيحاً وقت الإعداد ما زال صحيحاً.

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

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

لا شيء يرمي — كل جواب bool

مكبّر صوت ميّت، و404 على التسجيل، ومحرّك نطق غائب: كلّها false. لا استثناء يصعد إليك من أيٍّ من الأربعة.

وهذا مقصود لأن الجمهور جدار: على شاشة لا يقف خلفها أحد، الاستثناءُ شاشةٌ حمراء لا يمسحها أحد حتى نهاية اليوم الدراسي. فافحص القيمة المعادة ولا تكتب try حول النداء.

playAudio يأخذ رابطاً يخدمه خادمك أنت

الملف يجلبه مشغّل الصوت في المنصّة، لا العميل المُصادَق: لا bearer ولا ترويسة جهة تركب إلى مضيف ملفات شريك. أي أن الرابط يبلغ ما كان الجهاز ليبلغه مجهولاً، والنداء يعيد bool لا بايتات.

عملياً:

  • رابط مطلق http(s) فقط، وfile:/// وأخواتها مرفوضة.
  • الملف يجب أن يُخدَم بلا مصادقة أوكتا. ورابطٌ موقَّع قصير العمر يُصدره خادمك أنت حلٌّ سليم؛ ونقطةٌ تنتظر توكن أوكتا ليست حلاً — ستُجيب 401 وتصلك false.
  • إن كان التسجيل يعيش خلف نطاق تطبيقك، فأخرجه إلى مسار عام يخدمه خادمك، ولا تُمرّر مساراً نسبياً على /api/…: هذا النداء ليس Okta.get.

صوت واحد في القاعة

نداء playAudio ثانٍ يستبدل ما يُسمَع، وstopAudio() تُسكت المشغّل والنطق معاً. لا تختلط عندك قناتان.

مكبّر صوت واحد على حائط، واسمان يتداخلان في ردهة أسوأ من اسم واحد يصل متأخّراً. فإن وصلك نداءان متتاليان فآخرهما هو الذي يُسمع، وهذا هو السلوك الذي تريده من تلقاء نفسه.

true تعني «بدأ» لا «انتهى»

ليس على هذا السطح إشارةُ انتهاء. الـFuture يُحلّ حين يبدأ الصوت، فلا تستطيع اليوم أن تسلسل نداءين — «اقرع الجرس ثم انطق الاسم» ليس شيئاً يمكن التعبير عنه، لأن await على playAudio لا ينتظر انتهاء المقطع.

قيدٌ معروف نقوله الآن بدل أن تكتشفه بطابور نداءات يدهس بعضه بعضاً على حائط مدرسة. وإن احتجت تتابعاً فرتّبه بمهلة تقدّرها أنت، واعلم أنها تقدير.

ولماذا قد تتأخّر false

playAudio قد تُجيب false متأخّرة حين لا يُبلَغ الخادم: المشغّل ينتظر مهلة تجهيزه حتى نهايتها.

وهذا اختيار لا إغفال. جوابُ «لا» سريع يتبعه صوتٌ متأخّر كان سيجعل الحائط يشغّل الجرس البديل والتسجيل معاً — أي أن السرعة هنا تشتري بالضبط ما يمنعه القسم أعلاه.

playSound باقية ولم تُستبدَل

الجرس تأكيد بمفردات مغلقة يملكها المضيف (success · error · warning · chime، ومعها اهتزاز)، وهذا السطح نداء. شيئان مختلفان، ولذلك لم يُحذف الأول.

والجرس هو ما تسقط إليه حين لا يكون على الجهاز صوت ولا يُبلَغ التسجيل — موجودٌ على كل عقد منذ 5. لكن success نقرةُ النظام، ونقرة النظام لا تُسمَع على Windows — أي على حاسوب البوّابة بالذات. هناك نادِ chime.

chime — نغمة «مرّ طالبٌ وحُسب». نغمةٌ قصيرة من نوتتين يشحنها تطبيق أوكتا نفسه، ووُجدت لأن المرور السليم على حاسوب البوّابة كان صامتاً. ولم تُغيَّر success: ذلك كان سيغيّر صوت كل تطبيق منشور دفعةً واحدة ولا شيء في أيٍّ منها طلبه — فمن يريد النغمة يقولها.

  • ليست تغيير عقد. كلمةٌ في مفردات المضيف لا رمزٌ في مكتبة محقونة، فلا ترفع minContract لأجلها ولا تفحص شيئاً قبلها. مضيفٌ أقدم منها يعاملها اسماً مجهولاً، وذلك في تطبيق أوكتا نقرةُ success نفسها — السقوط الصحيح لا عطل.
  • لا تكون أهدأ من success أبداً. حين لا تُعزف — لا مخرج صوت، أو آيفون (جلسة الصوت فيه واحدة للتطبيق كلّه، فتبقى النقرة) — تُعزف النقرة بدلها. وعلى أندرويد تحترم وضع الصامت، وتخفض ما يُعزف تحتها ولا تقطعه.
  • مشغّلها مستقلّ: لا تقطع إعلاناً من playAudio/speak، ولا تُسكتها stopAudio(). ونداءٌ ثانٍ قبل أن تنتهي يعيدها من أوّلها.
  • استعملها لما تعنيه وحده: «رأيناه عند البوّابة». مرورٌ حُسب — حضورٌ أو انصراف، ومتأخّرٌ كذلك لأن المرور حُسب؛ والحكم يقوله النصّ لا الصوت. وطالبٌ حُسب مروره من قبل اليوم ثم عاد إلى الكاميرا أو نقر بطاقته تُسمعه النغمة نفسها مع بطاقةٍ تقول «مسبقاً» (هذا ما تفعله «حضور» منذ 1.6.44 بطلب المدارس: الصمت عند البوّابة يُقرأ «البوّابة توقّفت») — ولا تُسجّله ثانيةً، واللون أو النصّ يقول أيّهما. الحفظ والإعدادات تبقى على success: نغمةٌ تُسمَع مع كل حفظ لا تعود تقول شيئاً عند البوّابة. والتكرار الآليّ (البطاقة نفسها في مدى القارئ، وقارئ UHF الذي يقرأ ما في مداه مرّات في الثانية) لا يُسمَع أصلاً — اصمت عنه في كودك.

minContract: 23 — ومتى لا ترفعه

تطبيق يُعلن minContract: 23 يُرفَض على مضيف أقدم بشاشة «حدِّث التطبيق» — وهو الرفض الصحيح، لأن البديل موتٌ في الترجمة على تلفاز في مدرسة (Cannot find static method Okta.speak).

لا ترفع الأرضية إلى 23 إلا إن كنت تنادي واحداً من الأربعة فعلاً. تطبيق لا يُصدر صوتاً لا يتحرّك، ورفعُ الأرضية بلا حاجة لا يجعله أحدث — يجعله يرفض أجهزة كان يعمل عليها.

وتطبيقٌ يريد التسجيلات وحدها يحتاج 23 كذلك: playAudio جديدة هي الأخرى، والجديدُ في مكتبة محقونة أرضيةٌ أياً كان الغرض منه.

المحاكي يجيب false عن الأربعة — عمداً

على «الجهاز الافتراضي» تُجيب الأربعة false دائماً، مع أن تبويب متصفّح يستطيع تشغيل <audio> واستعمال Web Speech.

السبب أن محاكياً يقول canSpeak() == true دائماً لا يُشغّل أبداً مسار السقوط الذي يطلبه منك هذا الفصل — وأول مكان تكتشف فيه أنك لم تكتبه يكون قاعةً ملأى بأولياء الأمور. فاختبر في المحاكي أن تطبيقك ينزل الدرجات بلا رسالة خطأ وبلا شاشة فارغة، ثم اختبر المسار المنطوق على جهاز حقيقي أو شاشة مقترنة.

مثال: نداء البوّابة

استطلاع، ثم نطق، وإلا تسجيل المدرسة، وإلا الجرس. لاحظ أن كل درجة تسقط بصمت إلى التي تحتها: لا toast ولا رسالة عطل — لأن ما وقع ليس عطلاً.

// mode: 'voice' | 'recording' | 'bell' — إعداد تختاره المدرسة.
Future<void> announce(String name, String mode, String recordingUrl) async {
  // اقطع النداء السابق: مكبّر صوت واحد، واسمان متداخلان أسوأ من اسم متأخّر.
  Okta.stopAudio();

  if (mode == 'voice') {
    final bool can = await Okta.canSpeak();
    if (can) {
      final bool spoke = await Okta.speak('وصل وليّ أمر $name');
      if (spoke) return;
    }
    // لا صوت على هذا الجهاز — انزل درجة، ولا تُبلّغ أحداً بعطل لم يقع.
  }

  if (recordingUrl.isNotEmpty) {
    final bool played = await Okta.playAudio(recordingUrl);
    if (played) return;
  }

  // الجرس: القاع الذي لا يسقط منه شيء — لكنه نقرة النظام، ولا تُسمَع على
  // Windows. وفي تطبيق أوكتا على ويندوز يُسمَع `chime` بدلها.
  Okta.playSound('success');
}

وفي إعداداتك، لا تعرض ما لا يعمل:

class _SettingsState extends State<Settings> {
  bool _voiceAvailable = false;

  @override
  void initState() {
    super.initState();
    _probe();
  }

  Future<void> _probe() async {
    final bool can = await Okta.canSpeak();
    setState(() { _voiceAvailable = can; });
  }

  // ... وفي build: اعرض خيار «النداء بالاسم» حين _voiceAvailable وحده،
  // واعرض بدلاً منه سطراً يقول إن هذا الجهاز بلا صوت مثبَّت.
}

ووصلُه بالرسالة الفورية — الشاشة تسمع، فتنادي، والاسم يصل من الحمولة:

Okta.onMessage((String event, Map data) {
  if (event != 'guardian.arrived') return;
  final dynamic v = data['student_name'];
  setState(() { _name = '$v'; });
  announce('$v', _mode, _recordingUrl);
});

وتبقى قاعدة الفصل السابق كما هي: لا تنتظر رسالةً لترسم. النداء المسموع إعلانٌ فوق حالةٍ قرأتها بـOkta.get، لا بديلٌ عنها.

لا مفتاح في البيان، ولا نطاق

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

الشيء الوحيد الذي يتحرّك هو minContract — فلا تبحث عن مفتاح تضيفه.

عرض الدليل كاملاً في صفحة واحدة · ↧ Markdown خام