alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH 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
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,
})
}