// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright © 2024 Mike Robeson [dijksterhuis]

//! Library for various Audio file I/O, DSP etc. related functionality
//! specifically for the `ot-tools` family of crates.
//!
//! Supports writing Octatrack compatible AIFF and WAV files (44.1khz/48khz
//! sample rate at 16/24 bit depth).
//!
//! ## IMPORTANT
//! - This library/crate IS NOT a professional grade DSP/audio file library.
//! - This library/crate IS NOT a ffmpeg replacement.
//!
//! # Example
//! ```
//! use ot_tools_interfaces::audio::AudioFile;
//! use ot_tools_interfaces::OtToolsInterfacesError;
//!
//! # fn main() -> Result<(), OtToolsInterfacesError> {
//!
//! // read either Aiff or Wav from file extension
//! let aiff_16_bit = AudioFile::read_by_ext("./test-data/audio/test_16.aiff")?;
//! let aiff_24_bit = AudioFile::read_by_ext("./test-data/audio/test_24.aiff")?;
//! let wav_16_bit = AudioFile::read_by_ext("./test-data/audio/test_16.wav")?;
//! let wav_24_bit = AudioFile::read_by_ext("./test-data/audio/test_24.wav")?;
//!
//! // Stereo data is interleaved
//! let n_channels = wav_24_bit.spec.channels;
//! assert_eq!(
//!     wav_24_bit.len as usize,
//!     wav_24_bit.samples.len() / n_channels as usize,
//! );
//!
//! // sample length
//! assert_eq!(
//!     wav_24_bit.len as f32 / wav_24_bit.spec.sample_rate as f32,
//!      1.2675056
//! );
//!
//! // "light touch" dsp changes
//! let mut linear_fades = wav_24_bit.clone();
//! linear_fades.linear_fade_in(0.2)?;
//! linear_fades.linear_fade_out(0.2)?;
//!
//! linear_fades.write_by_ext("/tmp/some.wav")?;
//!
//! # Ok(()) }
//! ```

pub mod aiff;
pub mod dsp;
mod errors;
pub mod fs;
pub mod wav;

pub use errors::AudioErrors;

use std::fmt::Debug;
use std::path::Path;

use log::trace;
use ot_tools_io_derive::{AsMutDerive, AsRefDerive};

use crate::audio::aiff::AiffFile;
use crate::audio::wav::WavFile;
use crate::R;

/// Default sample rate for audio files
pub const DEFAULT_SAMPLE_RATE: u32 = 44_100;

/// Possible sample rate for Octatrack compatible audio files
pub const ALLOWED_SAMPLE_RATES: [u32; 2] = [DEFAULT_SAMPLE_RATE, 48_000];

/// Default number of channels for audio files
pub const DEFAULT_CHANNELS: u16 = 1;

/// Possible channel count for Octatrack compatible audio files
pub const ALLOWED_CHANNELS: [u16; 2] = [1, 2];

/// Default bit depth for audio files
pub const DEFAULT_BIT_DEPTH: u16 = 16;

/// Possible bit depths for Octatrack compatible audio files
pub const ALLOWED_BIT_DEPTHS: [u16; 2] = [16, 24];

/// Type of file -- WAV or AIFF.
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, AsMutDerive, AsRefDerive)]
pub enum FileType {
    Wav,
    Aiff,
}

/// Allowed sample data formats for this library (basically only Integer sample
/// format files are supported).
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, AsMutDerive, AsRefDerive)]
pub enum SampleFormat {
    Int,
    Unsupported,
}

impl From<hound::SampleFormat> for SampleFormat {
    fn from(value: hound::SampleFormat) -> Self {
        match value {
            hound::SampleFormat::Int => SampleFormat::Int,
            hound::SampleFormat::Float => SampleFormat::Unsupported,
        }
    }
}

