nooise 1.1.1

Ambient music generator for the terminal
use std::error::Error;

use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, Stream, StreamConfig};

pub(crate) trait StereoEngine: Send + 'static {
    fn next_stereo(&mut self) -> (f32, f32);
}

pub(crate) fn start_stream<E>(
    app_id: &str,
    engine_factory: impl FnOnce(f32) -> E,
) -> Result<Stream, Box<dyn Error>>
where
    E: StereoEngine,
{
    let host = cpal::default_host();
    let device = host
        .default_output_device()
        .ok_or("no default output audio device found")?;
    let supported_config = device.default_output_config()?;
    let sample_format = supported_config.sample_format();
    let stream_config: StreamConfig = supported_config.into();
    let sample_rate = stream_config.sample_rate.0 as f32;

    println!(
        "running {app_id} at {} Hz on {}",
        sample_rate as u32,
        device.name()?
    );

    let stream = match sample_format {
        SampleFormat::F32 => {
            build_f32_stream(&device, &stream_config, engine_factory(sample_rate))?
        }
        SampleFormat::I16 => {
            build_i16_stream(&device, &stream_config, engine_factory(sample_rate))?
        }
        SampleFormat::U16 => {
            build_u16_stream(&device, &stream_config, engine_factory(sample_rate))?
        }
        other => return Err(format!("unsupported sample format: {other:?}").into()),
    };

    stream.play()?;
    Ok(stream)
}

fn build_f32_stream<E>(
    device: &cpal::Device,
    config: &StreamConfig,
    engine: E,
) -> Result<Stream, Box<dyn Error>>
where
    E: StereoEngine,
{
    let channels = config.channels as usize;
    let mut engine = engine;
    Ok(device.build_output_stream(
        config,
        move |data: &mut [f32], _| {
            for frame in data.chunks_mut(channels) {
                let (left, right) = engine.next_stereo();
                write_frame(frame, left, right);
            }
        },
        audio_error,
        None,
    )?)
}

fn build_i16_stream<E>(
    device: &cpal::Device,
    config: &StreamConfig,
    engine: E,
) -> Result<Stream, Box<dyn Error>>
where
    E: StereoEngine,
{
    let channels = config.channels as usize;
    let mut engine = engine;
    Ok(device.build_output_stream(
        config,
        move |data: &mut [i16], _| {
            for frame in data.chunks_mut(channels) {
                let (left, right) = engine.next_stereo();
                write_frame(frame, to_i16(left), to_i16(right));
            }
        },
        audio_error,
        None,
    )?)
}

fn build_u16_stream<E>(
    device: &cpal::Device,
    config: &StreamConfig,
    engine: E,
) -> Result<Stream, Box<dyn Error>>
where
    E: StereoEngine,
{
    let channels = config.channels as usize;
    let mut engine = engine;
    Ok(device.build_output_stream(
        config,
        move |data: &mut [u16], _| {
            for frame in data.chunks_mut(channels) {
                let (left, right) = engine.next_stereo();
                write_frame(frame, to_u16(left), to_u16(right));
            }
        },
        audio_error,
        None,
    )?)
}

fn write_frame<T: Copy>(frame: &mut [T], left: T, right: T) {
    if let Some(sample) = frame.first_mut() {
        *sample = left;
    }
    if frame.len() > 1 {
        frame[1] = right;
    }
    for sample in frame.iter_mut().skip(2) {
        *sample = left;
    }
}

fn to_i16(sample: f32) -> i16 {
    (sample.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
}

fn to_u16(sample: f32) -> u16 {
    ((sample.clamp(-1.0, 1.0) * 0.5 + 0.5) * u16::MAX as f32) as u16
}

fn audio_error(error: cpal::StreamError) {
    eprintln!("audio stream error: {error}");
}