Skip to content

Commit

Permalink
Add output mapping for Pioneer DDJ-400
Browse files Browse the repository at this point in the history
  • Loading branch information
flosse committed Aug 21, 2023
1 parent 1d97577 commit b07dc75
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 4 deletions.
29 changes: 29 additions & 0 deletions examples/midi_dj_controller_hotplug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,33 @@ impl MidiOutputGateway<Box<DynMidiOutputConnection>> for KorgKaossDj {
}
}

#[derive(Default)]
struct PioneerDdj400 {
output_gateway: Option<pioneer_ddj_400::OutputGateway<Box<DynMidiOutputConnection>>>,
}

impl Controller for PioneerDdj400 {}

impl MidiOutputGateway<Box<DynMidiOutputConnection>> for PioneerDdj400 {
fn attach_midi_output_connection(
&mut self,
connection: &mut Option<Box<DynMidiOutputConnection>>,
) -> OutputResult<()> {
debug_assert!(self.output_gateway.is_none());
let mut output_gateway =
pioneer_ddj_400::OutputGateway::<Box<DynMidiOutputConnection>>::default();
output_gateway.attach_midi_output_connection(connection)?;
self.output_gateway = Some(output_gateway);
Ok(())
}

fn detach_midi_output_connection(&mut self) -> Option<Box<DynMidiOutputConnection>> {
self.output_gateway
.take()
.and_then(|mut output_gateway| output_gateway.detach_midi_output_connection())
}
}

fn new_midi_controller<I>(
device: &MidirDevice<I>,
output_connection: &mut Option<Box<DynMidiOutputConnection>>,
Expand All @@ -150,6 +177,8 @@ where
let mut controller: Box<dyn MidiController> =
if device.descriptor() == korg_kaoss_dj::MIDI_DEVICE_DESCRIPTOR {
Box::<KorgKaossDj>::default() as _
} else if device.descriptor() == pioneer_ddj_400::MIDI_DEVICE_DESCRIPTOR {
Box::<PioneerDdj400>::default() as _
} else {
return Ok(None);
};
Expand Down
40 changes: 36 additions & 4 deletions src/devices/pioneer_ddj_400/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@

use std::borrow::Cow;

use strum::{EnumCount, EnumIter, FromRepr};

use crate::{DeviceDescriptor, MidiDeviceDescriptor};

pub mod input;
pub use self::input::{DeckSensor, EffectSensor, MainSensor, MidiInputEventDecoder, Sensor};

mod output;
pub use self::output::{
led_output_into_midi_message, DeckLed, InvalidOutputControlIndex, Led, MainLed, OutputGateway,
};

pub const MIDI_DEVICE_DESCRIPTOR: &MidiDeviceDescriptor = &MidiDeviceDescriptor {
device: DeviceDescriptor {
vendor_name: Cow::Borrowed("Pioneer"),
Expand All @@ -16,23 +23,43 @@ pub const MIDI_DEVICE_DESCRIPTOR: &MidiDeviceDescriptor = &MidiDeviceDescriptor
port_name_prefix: "DDJ-400",
};

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, FromRepr, EnumIter, EnumCount)]
#[repr(u8)]
pub enum Deck {
/// Left
One,
/// Right
Two,
}

const MIDI_CHANNEL_MAIN: u8 = 0x06;
const MIDI_CHANNEL_EFFECT: u8 = 0x04;
impl Deck {
const fn midi_channel(self) -> u8 {
match self {
Self::One => MIDI_CHANNEL_DECK_ONE,
Self::Two => MIDI_CHANNEL_DECK_TWO,
}
}

const fn control_index_bit_mask(self) -> u32 {
match self {
Self::One => CONTROL_INDEX_DECK_ONE,
Self::Two => CONTROL_INDEX_DECK_TWO,
}
}
}

const MIDI_CHANNEL_DECK_ONE: u8 = 0x00;
const MIDI_CHANNEL_DECK_TWO: u8 = 0x01;
const MIDI_CHANNEL_EFFECT: u8 = 0x04;
const MIDI_CHANNEL_MAIN: u8 = 0x06;
const MIDI_CHANNEL_PERFORMANCE_DECK_ONE: u8 = 0x07;
// TODO: const MIDI_CHANNEL_PERFORMANCE_DECK_ONE_SHIFTED: u8 = 0x08;
const MIDI_CHANNEL_PERFORMANCE_DECK_TWO: u8 = 0x09;
// TODO: const MIDI_CHANNEL_PERFORMANCE_DECK_TWO_SHIFTED: u8 = 0x0A;
// TODO: const MIDI_CHANNEL_OUT: u8 = 0x15;