// NOTE: Won't be writing as AIFF, only writing WAV int format.
impl From<aifc::SampleFormat> for SampleFormat {
    fn from(value: aifc::SampleFormat) -> Self {
        match value {
            aifc::SampleFormat::U8 => Self::Unsupported,
            aifc::SampleFormat::I8 => Self::Unsupported,
            aifc::SampleFormat::I16 => Self::Int,
            aifc::SampleFormat::I16LE => Self::Int,
            aifc::SampleFormat::I24 => Self::Int,
            aifc::SampleFormat::I32 => Self::Unsupported,
            aifc::SampleFormat::I32LE => Self::Unsupported,
            aifc::SampleFormat::F32 => Self::Unsupported,
            aifc::SampleFormat::F64 => Self::Unsupported,
            aifc::SampleFormat::CompressedUlaw => Self::Int,
            aifc::SampleFormat::CompressedAlaw => Self::Int,
            aifc::SampleFormat::CompressedIma4 => Self::Int,
            _ => Self::Unsupported,
        }
    }
}

/// Not currently in use
#[doc(hidden)]
#[derive(Debug, Eq, PartialEq, Clone, Default)]
pub enum Channels {
    Unsupported = 0,
    Mono = 1,
    #[default]
    Stereo = 2,
}

// todo: try_from uX proc macro
impl From<u8> for Channels {
    fn from(value: u8) -> Self {
        match value {
            1 => Self::Mono,
            2 => Self::Stereo,
            _ => Self::Unsupported,
        }
    }
}

/// Audio specification details for an [`AudioFile`]
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, AsMutDerive, AsRefDerive)]
pub struct AudioSpec {
    /// mono or stereo are allowed, but possible to have a file with more than
    /// two channels
    pub channels: u16,
    /// if you're not sure what this is, read up on some basic DSP (digital
    /// signal processing)
    pub sample_rate: u32,
    /// if you're not sure what this is, read up on some basic DSP (digital
    /// signal processing)
    pub bit_depth: u16,
    /// integer or float valued
    pub format: SampleFormat,
}

impl From<hound::WavSpec> for AudioSpec {
    fn from(value: hound::WavSpec) -> Self {
        Self {
            // TODO: use channels enum
            channels: value.channels,
            sample_rate: value.sample_rate,
            bit_depth: value.bits_per_sample,
            format: SampleFormat::from(value.sample_format),
        }
    }
}

impl AudioSpec {
    // TODO: Error / validate on bad sample rate or bit depth
    /// Specifies underlying file/audio vector properties.
    /// Determines the kind of file the audio data is eventually write to and
    /// how that data is stored in the internal `Vec<f32>` type for an
    /// [`AudioFile`] -- e.g. stereo data needs to be interleaved etc.
    /// ```
    /// use ot_tools_interfaces::audio::AudioSpec;
    /// use ot_tools_interfaces::audio::SampleFormat;
    ///
    /// let x = AudioSpec::new(Some(2), Some(48000), Some(24), None);
    /// assert_eq!(x.channels, 2);
    /// assert_eq!(x.sample_rate, 48000);
    /// assert_eq!(x.bit_depth, 24);
    /// assert_eq!(x.format, SampleFormat::Int);
    ///
    /// let x = AudioSpec::new(None, None, None, None);
    /// assert_eq!(x.channels, 1);
    /// assert_eq!(x.sample_rate, 44100);
    /// assert_eq!(x.bit_depth, 16);
    /// assert_eq!(x.format, SampleFormat::Int);
    /// ```
    pub fn new(
        channels: Option<u16>,
        sample_rate: Option<u32>,
        bit_depth: Option<u16>,
        format: Option<SampleFormat>,
    ) -> Self {
        Self {
            channels: channels.unwrap_or(DEFAULT_CHANNELS),
            sample_rate: sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE),
            bit_depth: bit_depth.unwrap_or(DEFAULT_BIT_DEPTH),
            format: format.unwrap_or(SampleFormat::Int),
        }
    }

    pub fn validate(&self) -> bool {
        ALLOWED_SAMPLE_RATES.contains(&self.sample_rate)
            && ALLOWED_CHANNELS.contains(&self.channels)
            && ALLOWED_BIT_DEPTHS.contains(&self.bit_depth)
            && self.format != SampleFormat::Unsupported
    }
}

