Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / audio.rs

use std::fs::File;
use std::io::BufReader;
use std::num::NonZero;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc::{self, Receiver, SyncSender};
use std::thread::JoinHandle;
use std::time::Duration;

use common::JavaRandom;
use glam::DVec3;
use rodio::mixer::Mixer;
use rodio::{Decoder, DeviceSinkBuilder, Player, Source};
use rustc_hash::FxHashMap;

enum Command {
    /// The listener's position and yaw.
    Tick(DVec3, f32),
    /// A sound at a position, or `SoundManager.playSoundFX` without one.
    Play {
        name: String,
        position: Option<DVec3>,
        volume: f32,
        pitch: f32,
    },
    /// `SoundManager.playRandomMusicIfReady`, once per unpaused world tick.
    Music,
    /// `SoundManager.playStreaming`: a record from a jukebox, or silence for `None`.
    Record {
        name: Option<String>,
        position: DVec3,
    },
}

pub struct Audio {
    sender: Option<SyncSender<Command>>,
    thread: Option<JoinHandle<()>>,
    volumes: Arc<[AtomicU32; 2]>,
}

#[derive(Clone)]
struct Sound {
    /// Interleaved frames of `channels` samples.
    samples: Arc<[f32]>,
    channels: usize,
    rate: NonZero<u32>,
}

impl Sound {
    fn frames(&self) -> usize {
        self.samples.len() / self.channels
    }
}

/// A sound played in stereo: mixed down to mono and panned, or kept as it is for stereo effects.
struct Playback {
    sound: Sound,
    index: usize,
    gains: Arc<[AtomicU32; 2]>,
    stereo: bool,
}

impl Iterator for Playback {
    type Item = f32;
    fn next(&mut self) -> Option<f32> {
        let channels = self.sound.channels;
        let (frame, channel) = (self.index / 2, self.index % 2);
        let samples = self
            .sound
            .samples
            .get(frame * channels..(frame + 1) * channels)?;
        let sample = if self.stereo && channels == 2 {
            samples[channel]
        } else if self.stereo {
            // A centered mono source, as positional voices at the listener
            samples.iter().sum::<f32>() / channels as f32 * std::f32::consts::FRAC_1_SQRT_2
        } else {
            samples.iter().sum::<f32>() / channels as f32
        };
        let gain = f32::from_bits(self.gains[channel].load(Ordering::Relaxed));
        self.index += 1;
        Some(sample * gain)
    }
}

impl Source for Playback {
    fn current_span_len(&self) -> Option<usize> {
        Some((self.sound.frames() * 2).saturating_sub(self.index))
    }
    fn channels(&self) -> NonZero<u16> {
        NonZero::new(2).unwrap()
    }
    fn sample_rate(&self) -> NonZero<u32> {
        self.sound.rate
    }
    fn total_duration(&self) -> Option<Duration> {
        Some(Duration::from_secs_f64(
            self.sound.frames() as f64 / f64::from(self.sound.rate.get()),
        ))
    }
}

/// A decoded stream mixed down to mono and panned like a [`Playback`].
struct Stream {
    decoder: Decoder<BufReader<File>>,
    channels: u16,
    rate: NonZero<u32>,
    sample: f32,
    index: usize,
    gains: Arc<[AtomicU32; 2]>,
}

impl Iterator for Stream {
    type Item = f32;
    fn next(&mut self) -> Option<f32> {
        if self.index.is_multiple_of(2) {
            let mut sum = 0.0;
            for _ in 0..self.channels {
                sum += self.decoder.next()?;
            }
            self.sample = sum / f32::from(self.channels);
        }
        let gain = f32::from_bits(self.gains[self.index % 2].load(Ordering::Relaxed));
        self.index += 1;
        Some(self.sample * gain)
    }
}

impl Source for Stream {
    fn current_span_len(&self) -> Option<usize> {
        None
    }
    fn channels(&self) -> NonZero<u16> {
        NonZero::new(2).unwrap()
    }
    fn sample_rate(&self) -> NonZero<u32> {
        self.rate
    }
    fn total_duration(&self) -> Option<Duration> {
        None
    }
}

struct Voice {
    player: Player,
    gains: Arc<[AtomicU32; 2]>,
    /// `None` for an unattenuated effect that stays with the listener.
    position: Option<DVec3>,
    volume: f32,
}

impl Voice {
    fn new(mixer: &Mixer, position: Option<DVec3>, volume: f32, listener: (DVec3, f32)) -> Self {
        let voice = Self {
            player: Player::connect_new(mixer),
            gains: Arc::default(),
            position,
            volume,
        };
        voice.update(listener);
        voice
    }