const MIDI_COMMAND_NOTE_ON: u8 = 0x90;
const MIDI_COMMAND_CC: u8 = 0xb0;
const MIDI_COMMAND_CC: u8 = 0xB0;

const MIDI_STATUS_BUTTON_MAIN: u8 = MIDI_COMMAND_NOTE_ON | MIDI_CHANNEL_MAIN;
const MIDI_STATUS_BUTTON_EFFECT: u8 = MIDI_COMMAND_NOTE_ON | MIDI_CHANNEL_EFFECT;
Expand All @@ -48,6 +75,11 @@ const MIDI_STATUS_CC_EFFECT: u8 = MIDI_COMMAND_CC | MIDI_CHANNEL_EFFECT;
const MIDI_STATUS_CC_DECK_ONE: u8 = MIDI_COMMAND_CC | MIDI_CHANNEL_DECK_ONE;
const MIDI_STATUS_CC_DECK_TWO: u8 = MIDI_COMMAND_CC | MIDI_CHANNEL_DECK_TWO;

const MIDI_DECK_PLAYPAUSE_BUTTON: u8 = 0x0B;

const MIDI_MASTER_CUE: u8 = 0x63;
const MIDI_BEAT_FX: u8 = 0x47;

const CONTROL_INDEX_DECK_ONE: u32 = 0x0100;
const CONTROL_INDEX_DECK_TWO: u32 = 0x0200;
const CONTROL_INDEX_PERFORMANCE_DECK_ONE: u32 = 0x0300;
Expand Down
231 changes: 231 additions & 0 deletions src/devices/pioneer_ddj_400/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// SPDX-FileCopyrightText: The djio authors
// SPDX-License-Identifier: MPL-2.0

use derive_more::From;
use strum::{EnumCount, EnumIter, FromRepr, IntoEnumIterator as _};

use super::{
Deck, CONTROL_INDEX_DECK_BIT_MASK, CONTROL_INDEX_DECK_ONE, CONTROL_INDEX_DECK_TWO,
CONTROL_INDEX_ENUM_BIT_MASK, MIDI_BEAT_FX, MIDI_COMMAND_NOTE_ON, MIDI_DECK_PLAYPAUSE_BUTTON,
MIDI_MASTER_CUE, MIDI_STATUS_BUTTON_MAIN,
};
use crate::{
ControlIndex, ControlOutputGateway, ControlRegister, LedOutput, MidiOutputConnection,
MidiOutputGateway, OutputError, OutputResult,
};

#[derive(Debug, Clone, Copy, From)]
pub enum Led {
Main(MainLed),
Deck(Deck, DeckLed),
Performance(Deck, PerformanceLed),
}

impl Led {
#[must_use]
pub const fn deck(self) -> Option<Deck> {
match self {
Self::Main(_) => None,
Self::Deck(deck, _) | Self::Performance(deck, _) => Some(deck),
}
}

#[must_use]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::match_wildcard_for_single_variants)]
pub const fn to_control_index(self) -> ControlIndex {
match self {
Self::Main(led) => ControlIndex::new(led as u32),
Self::Deck(deck, led) => ControlIndex::new(deck.control_index_bit_mask() | led as u32),
_ => todo!(),
}
}
}

const LED_OFF: u8 = 0x00;
const LED_ON: u8 = 0x7f;

fn led_to_u7(output: LedOutput) -> u8 {
match output {
LedOutput::Off => LED_OFF,
LedOutput::On => LED_ON,
}
}

/// Deck LED
#[derive(Debug, Clone, Copy, FromRepr, EnumIter, EnumCount)]
#[repr(u8)]
pub enum DeckLed {
PlayPauseButton,
CueButton,
BeatSyncButton,
LoopInButton,
LoopOutButton,
ReloopExitButton,
// -- Mixer section -- //
HeadphoneCueButton,
}

/// Main LED
#[derive(Debug, Clone, Copy, FromRepr, EnumIter, EnumCount)]
#[repr(u8)]
pub enum MainLed {
MasterCue,
BeatFx,
}

#[derive(Debug, Clone, Copy, FromRepr, EnumIter, EnumCount)]
pub enum PerformanceLed {
// TODO
}

impl From<Led> for ControlIndex {
fn from(from: Led) -> Self {
from.to_control_index()
}
}

#[derive(Debug)]
pub struct InvalidOutputControlIndex;