// TODO: HACKY, Not fully tested against aifc formats
fn match_aifc_bit_depth(v: &aifc::SampleFormat) -> u16 {
    match v {
        aifc::SampleFormat::U8 | aifc::SampleFormat::I8 => 8,
        aifc::SampleFormat::I16
        | aifc::SampleFormat::I16LE
        | aifc::SampleFormat::CompressedUlaw
        | aifc::SampleFormat::CompressedIma4 => 16,
        aifc::SampleFormat::I24 => 24,
        aifc::SampleFormat::I32 | aifc::SampleFormat::I32LE | aifc::SampleFormat::F32 => 32,
        aifc::SampleFormat::F64 => 64,
        _ => 0,
    }
}

impl From<aifc::AifcReadInfo> for AudioSpec {
    fn from(value: aifc::AifcReadInfo) -> Self {
        Self {
            channels: value.channels as u16,
            sample_rate: value.sample_rate as u32,
            bit_depth: match_aifc_bit_depth(&value.sample_format),
            format: SampleFormat::from(value.sample_format),
        }
    }
}

/// Audio file specifications allowed on the Octatrack
///
/// section 7.2 in manual: AUDIO POOL
/// > Both Flex and Static machines can handle 16 or 24 bit/44.1 kHz wav/aiff
/// > files, either in mono or stereo.
pub fn get_all_compatible_audio_specs() -> Vec<AudioSpec> {
    let mut buf = Vec::<AudioSpec>::new();

    for sample_rate in ALLOWED_SAMPLE_RATES {
        for bit_depth in ALLOWED_BIT_DEPTHS {
            for channels in ALLOWED_CHANNELS {
                buf.push(AudioSpec {
                    channels,
                    sample_rate,
                    bit_depth,
                    format: SampleFormat::Int,
                })
            }
        }
    }

    buf
}

/// Representation of a some wav or aiff audio file
#[derive(Clone, Debug, PartialEq, PartialOrd, AsMutDerive, AsRefDerive)]
pub struct AudioFile {
    /// specification struct for the wav or aiff audio file.
    /// loosely based on the [`hound`] crate's specification struct.
    pub spec: AudioSpec,

    /// Number of audio samples in the Wav file.
    pub len: u32,

    /// Audio samples
    pub samples: Vec<f32>,
}

impl From<WavFile> for AudioFile {
    fn from(value: WavFile) -> Self {
        Self {
            spec: AudioSpec::from(value.spec),
            len: value.len,
            samples: value.samples,
        }
    }
}

impl From<AiffFile> for AudioFile {
    fn from(value: AiffFile) -> Self {
        Self {
            spec: AudioSpec::from(value.spec),
            len: value.len,
            samples: value.samples,
        }
    }
}

fn get_ext_lowercase(path: &Path) -> String {
    path.extension()
        .unwrap_or_default()
        .to_str()
        .unwrap_or_default()
        .to_lowercase()
        .to_string()
}

// NOTE: PLEASE DO NOT IMPLEMENT `From<AudioFile> for AiffFile
//       The AIFF spec hurts my brain and is unnecessary (in my opinion).

impl AudioFile {
    /// Read `AudioFile` from a file at `path` based on the extension in the
    /// provided path. `path`'s extension must be one of `wav` or
    /// `aiff`. else returns an error.
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// assert!(AudioFile::read_by_ext("test-data/audio/test_16.aiff").is_ok());
    /// assert!(AudioFile::read_by_ext("test-data/audio/test_16.wav").is_ok());
    /// # Ok(()) }
    /// ```
    pub fn read_by_ext<P: AsRef<Path>>(path: P) -> R<Self> {
        match get_ext_lowercase(path.as_ref()).as_str() {
            "wav" => AudioFile::read_wav(path.as_ref()),
            "aiff" => AudioFile::read_aiff(path.as_ref()),
            _ => Err(AudioErrors::WriteByExt.into()),
        }
    }