    /// Pans and attenuates the voice for the listener's position and yaw.
    fn update(&self, (listener, yaw): (DVec3, f32)) {
        let Some(position) = self.position else {
            for gain in self.gains.iter() {
                gain.store(self.volume.to_bits(), Ordering::Relaxed);
            }
            return;
        };
        let delta = position - listener;
        let distance = delta.length() as f32;
        let radius = 16.0 * self.volume.max(1.0);
        let volume = self.volume.min(1.0) * (1.0 - distance / radius).clamp(0.0, 1.0);
        let right = DVec3::new(
            -f64::from(common::math::cos(yaw.to_radians())),
            0.0,
            -f64::from(common::math::sin(yaw.to_radians())),
        );
        let pan = delta
            .try_normalize()
            .map_or(0.0, |direction| direction.dot(right) as f32)
            .clamp(-1.0, 1.0);
        for (gain, channel) in self.gains.iter().zip([1.0 - pan, 1.0 + pan]) {
            gain.store(
                (volume * (channel * 0.5).sqrt()).to_bits(),
                Ordering::Relaxed,
            );
        }
    }
}

impl Audio {
    pub fn new(assets: &Path, seed: i64) -> Self {
        let (sender, commands) = mpsc::sync_channel(256);
        let volumes = Arc::new([1.0_f32; 2].map(|volume| AtomicU32::new(volume.to_bits())));
        let thread = std::thread::Builder::new()
            .name("audio".into())
            .spawn({
                let (assets, volumes) = (assets.to_owned(), volumes.clone());
                move || mix(&assets, seed, &volumes, commands)
            })
            .inspect_err(|error| tracing::warn!("cannot start audio thread: {error}"))
            .ok();
        Self {
            sender: Some(sender),
            thread,
            volumes,
        }
    }

    pub fn set_volume(&self, music: f32, sound: f32) {
        self.volumes[0].store(music.to_bits(), Ordering::Relaxed);
        self.volumes[1].store(sound.to_bits(), Ordering::Relaxed);
    }

    pub fn click(&self) {
        self.play_fx("random.click", 1.0, 1.0);
    }

    pub fn tick(&self, position: DVec3, yaw: f32) {
        self.send(Command::Tick(position, yaw));
    }

    pub fn music(&self) {
        self.send(Command::Music);
    }

    /// `World.playRecord`: starts the record at the jukebox at `position`, or stops it.
    pub fn record(&self, name: Option<&str>, position: DVec3) {
        let name = name.map(str::to_owned);
        self.send(Command::Record { name, position });
    }

    pub fn play(&self, name: &str, position: DVec3, volume: f32, pitch: f32) {
        self.send(Command::Play {
            name: name.into(),
            position: Some(position),
            volume,
            pitch,
        });
    }

    /// `SoundManager.playSoundFX`: unpositioned at a quarter of the volume.
    pub fn play_fx(&self, name: &str, volume: f32, pitch: f32) {
        self.send(Command::Play {
            name: name.into(),
            position: None,
            volume: volume.min(1.0) * 0.25,
            pitch,
        });
    }

    fn send(&self, command: Command) {
        if let Some(sender) = &self.sender {
            let _ = sender.try_send(command);
        }
    }
}

impl Drop for Audio {
    fn drop(&mut self) {
        self.sender.take();
        if let Some(thread) = self.thread.take() {
            let _ = thread.join();
        }
    }
}

