launchy 0.2.0

Low-level bindings and high-level abstractions for the Novation Launchpad MIDI devices
Documentation
use super::*;

/// Launchpad's implement this trait to signify how they can be used as a [`Canvas`]. Based on this
/// specification, [`DeviceCanvas`] provides a generic [`Canvas`] implemention that can be used for
/// all devices.
///
/// You as a user of this library will not need to use this trait directly.
pub trait DeviceSpec {
    /// The width of the smallest rectangle that still fully encapsulates the shape of this device
    const BOUNDING_BOX_WIDTH: u32;
    /// The height of the smallest rectangle that still fully encapsulates the shape of this device
    const BOUNDING_BOX_HEIGHT: u32;
    /// How many different colors can be shown per channel. As an example; the MK2 uses 6 bit color,
    /// so it supports color values from 0 up to 63 - in total 64 values.
    const COLOR_PRECISION: u16;

    /// The input handler type
    type Input: crate::InputDevice;
    /// The output handler type
    type Output: crate::OutputDevice;

    /// Returns whether the point at the given `x` and `y` coordinates are in bounds
    fn is_valid(x: u32, y: u32) -> bool;

    /// Flush the changes, as specified by `changes`, to the given underlying output handler.
    ///
    /// `changes` is a slice of tuples `(u32, u32, (u8, u8, u8))`, where the first element is the x
    /// coordinate, the second element is the y coordinate, and the third element is an RGB color
    /// tuple, according to `COLOR_PRECISION`.
    fn flush(
        canvas: &mut crate::DeviceCanvas<Self>,
        changes: &[(u32, u32, (u8, u8, u8))],
    ) -> Result<(), crate::MidiError>
    where
        Self: Sized;

    /// Convert a message from the underlying input handler into an abstract CanvasMessage. If the
    /// low-level message has no CanvasMessage equivalent, i.e. if it's irrelevant in a canvas
    /// context, None is returned.
    fn convert_message(msg: <Self::Input as crate::InputDevice>::Message) -> Option<CanvasMessage>;

    /// Optional code to setup this device for canvas usage
    fn setup(output: &mut Self::Output) -> Result<(), crate::MidiError> {
        let _ = output;
        Ok(())
    }
}

/// Utility to be able to process messages from a CanvasLayout by polling
pub struct DeviceCanvasPoller {
    receiver: std::sync::mpsc::Receiver<CanvasMessage>,
}

impl crate::MsgPollingWrapper for DeviceCanvasPoller {
    type Message = CanvasMessage;

    fn receiver(&self) -> &std::sync::mpsc::Receiver<Self::Message> {
        &self.receiver
    }
}

/// A generic [`Canvas`] implementation for all launchpads, that relies on a [`DeviceSpec`]. You as
/// a user of the library don't need to access this struct directly. Use the "Canvas" type aliases
/// that each launchpad module provides, for example `launchy::mk2::Canvas` or
/// `launchy::s::Canvas`.
pub struct DeviceCanvas<Spec: DeviceSpec> {
    _input: crate::InputDeviceHandler,
    pub(crate) output: Spec::Output,
    curr_state: crate::util::Array2d<crate::Color>,
    new_state: crate::util::Array2d<crate::Color>,
    // This is a debug variable to be able to see how many messages I'm actually spewing out.
    num_sent_changes: usize,
}

impl<Spec: DeviceSpec> DeviceCanvas<Spec> {
    /// Create a new canvas by guessing both input and output MIDI connection by their name. If you
    /// need precise control over the specific MIDI connections that will be used, use
    /// [`DeviceCanvas::from_ports`] instead // TODO: not implemented yet
    pub fn guess(
        mut callback: impl FnMut(CanvasMessage) + Send + 'static,
    ) -> Result<Self, crate::MidiError> {
        use crate::midi_io::{InputDevice, OutputDevice};

        let _input = Spec::Input::guess(move |msg| {
            if let Some(msg) = Spec::convert_message(msg) {
                (callback)(msg);
            }
        })?;
        let mut output = Spec::Output::guess()?;
        Spec::setup(&mut output)?;

        let curr_state =
            crate::util::Array2d::new(Spec::BOUNDING_BOX_WIDTH, Spec::BOUNDING_BOX_HEIGHT);
        let new_state =
            crate::util::Array2d::new(Spec::BOUNDING_BOX_WIDTH, Spec::BOUNDING_BOX_HEIGHT);

        Ok(Self {
            _input,
            output,
            curr_state,
            new_state,
            num_sent_changes: 0,
        })
    }

    pub fn guess_polling() -> Result<(Self, DeviceCanvasPoller), crate::MidiError> {
        let (sender, receiver) = std::sync::mpsc::channel();
        let canvas = Self::guess(move |msg| {
            sender
                .send(msg)
                .expect("Message receiver has hung up (this shouldn't happen)")
        })?;

        let poller = DeviceCanvasPoller { receiver };

        Ok((canvas, poller))
    }
}

#[doc(hidden)] // this is crap workaround and shouldn't be seen by user directly
pub trait DeviceCanvasTrait {
    type Spec: DeviceSpec;
}

impl<S: DeviceSpec> DeviceCanvasTrait for DeviceCanvas<S> {
    type Spec = S;
}

impl_traits_for_canvas!(DeviceCanvas[S: DeviceSpec]);

impl<Spec: DeviceSpec> crate::Canvas for DeviceCanvas<Spec> {
    fn bounding_box(&self) -> (u32, u32) {
        (Spec::BOUNDING_BOX_WIDTH, Spec::BOUNDING_BOX_HEIGHT)
    }
    fn lowest_visible_brightness(&self) -> f32 {
        1.0 / Spec::COLOR_PRECISION as f32
    }

    fn low_level_get(&self, x: u32, y: u32) -> Option<&Color> {
        if !Spec::is_valid(x, y) {
            return None;
        }

        self.curr_state.get(x, y)
    }

    fn low_level_get_pending_mut(&mut self, x: u32, y: u32) -> Option<&mut Color> {
        if !Spec::is_valid(x, y) {
            return None;
        }

        self.new_state.get_mut(x, y)
    }

    fn low_level_get_pending(&self, x: u32, y: u32) -> Option<&Color> {
        if !Spec::is_valid(x, y) {
            return None;
        }

        self.new_state.get(x, y)
    }

    fn flush(&mut self) -> Result<(), crate::MidiError> {
        let mut changes: Vec<(u32, u32, (u8, u8, u8))> = Vec::with_capacity(9 * 9);

        for pad in self.iter() {
            let old = self.get(pad).unwrap();
            let new = self.get_pending(pad).unwrap();

            let old = old.quantize(Spec::COLOR_PRECISION as u8);
            let new = new.quantize(Spec::COLOR_PRECISION as u8);

            if new != old {
                changes.push((pad.x as u32, pad.y as u32, new));
            }
        }

        if !changes.is_empty() {
            use crate::midi_io::OutputDevice;
            self.num_sent_changes += changes.len();
            if self.num_sent_changes / 1000 != (self.num_sent_changes - changes.len()) / 1000 {
                println!(
                    "{}: we're at {} total transmitted changes now",
                    Spec::Output::MIDI_DEVICE_KEYWORD,
                    self.num_sent_changes,
                );
            }

            Spec::flush(self, &changes)?;
        }

        self.curr_state = self.new_state.clone();

        Ok(())
    }
}