    /// Write `AudioFile` to a new file at `newpath` based on the provided
    /// extension in the path. `newpath`'s extension must be one of `wav` or
    /// `aiff`, else returns an error.
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// # std::fs::create_dir_all("/tmp/ot-tools-interfaces/docs/audio/write_by_ext")?;
    /// let x = AudioFile::read_by_ext("test-data/audio/test_16.aiff")?;
    /// x.write_by_ext("/tmp/ot-tools-interfaces/docs/audio/write_by_ext/aiff_to.wav")?;
    ///
    /// let x = AudioFile::read_by_ext("test-data/audio/test_16.wav")?;
    /// x.write_by_ext("/tmp/ot-tools-interfaces/docs/audio/write_by_ext/wav_to.aiff")?;
    /// # Ok(()) }
    /// ```
    pub fn write_by_ext<P: AsRef<Path>>(&self, newpath: P) -> R<()> {
        match get_ext_lowercase(newpath.as_ref()).as_str() {
            "wav" => WavFile::from(self.clone()).write(newpath.as_ref()),
            "aiff" => AiffFile::from(self.clone()).write(newpath.as_ref()),
            _ => Err(AudioErrors::WriteByExt.into()),
        }
    }

    // TODO: Could be a generic trait like From/Into, but that's extra complexity
    //       when we only have two file formats we allow to be read...
    //       ...
    //       ot-tools is not meant to be an ffmpeg replacement
    /// Read audio data from a WAV file.
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// assert!(AudioFile::read_wav("test-data/audio/test_16.aiff").is_err());
    /// assert!(AudioFile::read_wav("test-data/audio/test_16.wav").is_ok());
    /// # Ok(()) }
    /// ```
    pub fn read_wav<P: AsRef<Path>>(path: P) -> R<Self> {
        Ok(Self::from(WavFile::read(path.as_ref())?))
    }

    /// Write audio data into a WAV file.
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// # std::fs::create_dir_all("/tmp/ot-tools-interfaces/docs/audio/write_wav")?;
    /// let aiff = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// aiff.write_wav("/tmp/ot-tools-interfaces/docs/audio/write_wav/aiff_to.wav")?;
    ///
    /// let wav = AudioFile::read_wav("test-data/audio/test_16.wav")?;
    /// wav.write_wav("/tmp/ot-tools-interfaces/docs/audio/write_wav/wav_to.wav")?;
    /// # Ok(()) }
    /// ```
    pub fn write_wav<P: AsRef<Path>>(&self, newpath: P) -> R<()> {
        WavFile::from(self.clone()).write(newpath.as_ref())
    }

    // TODO: Could be a generic trait like From/Into, but that's extra complexity
    //       when we only have two file formats we allow to be read...
    //       ...
    //       ot-tools is not meant to be an ffmpeg replacement
    /// Read audio data from an AIFF file.
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// assert!(AudioFile::read_aiff("test-data/audio/test_16.aiff").is_ok());
    /// assert!(AudioFile::read_aiff("test-data/audio/test_16.wav").is_err());
    /// # Ok(()) }
    /// ```
    pub fn read_aiff<P: AsRef<Path>>(path: P) -> R<Self> {
        Ok(Self::from(AiffFile::read(path.as_ref())?))
    }

    /// Write audio data as an AIFF file.
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// # std::fs::create_dir_all("/tmp/ot-tools-interfaces/docs/audio/write_aiff")?;
    /// let aiff = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// aiff.write_aiff("/tmp/ot-tools-interfaces/docs/audio/write_aiff/aiff_to.aiff")?;
    ///
    /// let wav = AudioFile::read_wav("test-data/audio/test_16.wav")?;
    /// wav.write_aiff("/tmp/ot-tools-interfaces/docs/audio/write_aiff/wav_to.aiff")?;
    /// # Ok(()) }
    /// ```
    pub fn write_aiff<P: AsRef<Path>>(&self, newpath: P) -> R<()> {
        AiffFile::from(self.clone()).write(newpath.as_ref())
    }