/// The audio thread: plays the commands until the [`Audio`] is dropped.
fn mix(assets: &Path, seed: i64, volumes: &[AtomicU32; 2], commands: Receiver<Command>) {
    let mut output = match DeviceSinkBuilder::open_default_sink() {
        Ok(output) => output,
        Err(error) => {
            tracing::warn!("audio unavailable: {error}");
            return;
        }
    };
    output.log_on_drop(false);
    // Numbered variants share a name: `sound/step/grass1.ogg` is one of `step.grass`
    let mut sounds: FxHashMap<String, Vec<PathBuf>> = FxHashMap::default();
    for folder in ["sound", "newsound"] {
        let root = assets.join(folder);
        for path in files(&root) {
            if let Ok(relative) = path.strip_prefix(&root)
                && let Some(stem) = relative.with_extension("").to_str()
            {
                let name = stem
                    .trim_end_matches(|ch: char| ch.is_ascii_digit())
                    .replace('/', ".");
                sounds.entry(name).or_default().push(path);
            }
        }
    }
    let music: Vec<_> = ["music", "newmusic"]
        .into_iter()
        .flat_map(|folder| files(&assets.join(folder)))
        .collect();
    let open = |path: &Path| Decoder::try_from(File::open(path).ok()?).ok();
    let mut cache: FxHashMap<PathBuf, Sound> = FxHashMap::default();
    let mut voices: Vec<Voice> = Vec::new();
    let mut record: Option<Voice> = None;
    let mut random = JavaRandom::new(seed);
    let mut music_wait = random.next_int(12000);
    let mut music_player: Option<Player> = None;
    let mut listener = (DVec3::ZERO, 0.0);
    while let Ok(command) = commands.recv() {
        voices.retain(|voice| !voice.player.empty());
        let [music_volume, sound_volume] = volumes
            .each_ref()
            .map(|volume| f32::from_bits(volume.load(Ordering::Relaxed)));
        // `SoundManager.onSoundOptionsChanged` stops muted music
        if music_volume == 0.0 {
            music_player = None;
        }
        if let Some(player) = &music_player {
            player.set_volume(music_volume);
        }
        for voice in &voices {
            voice.player.set_volume(sound_volume);
        }
        if let Some(voice) = &record {
            voice.player.set_volume(0.5 * sound_volume);
        }
        match command {
            Command::Tick(position, yaw) => {
                listener = (position, yaw);
                for voice in voices.iter().chain(&record) {
                    voice.update(listener);
                }
                if record.as_ref().is_some_and(|voice| voice.player.empty()) {
                    record = None;
                }
            }
            Command::Music => {
                if record.is_none()
                    && music_player.as_ref().is_none_or(Player::empty)
                    && !music.is_empty()
                    && music_volume > 0.0
                {
                    if music_wait > 0 {
                        music_wait -= 1;
                    } else {
                        music_wait = random.next_int(12000) + 12000;
                        let path = &music[random.next_int(music.len() as i32) as usize];
                        if let Some(decoder) = open(path) {
                            let player = Player::connect_new(output.mixer());
                            player.set_volume(music_volume);
                            player.append(decoder);
                            music_player = Some(player);
                        }
                    }
                }
            }
            Command::Record { name, position } => {
                record = None;
                let Some(name) = name.filter(|_| sound_volume > 0.0) else {
                    continue;
                };
                let path = assets.join("streaming").join(format!("{name}.ogg"));
                let Some(decoder) = open(&path) else {
                    tracing::warn!("cannot play record {}", path.display());
                    continue;
                };
                music_player = None;
                let voice = Voice::new(output.mixer(), Some(position), 4.0, listener);
                voice.player.set_volume(0.5 * sound_volume);
                voice.player.append(Stream {
                    channels: decoder.channels().get(),
                    rate: decoder.sample_rate(),
                    decoder,
                    sample: 0.0,
                    index: 0,
                    gains: voice.gains.clone(),
                });
                record = Some(voice);
            }
            Command::Play {
                name,
                position,
                volume,
                pitch,
            } => {
                if voices.len() >= 256
                    || volume <= 0.0
                    || position.is_some_and(|position| {
                        position.distance(listener.0) >= f64::from(16.0 * volume.max(1.0))
                    })
                {
                    continue;
                }
                let Some(variants) = sounds.get(&name) else {
                    continue;
                };
                let path = &variants[random.next_int(variants.len() as i32) as usize];
                if !cache.contains_key(path) {
                    match decode(path) {
                        Ok(sound) => {
                            cache.insert(path.clone(), sound);
                        }
                        Err(error) => {
                            tracing::warn!("cannot decode {}: {error}", path.display());
                            continue;
                        }
                    }
                }
                let voice = Voice::new(output.mixer(), position, volume, listener);
                voice.player.set_speed(pitch.clamp(0.5, 2.0));
                voice.player.set_volume(sound_volume);
                voice.player.append(Playback {
                    sound: cache[path].clone(),
                    index: 0,
                    gains: voice.gains.clone(),
                    stereo: position.is_none(),
                });
                voices.push(voice);
            }
        }
    }
}

fn files(root: &Path) -> Vec<PathBuf> {
    let mut pending = vec![root.to_owned()];
    let mut paths = Vec::new();
    while let Some(dir) = pending.pop() {
        let Ok(entries) = std::fs::read_dir(dir) else {
            continue;
        };
        for path in entries.flatten().map(|entry| entry.path()) {
            let extension = path.extension().and_then(|ext| ext.to_str());
            if path.is_dir() {
                pending.push(path);
            } else if matches!(extension, Some("ogg" | "wav")) {
                paths.push(path);
            }
        }
    }
    paths.sort();
    paths
}

fn decode(path: &Path) -> Result<Sound, Box<dyn std::error::Error>> {
    let decoder = Decoder::try_from(File::open(path)?)?;
    let rate = decoder.sample_rate();
    let channels = usize::from(decoder.channels().get());
    let mut samples: Vec<f32> = decoder.collect();
    samples.truncate(samples.len() / channels * channels);
    Ok(Sound {
        samples: samples.into(),
        channels,
        rate,
    })
}