humster 0.0.2

Modern music toolkit for Rust
Documentation
use crate::errors::{Error, Result};
use crate::processor::Processor;
use std::fs;
use std::io::Write;
use std::path::Path;

#[cfg(not(any(feature = "midi", feature = "audio")))]
compile_error!("Enable at least one of the `midi` or `audio` features to use `Track`.");

#[cfg(feature = "audio")]
use crate::audio::AudioTrack;
#[cfg(feature = "midi")]
use crate::midi::{MidiEvent, MidiTrack};

/// Wrapper type allowing processors to operate on MIDI or audio data.
#[derive(Debug, Clone, PartialEq)]
pub enum Track {
    #[cfg(feature = "midi")]
    Midi(MidiTrack),
    #[cfg(feature = "audio")]
    Audio(AudioTrack),
}

impl Track {
    /// Load a MIDI track from a lightweight text representation where each line contains `note velocity`.
    #[cfg(feature = "midi")]
    pub fn from_midi<P: AsRef<Path>>(path: P) -> Result<Self> {
        let contents = fs::read_to_string(path)?;
        let mut events = Vec::new();

        for (idx, raw_line) in contents.lines().enumerate() {
            let line = raw_line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }

            let parts: Vec<_> = line.split_whitespace().collect();
            if parts.len() != 2 {
                return Err(Error::Parse(format!(
                    "line {}: expected 'note velocity', got '{line}'",
                    idx + 1
                )));
            }

            let note = parts[0].parse::<u8>().map_err(|_| {
                Error::Parse(format!(
                    "line {}: invalid MIDI note '{}'",
                    idx + 1,
                    parts[0]
                ))
            })?;
            let velocity = parts[1].parse::<u8>().map_err(|_| {
                Error::Parse(format!(
                    "line {}: invalid velocity '{}'",
                    idx + 1,
                    parts[1]
                ))
            })?;

            if note > 127 {
                return Err(Error::Parse(format!(
                    "line {}: MIDI note must be <= 127",
                    idx + 1
                )));
            }
            if velocity > 127 {
                return Err(Error::Parse(format!(
                    "line {}: velocity must be <= 127",
                    idx + 1
                )));
            }

            events.push(MidiEvent::new(note, velocity));
        }

        Ok(Track::Midi(MidiTrack::new(events)))
    }

    /// Construct an audio track from samples and a sample rate.
    #[cfg(feature = "audio")]
    pub fn from_audio(samples: Vec<f32>, sample_rate: u32) -> Self {
        Track::Audio(AudioTrack::new(samples, sample_rate))
    }

    /// Apply a processor to the underlying track representation.
    pub fn apply<P: Processor>(&mut self, processor: &P) {
        match self {
            #[cfg(feature = "midi")]
            Track::Midi(track) => processor.process_midi(track),
            #[cfg(feature = "audio")]
            Track::Audio(track) => processor.process_audio(track),
        }
    }

    /// Export a track to the lightweight text formats understood by `from_midi` and `from_audio`.
    pub fn export<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        match self {
            #[cfg(feature = "midi")]
            Track::Midi(track) => export_midi(track, path),
            #[cfg(feature = "audio")]
            Track::Audio(track) => export_audio(track, path),
        }
    }

    /// Returns the contained MIDI track if present.
    #[cfg(feature = "midi")]
    pub fn as_midi(&self) -> Option<&MidiTrack> {
        if let Track::Midi(track) = self {
            Some(track)
        } else {
            None
        }
    }

    /// Returns the contained MIDI track mutably if present.
    #[cfg(feature = "midi")]
    pub fn as_midi_mut(&mut self) -> Option<&mut MidiTrack> {
        if let Track::Midi(track) = self {
            Some(track)
        } else {
            None
        }
    }

    /// Returns the contained audio track if present.
    #[cfg(feature = "audio")]
    pub fn as_audio(&self) -> Option<&AudioTrack> {
        if let Track::Audio(track) = self {
            Some(track)
        } else {
            None
        }
    }

    /// Returns the contained audio track mutably if present.
    #[cfg(feature = "audio")]
    pub fn as_audio_mut(&mut self) -> Option<&mut AudioTrack> {
        if let Track::Audio(track) = self {
            Some(track)
        } else {
            None
        }
    }
}

#[cfg(feature = "midi")]
fn export_midi<P: AsRef<Path>>(track: &MidiTrack, path: P) -> Result<()> {
    let mut file = fs::File::create(path)?;
    for event in &track.events {
        writeln!(file, "{} {}", event.note, event.velocity)?;
    }
    Ok(())
}

#[cfg(feature = "audio")]
fn export_audio<P: AsRef<Path>>(track: &AudioTrack, path: P) -> Result<()> {
    let mut file = fs::File::create(path)?;
    writeln!(file, "sample_rate {}", track.sample_rate)?;
    for sample in &track.samples {
        writeln!(file, "{sample}")?;
    }
    Ok(())
}