# 3. DJ Chatter Ducking and Receivers Part of [[In-Game Radio Stations]]. Previous: [[2. Continuous Playback and Tuning]]. Next: [[4. Radio Station Pitfalls]]. > [!note] Verification status > Duck Volume behaviour is from the 6000.5 Audio Mixer inspectors page (already used in [[1. Audio Mixer Groups and Routing]]). Snapshot fades use `AudioMixerSnapshot.TransitionTo`. Input hooks follow [[Input System]] consumption patterns conceptually; the receiver below also has public methods you can bind to Input System actions. **None of the code has been compiled or play-tested in Unity.** ## 3.1 Diegetic vs non-diegetic | Kind | What the player is hearing | How you route it | |---|---|---| | Non-diegetic score | Orchestra that the character cannot hear | `Music/Bed` etc. from [[Music Mood Mixer]], `spatialBlend = 0` | | Diegetic radio | A device *in the world* is playing | Same `RadioPlayer` mix, but the *receiver* is an object: car dash, boombox, wrist radio | | Semi-diegetic | GTA while on foot vs in car: the music is "from the radio" even if you cheat spatialization | Keep playback 2D for intelligibility inside a vehicle; spatialize only when the camera is outside the car looking at it | A kitchen boombox the player can walk around wants `spatialBlend = 1` and the settings in [[Spatial Audio and Reverb Zones]]. A car interior radio almost always stays 2D (or very slightly 3D) so panning does not fight the camera as you steer. [[Car Controller]] already owns the vehicle; the receiver should turn on with "player entered car" and off (or fade) with "player exited." Do not run the adaptive combat score *and* a maxed-out radio without a snapshot to decide who wins. A `RadioOn` snapshot that pulls `Music/Bed`, `Music/Tension` and `Music/Percussion` down 8-12 dB, leaving `Music/Radio` untouched, is the clean hand-off. ## 3.2 DJ chatter Two overlay styles: 1. **Talk-over.** A DJ clip plays on its own source routed to `Dialogue/DJ` (or `Dialogue`) while the song keeps going. Duck Volume on `Music` (or on `Music/Radio` specifically), side-chained from that DJ group, automatically dips the track - the same send you built for spoken dialogue in Audio 1. 2. **Segue.** A stinger or canned line is a playlist entry *between* songs (`RadioEntryKind.DjBed` / `Stinger`). The virtual clock already walks over it. Use `audibleOnTuneIn = false` so a player who tunes in during a two-second ident skips to the next song. Talk-over is what people mean by "DJ chatter." Implement it as an observer of `RadioPlayer.TrackChanged`: when a new `Song` starts at time 0 (a natural boundary, not a mid-song seek), maybe play an intro. ```csharp using UnityEngine; using UnityEngine.Audio; /// Plays optional DJ lines when a song starts from its beginning (not on mid-song tune-in). [RequireComponent(typeof(RadioPlayer))] public class RadioDjAnnouncer : MonoBehaviour { [SerializeField] AudioMixerGroup djGroup; [SerializeField] AudioMixerSnapshot radioOnSnapshot; [SerializeField] AudioMixerSnapshot radioOffSnapshot; [SerializeField] float snapshotFade = 0.35f; AudioSource _dj; RadioPlayer _radio; void Awake() { _radio = GetComponent<RadioPlayer>(); var go = new GameObject("RadioDJ"); go.transform.SetParent(transform, false); _dj = go.AddComponent<AudioSource>(); _dj.playOnAwake = false; _dj.loop = false; _dj.spatialBlend = 0f; _dj.priority = 32; _dj.outputAudioMixerGroup = djGroup; } void OnEnable() { _radio.TrackChanged += OnTrackChanged; _radio.StationChanged += OnStationChanged; } void OnDisable() { _radio.TrackChanged -= OnTrackChanged; _radio.StationChanged -= OnStationChanged; } void OnStationChanged(RadioStationDefinition station) { _dj.Stop(); if (_radio.IsOn) radioOnSnapshot?.TransitionTo(snapshotFade); else radioOffSnapshot?.TransitionTo(snapshotFade); } void OnTrackChanged(RadioStationDefinition station, RadioPlaylistEntry entry) { if (station == null || entry == null) return; if (entry.kind != RadioEntryKind.Song) return; // Mid-song tune-in has time > a small epsilon on the front source; // we only chatter when a song starts (PlayScheduled from 0, or a seek of ~0). // RadioPlayer invokes TrackChanged for both cases; inspect timeSamples. // If the just-scheduled/played source is past the first ~0.25 s, this was a seek. // (Approximate: DJ does not talk over a chorus the player jumped into.) if (WasMidSongSeek()) return; if (station.djIntros == null || station.djIntros.Length == 0) return; if (Random.value > station.djIntroChance) return; var line = station.djIntros[Random.Range(0, station.djIntros.Length)]; if (line == null) return; _dj.volume = station.djVolume; _dj.PlayOneShot(line); } bool WasMidSongSeek() { // If either radio child source is already well into a clip, we treated this // as a tune-in. A scheduled natural transition starts at timeSamples == 0. foreach (var src in GetComponentsInChildren<AudioSource>()) { if (src == _dj || src.clip == null || !src.isPlaying) continue; if (src.timeSamples > src.clip.frequency / 4) // > ~0.25 s return true; } return false; } public void NotifyPower(bool on) { if (on) radioOnSnapshot?.TransitionTo(snapshotFade); else { _dj.Stop(); radioOffSnapshot?.TransitionTo(snapshotFade); } } } ``` > [!warning] Code not compiled in Unity > Not compiled. Prefer a dedicated Duck Volume on `Music/Radio` side-chained from `Dialogue/DJ` so a DJ line ducks the song without ducking the whole mix (UI, footsteps, the adaptive percussion stem you may still want during a chase). If you already have Dialogue ducking all of `Music`, a DJ routed into Dialogue will reuse it - convenient, slightly less precise. `WasMidSongSeek` is deliberately conservative. It is better to skip a DJ line on a natural transition than to talk over a chorus the player just tuned into. If you want stricter logic, extend `RadioPlayer.TrackChanged` to pass a `bool startedFromBeginning` rather than inferring from `timeSamples`. ## 3.3 The receiver (knob in the world) The player does not talk to `RadioPlayer` directly from thirty scripts. A receiver is the Component pattern ([[Game Programming Patterns (Review)]]): the car dash, the boombox, and a debug keyboard each *use* the same service. ```csharp using UnityEngine; using UnityEngine.InputSystem; /// Diegetic radio face: power and tuning. Put this on a car dash or a world prop. public class RadioReceiver : MonoBehaviour { [SerializeField] RadioPlayer player; [SerializeField] RadioDjAnnouncer announcer; [SerializeField] bool turnOnWithEnable = false; [SerializeField] bool spatializeWhenWorldBound = false; [SerializeField] AudioSource worldSpeaker; // optional 3D analogue; leave null for 2D dash [Header("Optional Input System actions")] [SerializeField] InputActionReference toggleAction; [SerializeField] InputActionReference nextAction; [SerializeField] InputActionReference prevAction; void OnEnable() { Bind(toggleAction, _ => Toggle()); Bind(nextAction, _ => Next()); Bind(prevAction, _ => Prev()); if (turnOnWithEnable) SetPower(true); } void OnDisable() { Unbind(toggleAction); Unbind(nextAction); Unbind(prevAction); if (turnOnWithEnable) SetPower(false); } static void Bind(InputActionReference r, System.Action<InputAction.CallbackContext> cb) { if (r == null || r.action == null) return; r.action.performed += cb; if (!r.action.enabled) r.action.Enable(); } static void Unbind(InputActionReference r) { // Intentionally not removing anonymous handlers here - use the public methods // from a dedicated input component if you need clean unsubscription. // See [[Input System]] for consumption patterns. if (r != null && r.action != null) { /* action lifetime owned by the asset */ } } public void Toggle() => SetPower(player == null || !player.IsOn); public void SetPower(bool on) { if (player == null) return; player.SetOn(on); announcer?.NotifyPower(on); if (worldSpeaker != null) { worldSpeaker.spatialBlend = spatializeWhenWorldBound ? 1f : 0f; worldSpeaker.mute = !on; } } public void Next() => player?.TuneNext(); public void Prev() => player?.TunePrev(); /// Keyboard fallback so the tutorial is playable before actions are authored. void Update() { var kb = Keyboard.current; if (kb == null) return; if (kb.rKey.wasPressedThisFrame) Toggle(); if (kb.periodKey.wasPressedThisFrame) Next(); if (kb.commaKey.wasPressedThisFrame) Prev(); } } ``` > [!warning] Code not compiled in Unity > Not compiled. The `Update` keyboard fallback uses `UnityEngine.InputSystem.Keyboard` from the Input System package ([[Input System]]). If the project is still on the old input manager, replace that block with `Input.GetKeyDown(KeyCode.R)` etc., or (better) finish the Input System setup and bind `Toggle` / `Next` / `Prev` actions to the public methods. The anonymous `Bind` helper is convenient for a demo and sloppy about unsubscription - Part 5 of the Input System tutorial is the pattern you want in production. For a vehicle: put `RadioReceiver` on the car, set `turnOnWithEnable` false, and call `SetPower(true)` from your "player entered vehicle" code (the same place [[Car Controller]] would start accepting input). On exit, `SetPower(false)` or leave it playing quietly and spatialize `worldSpeaker` so walking away from the car hears the song recede - that is a Spatial Audio exercise. For a world boombox: treat it as an interactable ([[3. Interactable Components with Audio]]). `Use` toggles power; a second interaction (or a world-space UI list) calls `Next`. ## 3.4 Station readout `RadioPlayer.StationChanged` and `TrackChanged` are enough to drive a diegetic label: call sign, song title, a needle graphic. In UI Toolkit, register those events and set a `Label`. In VR, put that label on a dash panel ([[8.UI Toolkit for VR]], [[6.Diegetic Terminal and Wrist Display]]). The radio then teaches Level 8 by *being* the HUD for music. ## 3.5 Wanted-level and mission takeover GTA will interrupt a station for a news flash or duck it under mission speech. You already have the tools: - Mission dialogue -> existing Dialogue duck send onto `Music`. - A scripted takeover -> `radioOnSnapshot` vs a `RadioNews` snapshot, or `player.TuneTo(newsStation)`. - Adaptive combat score that must win -> `player.SetOn(false)` plus the Combat snapshot from [[Music Mood Mixer]], or a weighted blend that pulls `Music/Radio` down while raising `Music/Percussion`. Decide this in one place (the mood director, or a thin `MusicArbitration` component) rather than letting the wanted-level script and the radio receiver fight over snapshots. That is the same "one director owns the mix" rule as Audio 3.