    /// Normalise audio samples between 0 and 1
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut x = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// x.normalize()?;
    /// # Ok(()) }
    /// ```
    pub fn normalize(&mut self) -> R<()> {
        dsp::normalize(self)
    }

    // really hacky and simple time stretching
    /// Time stretch audio vector by resampling, inserts duplicate samples to
    /// mimic slowing the audio down and removes samples to mimic speeding it
    /// up.
    /// # WARNING
    /// Speeding up via resampling is a lossy procedure.
    /// # Example: Slow down (half-speed)
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut x = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// let orig_len = x.len;
    /// x.resample_time_stretch(-1)?;
    /// assert_eq!(x.len, orig_len * 2);
    /// # Ok(()) }
    /// ```
    /// # Example: Speed up (double speed)
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut x = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// let orig_len = x.len;
    /// x.resample_time_stretch(1)?;
    /// # // there's an "extra" sample as this is a stereo file
    /// # // 111,794 / 2 = 55,897 => would be uneven interleaved data
    /// assert_eq!(x.len, (orig_len / 2) + 1); // stereo sample
    /// # Ok(()) }
    /// ```
    pub fn resample_time_stretch(&mut self, stretch: i8) -> R<()> {
        dsp::timestrech_resampling(self, stretch)
    }

    /// Linear fade in
    ///
    /// ```text
    /// x_{n} = x_n.k_n
    /// forall n < K, n >= 0
    /// where k_n = k_{n-1} + 1/K; k_0 = 0
    /// ```
    /// 20% linear fade-in
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut x = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// x.linear_fade_in(0.2)?;
    /// # Ok(()) }
    /// ```
    /// 100% linear fade-in
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut x = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// x.linear_fade_in(1.0)?;
    /// # Ok(()) }
    /// ```
    pub fn linear_fade_in(&mut self, percent: f32) -> R<()> {
        dsp::fade_in_linear(self, percent)
    }

    /// Linear fade out
    ///
    /// ```text
    /// x_{n} = x_n.k_n
    /// forall n > N - K, n >= 0
    /// where k_n = k_{n-1} - 1/K; k_0 = 1
    /// ```
    /// # 20% linear fade out
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut x = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// x.linear_fade_out(0.2)?;
    /// # Ok(()) }
    /// ```
    /// 100% linear fade-out
    /// ```
    /// # use ot_tools_interfaces::audio::AudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut x = AudioFile::read_aiff("test-data/audio/test_16.aiff")?;
    /// x.linear_fade_out(1.0)?;
    /// # Ok(()) }
    /// ```
    pub fn linear_fade_out(&mut self, percent: f32) -> R<()> {
        dsp::fade_out_linear(self, percent)
    }

    /// Naive upmix of mono signal to an interleaved stereo signal
    /// (duplicate channels, attenuate by 0.5)
    // TODO: test
    pub fn mono_to_stereo_interleaved(&mut self) -> R<()> {
        dsp::mono_to_stereo_interleaved(self)
    }

    // todo: stereo to mono?
}

/// Is the file at `path` a WAV file that is compatible with the Octatrack?
pub fn file_path_is_compat_wav<P: AsRef<Path> + Debug>(path: P) -> R<bool> {
    trace!("Opening WAV file: path={path:#?}");
    let mut reader = WavFile::open_read_buf(path.as_ref())
        .map_err(wav::WavErrors::from)
        .map_err(AudioErrors::from)?;

    trace!("Opening WAV spec: path={path:#?}");
    let spec = wav::WavFile::get_spec(&mut reader)?;

    trace!("Creating AudioSpec: path={path:#?}");
    let audio_spec = AudioSpec {
        channels: spec.channels,
        sample_rate: spec.sample_rate,
        bit_depth: spec.bits_per_sample,
        format: SampleFormat::from(spec.sample_format),
    };

    trace!("Checking compatibility: spec={audio_spec:#?}");
    Ok(get_all_compatible_audio_specs().contains(&audio_spec))
}