impl TryFrom<ControlIndex> for Led {
type Error = InvalidOutputControlIndex;

fn try_from(from: ControlIndex) -> Result<Self, Self::Error> {
let value = from.value();
debug_assert!(CONTROL_INDEX_ENUM_BIT_MASK <= u8::MAX.into());
let enum_index = (value & CONTROL_INDEX_ENUM_BIT_MASK) as u8;
let deck = match value & CONTROL_INDEX_DECK_BIT_MASK {
CONTROL_INDEX_DECK_ONE => Deck::One,
CONTROL_INDEX_DECK_TWO => Deck::Two,
CONTROL_INDEX_DECK_BIT_MASK => return Err(InvalidOutputControlIndex),
_ => {
return MainLed::from_repr(enum_index)
.map(Led::Main)
.ok_or(InvalidOutputControlIndex);
}
};
DeckLed::from_repr(enum_index)
.map(|led| Led::Deck(deck, led))
.ok_or(InvalidOutputControlIndex)
}
}

#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn led_output_into_midi_message(led: Led, output: LedOutput) -> [u8; 3] {
let (status, data1) = match led {
Led::Main(led) => match led {
MainLed::MasterCue => (MIDI_STATUS_BUTTON_MAIN, MIDI_MASTER_CUE),
MainLed::BeatFx => (MIDI_STATUS_BUTTON_MAIN, MIDI_BEAT_FX),
},
Led::Deck(deck, led) => {
let midi_channel = deck.midi_channel();
let status = MIDI_COMMAND_NOTE_ON | midi_channel;
let data1 = match led {
DeckLed::PlayPauseButton => MIDI_DECK_PLAYPAUSE_BUTTON,
DeckLed::CueButton => 0x0c,
DeckLed::BeatSyncButton => 0x58,
DeckLed::LoopInButton => 0x10,
DeckLed::LoopOutButton => 0x11,
DeckLed::ReloopExitButton => 0x4D,
DeckLed::HeadphoneCueButton => 0x54,
};
(status, data1)
}
Led::Performance(_deck, _led) => {
todo!()
}
};
let data2 = led_to_u7(output);
[status, data1, data2]
}

fn send_led_output<C: MidiOutputConnection>(
midi_output_connection: &mut C,
led: Led,
output: LedOutput,
) -> OutputResult<()> {
midi_output_connection.send_midi_output(&led_output_into_midi_message(led, output))
}

fn on_attach<C: MidiOutputConnection>(midi_output_connection: &mut C) -> OutputResult<()> {
// TODO: How to query the initial position of all knobs and faders?
turn_off_all_leds(midi_output_connection)?;
Ok(())
}

fn on_detach<C: MidiOutputConnection>(midi_output_connection: &mut C) -> OutputResult<()> {
turn_off_all_leds(midi_output_connection)?;
Ok(())
}

fn turn_off_all_leds<C: MidiOutputConnection>(midi_output_connection: &mut C) -> OutputResult<()> {
for led in MainLed::iter() {
send_led_output(midi_output_connection, led.into(), LedOutput::Off)?;
}
for deck in Deck::iter() {
for led in DeckLed::iter() {
send_led_output(midi_output_connection, Led::Deck(deck, led), LedOutput::Off)?;
}
}
Ok(())
}

#[derive(Debug)]
#[allow(missing_debug_implementations)]
pub struct OutputGateway<C> {
midi_output_connection: Option<C>,
}

impl<C> Default for OutputGateway<C> {
fn default() -> Self {
Self {
midi_output_connection: None,
}
}
}

impl<C: MidiOutputConnection> OutputGateway<C> {
pub fn send_led_output(&mut self, led: Led, output: LedOutput) -> OutputResult<()> {
let Some(midi_output_connection) = &mut self.midi_output_connection else {
return Err(OutputError::Disconnected);
};
send_led_output(midi_output_connection, led, output)
}
}

impl<C: MidiOutputConnection> ControlOutputGateway for OutputGateway<C> {
fn send_output(&mut self, output: &ControlRegister) -> OutputResult<()> {
let ControlRegister { index, value } = *output;
let led = Led::try_from(index).map_err(|InvalidOutputControlIndex| OutputError::Send {
msg: format!("No LED with control index {index}").into(),
})?;
self.send_led_output(led, value.into())
}
}

impl<C: MidiOutputConnection> MidiOutputGateway<C> for OutputGateway<C> {
fn attach_midi_output_connection(
&mut self,
midi_output_connection: &mut Option<C>,
) -> OutputResult<()> {
assert!(self.midi_output_connection.is_none());
assert!(midi_output_connection.is_some());
// Initialize the hardware
on_attach(midi_output_connection.as_mut().expect("Some"))?;
// Finally take ownership
self.midi_output_connection = midi_output_connection.take();
Ok(())
}

fn detach_midi_output_connection(&mut self) -> Option<C> {
// Release ownership
let mut midi_output_connection = self.midi_output_connection.take()?;
// Reset the hardware
if let Err(err) = on_detach(&mut midi_output_connection) {
log::warn!("Failed reset MIDI hardware on detach: {err}");
}
Some(midi_output_connection)
}
}

0 comments on commit b07dc75

Please sign in to comment.