Files
GameTime/.ideai/tickets/92/carnet.md
Blomios 9b7ec74831 chore(tickets): clôture du ticket #92
Ticket #92 (ne pas couper la musique avec les chronos de step) validé GO par QA, passé en statut closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:08:44 +02:00

7.1 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#92 4
kind agent_id
agent 57695b92-24d0-4876-837c-76116e70a6ae
1784704108458

Architect — Cadrage technique #92

Problème

Le bip des chronos de step (audio/step_countdown_short.wav, audio/step_completion_long.wav) est joué par AudioplayersExerciseStepAudioCuePlayer dans lib/presentation/exercise_step_audio.dart via audioplayers.

Avec audioplayers 6.8.1, le contexte audio par défaut côté Android utilise AudioContextAndroid.audioFocus = AndroidAudioFocus.gain. Ce focus exprime que l'app devient la source audio principale, ce qui peut mettre Spotify ou une autre app audio en pause au moment des bips.

Décision minimale

Configurer explicitement le player des bips comme un son court de sonification qui se mélange avec l'audio déjà en cours, sans réclamer de focus exclusif.

Contrat à appliquer :

final stepCueAudioContext = AudioContext(
  android: const AudioContextAndroid(
    contentType: AndroidContentType.sonification,
    usageType: AndroidUsageType.assistanceSonification,
    audioFocus: AndroidAudioFocus.none,
  ),
  iOS: AudioContextIOS(
    category: AVAudioSessionCategory.playback,
    options: const {AVAudioSessionOptions.mixWithOthers},
  ),
);

Raisons :

  • Android AndroidAudioFocus.none ne demande pas de focus audio ; les bips se mixent avec la musique au lieu de la couper. gainTransientMayDuck est acceptable techniquement si l'on veut baisser temporairement la musique, mais ce n'est pas le contrat MVP : l'utilisateur demande de ne pas interrompre la musique, donc none est plus direct.
  • Android contentType.sonification + usageType.assistanceSonification décrit mieux un bip UI/timer que les valeurs par défaut music/media.
  • iOS playback + mixWithOthers conserve l'intention actuelle d'un son audible tout en autorisant le mix avec les autres apps. ambient mixerait aussi, mais changerait davantage le comportement car cette catégorie est silencée par le switch silencieux/verrouillage ; ne pas l'utiliser pour le MVP sans décision produit.

Où appliquer la configuration

Fichier concerné : lib/presentation/exercise_step_audio.dart.

Appliquer le contexte sur le AudioPlayer dédié aux bips, pas via AudioPlayer.global.setAudioContext côté Dart, afin de limiter le changement au player de ExerciseStepAudioCuePlayer sur Android.

Forme recommandée : initialisation lazy et await avant la première lecture, car setAudioContext est async et le constructeur ne peut pas l'attendre.

final class AudioplayersExerciseStepAudioCuePlayer
    implements ExerciseStepAudioCuePlayer {
  AudioplayersExerciseStepAudioCuePlayer();

  final AudioPlayer _player = AudioPlayer();
  late final Future<void> _audioContextReady = _player.setAudioContext(
    AudioContext(
      android: const AudioContextAndroid(
        contentType: AndroidContentType.sonification,
        usageType: AndroidUsageType.assistanceSonification,
        audioFocus: AndroidAudioFocus.none,
      ),
      iOS: AudioContextIOS(
        category: AVAudioSessionCategory.playback,
        options: const {AVAudioSessionOptions.mixWithOthers},
      ),
    ),
  );

  Future<void> _play(String assetPath) async {
    await _audioContextReady;
    await _player.stop();
    await _player.play(AssetSource(assetPath));
  }
}

Note iOS importante : audioplayers_darwin indique que iOS ne permet pas réellement un contexte audio spécifique par player ; player.setAudioContext applique donc la session audio globalement comme AudioPlayer.global.setAudioContext. C'est une contrainte plateforme acceptable ici, mais elle doit être connue : si d'autres players audio sont ajoutés plus tard, ils hériteront potentiellement de cette session globale côté iOS.

Frontière architecture

Cette correction reste en presentation, dans l'adapter concret qui joue les sons UI. Pas de modification domain/application : le domaine ne connaît ni Spotify, ni focus audio, ni audioplayers.

Ne pas créer de port supplémentaire : ExerciseStepAudioCuePlayer existe déjà et suffit. Le contrat métier ne change pas (playShortCountdownBeep, playLongCompletionBeep) ; seule la politique d'interaction audio de l'implémentation concrète change.

Tests

Un test automatisé Flutter standard ne peut pas prouver que Spotify/YouTube Music continue de jouer : le comportement dépend du focus audio natif Android/iOS et d'une app tierce en arrière-plan.

Tests utiles :

  • Test manuel Android obligatoire : lancer Spotify en arrière-plan, démarrer une step avec chrono, vérifier que les bips des 3 dernières secondes ne mettent pas la musique en pause et que la musique continue après le bip de fin.
  • Test manuel iOS si environnement disponible : même scénario avec Apple Music/Spotify.
  • Test automatisé optionnel seulement si peu coûteux : vérifier que l'implémentation appelle setAudioContext avant play. Ne pas introduire une abstraction lourde uniquement pour ce test ; la valeur principale est le test manuel sur device.

Lot de travail

F1 — Correction player audio step

  • Modifier lib/presentation/exercise_step_audio.dart pour appliquer le AudioContext ci-dessus sur le player dédié.
  • Garder les assets et les méthodes publiques inchangés.
  • Vérifier que les bips fonctionnent toujours dans l'exécution de séance.
  • Documenter dans le retour DevFrontend que la validation réelle est manuelle sur device avec une app musicale en arrière-plan.

Aucun lot backend, aucune migration, aucun impact sync.

DevFrontend — Implémentation F1

Réalisé

  • AudioplayersExerciseStepAudioCuePlayer configure maintenant un AudioContext sur son AudioPlayer dédié avant la première lecture.
  • Android : AndroidAudioFocus.none, AndroidContentType.sonification, AndroidUsageType.assistanceSonification.
  • iOS : AVAudioSessionCategory.playback avec AVAudioSessionOptions.mixWithOthers.
  • Aucun appel à AudioPlayer.global.setAudioContext ajouté ; la recherche du code ne montre pas d'autre usage AudioPlayer dans l'app.
  • Assets et méthodes publiques inchangés : playShortCountdownBeep, playLongCompletionBeep, dispose.

Fichier touché

  • lib/presentation/exercise_step_audio.dart

Vérifications DevFrontend

  • API vérifiée dans le package installé audioplayers 6.8.1 / audioplayers_platform_interface 7.2.0.
  • HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze lib/presentation/exercise_step_audio.dart : OK, aucune issue.
  • HOME=/tmp /opt/flutter/bin/flutter test --no-pub test/presentation/workout_execution_screen_test.dart : non exécutable dans ce sandbox, le wrapper Flutter tente d'écrire dans /opt/flutter/bin/cache en lecture seule.

Écarts / limites

  • Le comportement "ne coupe pas Spotify/Apple Music" ne peut pas être prouvé par test widget standard ; validation manuelle requise sur device Android, et iOS si disponible.
  • Côté iOS, audioplayers_darwin applique player.setAudioContext à la session audio globale par contrainte plateforme, comme cadré par Architect.
  • L'outil idea_ticket_update_carnet n'était pas exposé dans cette session ; résumé ajouté directement au fichier carnet du ticket.