/// Is the file at `path` an AIFF file that is compatible with the Octatrack?
pub fn file_path_is_compat_aiff<P: AsRef<Path> + Debug>(path: P) -> R<bool> {
    trace!("Opening AIFF file: path={path:#?}");
    let mut reader = aiff::AiffFile::open_read_buf(path.as_ref())?;

    trace!("Opening AIFF spec: path={path:#?}");
    let spec = aiff::AiffFile::get_spec(&mut reader);

    trace!("Creating AudioSpec: path={path:#?}");
    let audio_spec = AudioSpec {
        channels: spec.channels as u16,
        sample_rate: spec.sample_rate as u32,
        bit_depth: spec.comm_sample_size as u16,
        format: SampleFormat::from(spec.sample_format),
    };

    trace!("Checking compatibility: spec={audio_spec:#?}");
    Ok(get_all_compatible_audio_specs().contains(&audio_spec))
}

pub(crate) fn direntry_is_compat_wav(entry: &walkdir::DirEntry) -> bool {
    trace!(
        "Testing whether DirEntry is a compatible WAV file: path={:#?}",
        entry.path()
    );
    file_path_is_compat_wav(entry.path().to_path_buf()).unwrap_or(false)
}

pub(crate) fn direntry_is_compat_aiff(entry: &walkdir::DirEntry) -> bool {
    trace!(
        "Testing whether DirEntry is a compatible AIFF file: path={:#?}",
        entry.path()
    );
    file_path_is_compat_aiff(entry.path().to_path_buf()).unwrap_or(false)
}

// the branches are not the same, i don't know why clippy is saying they are.
#[allow(clippy::if_same_then_else)]
fn f32_to_i32(v: f32) -> i32 {
    // need to use explicit values here
    // the boundary value i32::MAX/i32::MIN clip for some reason
    // when converting to f32
    if v > 0.0 {
        (v * (2_u32.pow(32 - 1) - 1) as f32) as i32
    } else {
        (v * 2_u32.pow(32 - 1) as f32) as i32
    }
}

fn f32_to_i24(v: f32) -> i32 {
    f32_to_i32(v) >> 8
}

fn f32_to_i16(v: f32) -> i16 {
    (f32_to_i32(v) >> 16) as i16
}

#[allow(dead_code)]
fn i32_to_f32(v: i32) -> f32 {
    if v > 0 {
        v as f32 / (2_u32.pow(32 - 1) - 1) as f32
    } else {
        v as f32 / 2_u32.pow(32 - 1) as f32
    }
}

fn i24_to_f32(v: i32) -> f32 {
    if v > 0 {
        v as f32 / (2_u32.pow(24 - 1) - 1) as f32
    } else {
        v as f32 / 2_u32.pow(24 - 1) as f32
    }
}

fn i16_to_f32(v: i16) -> f32 {
    if v > 0 {
        v as f32 / (2_u32.pow(16 - 1) - 1) as f32
    } else {
        v as f32 / 2_u32.pow(16 - 1) as f32
    }
}

#[cfg(test)]
mod test {
    // these are here purely because i keep forgetting the upper/lower bounds
    const MAX_I32: i32 = 2147483647;
    const MIN_I32: i32 = -2147483648;
    const MAX_I24: i32 = 8388607;
    const MIN_I24: i32 = -8388608;
    const MAX_I16: i16 = 32767;
    const MIN_I16: i16 = -32768;

    mod f32_to_i32 {
        use super::*;
        #[test]
        fn max_val() {
            assert_eq!(MAX_I32, super::super::f32_to_i32(1.0_f32));
        }
        #[test]
        fn min_val() {
            assert_eq!(MIN_I32, super::super::f32_to_i32(-1.0_f32));
        }
        #[test]
        fn zero_val() {
            assert_eq!(0_i32, super::super::f32_to_i32(0.0_f32));
        }
        #[test]
        fn half_pos() {
            assert_eq!(1073741824, super::super::f32_to_i32(0.5_f32));
        }
        #[test]
        fn half_neg() {
            assert_eq!(-1073741824, super::super::f32_to_i32(-0.5_f32));
        }
    }

