#[cfg(test)]
#[path = "../../tests/interfaces/engine_sound.rs"]
mod tests;
use crate::entities::Entity;
use crate::math::Vector;
use crate::user_messages::Recipients;
use sdk_raw::interfaces::engine_sound::{
CHAN_AUTO, CHAN_BODY, CHAN_ITEM, CHAN_STATIC, CHAN_STREAM, CHAN_VOICE, CHAN_VOICE2,
CHAN_WEAPON, DEFAULT_SPECIAL_DSP, NO_SPEAKER_ENTITY, PITCH_HIGH, PITCH_LOW, PITCH_NORM,
SND_CHANGE_PITCH, SND_CHANGE_VOL, SND_DELAY, SND_DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL,
SND_IGNORE_NAME, SND_IGNORE_PHONEMES, SND_NOFLAGS, SND_SHOULDPAUSE, SND_SPEAKER, SND_STOP,
SND_STOP_LOOPING, SOUND_FROM_WORLD, VOL_NORM,
};
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
use std::num::NonZero;
use std::ptr::{self, null_mut};
const MAX_CHANNEL: c_int = CHAN_VOICE2;
interface! {
#[doc(alias("IEngineSound"))]
pub struct EngineSound(sys::IEngineSound) = Engine sdk_raw::interfaces::engine_sound::VERSION;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Channel(c_int);
impl Channel {
#[doc(alias("CHAN_AUTO"))]
pub const AUTO: Self = Self(CHAN_AUTO);
#[doc(alias("CHAN_BODY"))]
pub const BODY: Self = Self(CHAN_BODY);
#[doc(alias("CHAN_ITEM"))]
pub const ITEM: Self = Self(CHAN_ITEM);
#[doc(alias("CHAN_STATIC"))]
pub const STATIC: Self = Self(CHAN_STATIC);
#[doc(alias("CHAN_STREAM"))]
pub const STREAM: Self = Self(CHAN_STREAM);
#[doc(alias("CHAN_VOICE"))]
pub const VOICE: Self = Self(CHAN_VOICE);
#[doc(alias("CHAN_VOICE2"))]
pub const VOICE2: Self = Self(CHAN_VOICE2);
#[doc(alias("CHAN_WEAPON"))]
pub const WEAPON: Self = Self(CHAN_WEAPON);
pub const fn from_raw(raw: c_int) -> Option<Self> {
if raw >= 0 && raw <= MAX_CHANNEL {
Some(Self(raw))
} else {
None
}
}
pub const fn to_raw(self) -> c_int {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Pitch(NonZero<u8>);
impl Pitch {
#[doc(alias("PITCH_HIGH"))]
pub const HIGH: Self = Self::from_header(PITCH_HIGH);
#[doc(alias("PITCH_LOW"))]
pub const LOW: Self = Self::from_header(PITCH_LOW);
#[doc(alias("PITCH_NORM"))]
pub const NORM: Self = Self::from_header(PITCH_NORM);
pub const fn new(pitch: u8) -> Option<Self> {
match NonZero::new(pitch) {
Some(pitch) => Some(Self(pitch)),
None => None,
}
}
const fn from_header(pitch: c_int) -> Self {
let narrowed = pitch as u8;
assert!(narrowed as c_int == pitch);
match Self::new(narrowed) {
Some(pitch) => pitch,
None => panic!("a pitch must not be 0"),
}
}
pub const fn get(self) -> u8 {
self.0.get()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SoundEmission<'a> {
pub source: SoundSource<'a>,
pub channel: Channel,
pub sample: &'a CStr,
pub volume: Volume,
pub level: SoundLevel,
pub flags: SoundFlags,
pub pitch: Pitch,
pub origin: Option<Vector>,
pub sound_time: Option<f32>,
pub speaker: Option<Entity<'a>>,
}
impl<'a> SoundEmission<'a> {
pub const fn new(sample: &'a CStr, source: SoundSource<'a>) -> Self {
Self {
source,
channel: Channel::AUTO,
sample,
volume: Volume::NORM,
level: SoundLevel::NORM,
flags: SoundFlags::NONE,
pitch: Pitch::NORM,
origin: None,
sound_time: None,
speaker: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SoundError {
#[error("the sound's origin or start time is not finite")]
NotFinite,
#[error("the entity is not networked, so no client knows it")]
NotNetworked,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct SoundFlags: c_int {
#[doc(alias("SND_CHANGE_PITCH"))]
const CHANGE_PITCH = SND_CHANGE_PITCH;
#[doc(alias("SND_CHANGE_VOL"))]
const CHANGE_VOLUME = SND_CHANGE_VOL;
#[doc(alias("SND_DELAY"))]
const DELAY = SND_DELAY;
#[doc(alias("SND_DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL"))]
const DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL =
SND_DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL;
#[doc(alias("SND_IGNORE_NAME"))]
const IGNORE_NAME = SND_IGNORE_NAME;
#[doc(alias("SND_IGNORE_PHONEMES"))]
const IGNORE_PHONEMES = SND_IGNORE_PHONEMES;
#[doc(alias("SND_NOFLAGS"))]
const NONE = SND_NOFLAGS;
#[doc(alias("SND_SHOULDPAUSE"))]
const SHOULD_PAUSE = SND_SHOULDPAUSE;
#[doc(alias("SND_SPEAKER"))]
const SPEAKER = SND_SPEAKER;
#[doc(alias("SND_STOP"))]
const STOP = SND_STOP;
#[doc(alias("SND_STOP_LOOPING"))]
const STOP_LOOPING = SND_STOP_LOOPING;
}
}
#[doc(alias("soundlevel_t"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SoundLevel(u8);
impl SoundLevel {
#[doc(alias("SNDLVL_GUNFIRE"))]
pub const GUNFIRE: Self = Self::from_header(sys::soundlevel_t_SNDLVL_GUNFIRE);
#[doc(alias("SNDLVL_IDLE"))]
pub const IDLE: Self = Self::from_header(sys::soundlevel_t_SNDLVL_IDLE);
#[doc(alias("SNDLVL_NONE"))]
pub const NONE: Self = Self::from_header(sys::soundlevel_t_SNDLVL_NONE);
#[doc(alias("SNDLVL_NORM"))]
pub const NORM: Self = Self::from_header(sys::soundlevel_t_SNDLVL_NORM);
#[doc(alias("SNDLVL_STATIC"))]
pub const STATIC: Self = Self::from_header(sys::soundlevel_t_SNDLVL_STATIC);
#[doc(alias("SNDLVL_TALKING"))]
pub const TALKING: Self = Self::from_header(sys::soundlevel_t_SNDLVL_TALKING);
pub const fn new(decibels: u8) -> Self {
Self(decibels)
}
const fn from_header(level: sys::soundlevel_t) -> Self {
let decibels = level as u8;
assert!(decibels as sys::soundlevel_t == level);
Self(decibels)
}
pub const fn decibels(self) -> u8 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SoundSource<'a> {
Entity(Entity<'a>),
#[doc(alias("SOUND_FROM_WORLD"))]
World,
}
impl SoundSource<'_> {
fn to_raw(self) -> Result<c_int, SoundError> {
match self {
Self::Entity(entity) => entity
.edict()
.map(|edict| edict.index())
.ok_or(SoundError::NotNetworked),
Self::World => Ok(SOUND_FROM_WORLD),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volume(f32);
impl Volume {
#[doc(alias("VOL_NORM"))]
pub const NORM: Self = Self(VOL_NORM);
pub const fn new(volume: f32) -> Option<Self> {
if volume >= 0.0 && volume <= 1.0 {
Some(Self(volume))
} else {
None
}
}
pub const fn get(self) -> f32 {
self.0
}
}
impl<'s> EngineSound<'s> {
#[doc(alias("EmitSound"))]
pub fn emit_sound(
self,
recipients: &Recipients,
emission: &SoundEmission<'_>,
) -> Result<(), SoundError> {
let SoundEmission {
source,
channel,
sample,
volume,
level,
flags,
pitch,
origin,
sound_time,
speaker,
} = *emission;
let entity = source.to_raw()?;
let speaker = match speaker {
Some(speaker) => SoundSource::Entity(speaker).to_raw()?,
None => NO_SPEAKER_ENTITY,
};
let origin = finite_vector(origin)?;
let sound_time = match sound_time {
Some(time) if !time.is_finite() => return Err(SoundError::NotFinite),
Some(time) => time,
None => 0.0,
};
if recipients.is_empty() {
return Ok(());
}
let filter = recipients.filter();
unsafe {
vcall!(self.as_ptr() => IEngineSound_EmitSound1(
filter.as_raw(),
entity,
channel.to_raw(),
sample.as_ptr(),
volume.get(),
sys::soundlevel_t::from(level.decibels()),
flags.bits(),
c_int::from(pitch.get()),
DEFAULT_SPECIAL_DSP,
origin.as_ref().map_or(ptr::null(), ptr::from_ref),
ptr::null(),
null_mut(),
true,
sound_time,
speaker,
));
}
Ok(())
}
#[doc(alias("IsSoundPrecached"))]
pub fn is_sound_precached(self, sample: &CStr) -> bool {
unsafe { vcall!(self.as_ptr() => IEngineSound_IsSoundPrecached(sample.as_ptr())) }
}
#[doc(alias("PrecacheSound"))]
pub fn precache_sound(self, sample: &CStr, preload: bool) -> bool {
unsafe {
vcall!(self.as_ptr() => IEngineSound_PrecacheSound(sample.as_ptr(), preload, false))
}
}
#[doc(alias("GetSoundDuration"))]
pub fn sound_duration(self, sample: &CStr) -> Option<f32> {
let seconds =
unsafe { vcall!(self.as_ptr() => IEngineSound_GetSoundDuration(sample.as_ptr())) };
(seconds.is_finite() && seconds > 0.0).then_some(seconds)
}
#[doc(alias("StopSound"))]
pub fn stop_sound(
self,
source: SoundSource<'_>,
channel: Channel,
sample: &CStr,
) -> Result<(), SoundError> {
let entity = source.to_raw()?;
unsafe {
vcall!(self.as_ptr() => IEngineSound_StopSound(entity, channel.to_raw(), sample.as_ptr()))
};
Ok(())
}
}
fn finite_vector(vector: Option<Vector>) -> Result<Option<sys::Vector>, SoundError> {
match vector {
Some(vector) if !vector.is_finite() => Err(SoundError::NotFinite),
vector => Ok(vector.map(sys::Vector::from)),
}
}