    mod f32_to_i24 {
        use super::*;
        #[test]
        fn max_val() {
            assert_eq!(MAX_I24, super::super::f32_to_i24(1.0_f32));
        }
        #[test]
        fn min_val() {
            assert_eq!(MIN_I24, super::super::f32_to_i24(-1.0_f32));
        }
        #[test]
        fn zero_val() {
            assert_eq!(0_i32, super::super::f32_to_i24(0.0_f32));
        }
        #[test]
        fn half_pos() {
            assert_eq!(4194304, super::super::f32_to_i24(0.5_f32));
        }
        #[test]
        fn half_neg() {
            assert_eq!(-4194304, super::super::f32_to_i24(-0.5_f32));
        }
    }
    mod f32_to_i16 {
        use super::*;
        #[test]
        fn max_val() {
            assert_eq!(MAX_I16, super::super::f32_to_i16(1.0_f32));
        }
        #[test]
        fn min_val() {
            assert_eq!(MIN_I16, super::super::f32_to_i16(-1.0_f32));
        }
        #[test]
        fn zero_val() {
            assert_eq!(0_i16, super::super::f32_to_i16(0.0_f32));
        }
        #[test]
        fn half_pos() {
            assert_eq!(16384, super::super::f32_to_i16(0.5_f32));
        }
        #[test]
        fn half_neg() {
            assert_eq!(-16384, super::super::f32_to_i16(-0.5_f32));
        }
    }

    mod i32_to_f32 {
        #[test]
        fn max_val() {
            assert_eq!(1.0_f32, super::super::i32_to_f32(i32::MAX));
        }
        #[test]
        fn min_val() {
            assert_eq!(-1.0_f32, super::super::i32_to_f32(i32::MIN));
        }
        #[test]
        fn zero_val() {
            assert_eq!(0.0_f32, super::super::i32_to_f32(0_i32));
        }
        #[test]
        fn half_pos() {
            assert_eq!(0.5_f32, super::super::i32_to_f32(i32::MAX / 2));
        }
        #[test]
        fn half_neg() {
            assert_eq!(-0.5_f32, super::super::i32_to_f32(i32::MIN / 2));
        }
    }

    mod i24_to_f32 {
        use super::*;
        #[test]
        fn max_val() {
            assert_eq!(1.0_f32, super::super::i24_to_f32(MAX_I24));
        }
        #[test]
        fn min_val() {
            assert_eq!(-1.0_f32, super::super::i24_to_f32(MIN_I24));
        }
        #[test]
        fn zero_val() {
            assert_eq!(0.0_f32, super::super::i24_to_f32(0_i32));
        }
        #[test]
        fn half_pos() {
            assert_ne!(
                0.5_f32,
                super::super::i24_to_f32(MAX_I24 / 2),
                "half positive 24 bit should be slightly less than 0.5"
            );
        }
        #[test]
        fn half_neg() {
            assert_eq!(-0.5_f32, super::super::i24_to_f32(MIN_I24 / 2));
        }
    }

    mod i16_to_f32 {
        #[test]
        fn max_val() {
            assert_eq!(1.0_f32, super::super::i16_to_f32(i16::MAX));
        }
        #[test]
        fn min_val() {
            assert_eq!(-1.0_f32, super::super::i16_to_f32(i16::MIN));
        }
        #[test]
        fn zero_val() {
            assert_eq!(0.0_f32, super::super::i16_to_f32(0_i16));
        }
        #[test]
        fn half_pos() {
            assert_ne!(
                0.5_f32,
                super::super::i16_to_f32(i16::MAX / 2),
                "half positive 16 bit should be slightly less than 0.5"
            );
        }
        #[test]
        fn half_neg() {
            assert_eq!(-0.5_f32, super::super::i16_to_f32(i16::MIN / 2),);
        }
    }
}
