Skip to content

Commit

Permalink
#1301 Add Stream Deck support WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Nov 3, 2024
1 parent 3d1d2a9 commit 09f8d6a
Show file tree
Hide file tree
Showing 25 changed files with 1,498 additions and 463 deletions.
275 changes: 249 additions & 26 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ open = "5.0.1"
url = "2.5.2"
atomic = "0.6.0"
static_assertions = "1.1.0"
image = { version = "0.25.2", default-features = false }

[profile.release]
# This is important for having line numbers in bug reports.
Expand Down
60 changes: 60 additions & 0 deletions api/src/persistence/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub enum Source {
Osc(OscSource),
// Keyboard
Key(KeySource),
// StreamDeck
StreamDeck(StreamDeckSource),
// Virtual
Virtual(VirtualSource),
}
Expand Down Expand Up @@ -308,6 +310,64 @@ pub struct KeySource {
pub keystroke: Option<Keystroke>,
}

#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct StreamDeckSource {
pub button_index: u32,
pub button_design: StreamDeckButtonDesign,
}

#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonDesign {
pub background: StreamDeckButtonBackground,
pub foreground: StreamDeckButtonForeground,
}

#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum StreamDeckButtonForeground {
Solid(StreamDeckButtonSolidForeground),
Image(StreamDeckButtonImageForeground),
Bar(StreamDeckButtonBarForeground),
Arc(StreamDeckButtonArcForeground),
}

impl Default for StreamDeckButtonForeground {
fn default() -> Self {
Self::Solid(StreamDeckButtonSolidForeground::default())
}
}

#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum StreamDeckButtonBackground {
Solid(StreamDeckButtonSolidBackground),
Image(StreamDeckButtonImageBackground),
}

impl Default for StreamDeckButtonBackground {
fn default() -> Self {
Self::Solid(StreamDeckButtonSolidBackground::default())
}
}

#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonImageForeground {}

#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonSolidForeground {}

#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonBarForeground {}

#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonArcForeground {}

#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonImageBackground {}

#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonSolidBackground {}

#[derive(Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Keystroke {
pub modifiers: u8,
Expand Down
6 changes: 1 addition & 5 deletions dialogs/src/mapping_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,13 @@ pub fn create(context: ScopedContext, ids: &mut IdGenerator) -> Dialog {
) + NOT_WS_GROUP,
dropdown(
ids.named_id("ID_SOURCE_CHANNEL_COMBO_BOX"),
context.rect(48, 136, 120, 30),
context.rect(48, 136, 120, 15),
) + WS_VSCROLL
+ WS_TABSTOP,
edittext(
ids.named_id("ID_SOURCE_LINE_3_EDIT_CONTROL"),
context.rect(48, 135, 120, 14),
) + ES_AUTOHSCROLL,
dropdown(
ids.named_id("ID_SOURCE_MIDI_CLOCK_TRANSPORT_MESSAGE_TYPE_COMBOX_BOX"),
context.rect(48, 136, 120, 15),
) + WS_TABSTOP,
ltext(
"Note/CC number",
ids.named_id("ID_SOURCE_NOTE_OR_CC_NUMBER_LABEL_TEXT"),
Expand Down
8 changes: 6 additions & 2 deletions main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ tower-http = { version = "0.4.2", features = ["cors"] }
tonic.workspace = true
prost.workspace = true
# For generating projection QR code
qrcode = { version = "0.13.0" }
qrcode = { version = "0.14.1" }
# For rendering projection QR code to PNG
image = "0.24.8"
image = { workspace = true, features = ["png"] }
# For generating self-signed certificate for projection web server
rcgen = "0.12.0"
# For showing different ways of connecting to this computer (projection feature)
Expand Down Expand Up @@ -209,6 +209,10 @@ serde_plain.workspace = true
atomic.workspace = true
# For making sure that sharing global audio state uses atomics
static_assertions.workspace = true
# For Stream Deck support
streamdeck = "0.9.0"
# For Stream Deck support
hidapi = "2.4"

[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
# For speech source
Expand Down
2 changes: 1 addition & 1 deletion main/lib/helgoboss-learn
140 changes: 134 additions & 6 deletions main/src/application/source_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::base::CloneAsDefault;
use crate::domain::{
Backbone, CompartmentKind, CompartmentParamIndex, CompoundMappingSource, EelMidiSourceScript,
ExtendedSourceCharacter, FlexibleMidiSourceScript, KeySource, Keystroke, LuaMidiSourceScript,
MidiSource, RealearnParameterSource, ReaperSource, SpeechSource, TimerSource,
MidiSource, RealearnParameterSource, ReaperSource, SpeechSource, StreamDeckSource, TimerSource,
VirtualControlElement, VirtualControlElementId, VirtualSource,
};
use derive_more::Display;
Expand All @@ -16,7 +16,10 @@ use helgoboss_learn::{
DEFAULT_OSC_ARG_VALUE_RANGE,
};
use helgoboss_midi::{Channel, U14, U7};
use helgobox_api::persistence::{MidiScriptKind, VirtualControlElementCharacter};
use helgobox_api::persistence::{
MidiScriptKind, StreamDeckButtonBackground, StreamDeckButtonDesign, StreamDeckButtonForeground,
VirtualControlElementCharacter,
};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use serde_repr::*;
Expand Down Expand Up @@ -54,6 +57,9 @@ pub enum SourceCommand {
SetTimerMillis(u64),
SetParameterIndex(CompartmentParamIndex),
SetKeystroke(Option<Keystroke>),
SetButtonIndex(u32),
SetButtonBackgroundType(StreamDeckButtonBackgroundType),
SetButtonForegroundType(StreamDeckButtonForegroundType),
SetControlElementCharacter(VirtualControlElementCharacter),
SetControlElementId(VirtualControlElementId),
}
Expand Down Expand Up @@ -87,6 +93,9 @@ pub enum SourceProp {
TimerMillis,
ParameterIndex,
Keystroke,
ButtonIndex,
ButtonBackgroundType,
ButtonForegroundType,
}

impl GetProcessingRelevance for SourceProp {
Expand Down Expand Up @@ -213,6 +222,18 @@ impl<'a> Change<'a> for SourceModel {
self.keystroke = v;
One(P::Keystroke)
}
C::SetButtonIndex(v) => {
self.button_index = v;
One(P::ButtonIndex)
}
C::SetButtonBackgroundType(v) => {
self.button_background_type = v;
One(P::ButtonBackgroundType)
}
C::SetButtonForegroundType(v) => {
self.button_foreground_type = v;
One(P::ButtonForegroundType)
}
};
Some(affected)
}
Expand Down Expand Up @@ -250,6 +271,10 @@ pub struct SourceModel {
parameter_index: CompartmentParamIndex,
// Key
keystroke: Option<Keystroke>,
// Stream Deck
button_index: u32,
button_background_type: StreamDeckButtonBackgroundType,
button_foreground_type: StreamDeckButtonForegroundType,
// Virtual
control_element_character: VirtualControlElementCharacter,
control_element_id: VirtualControlElementId,
Expand Down Expand Up @@ -291,6 +316,9 @@ impl SourceModel {
timer_millis: Default::default(),
parameter_index: Default::default(),
keystroke: None,
button_index: 0,
button_background_type: Default::default(),
button_foreground_type: Default::default(),
}
}

Expand Down Expand Up @@ -382,6 +410,18 @@ impl SourceModel {
self.keystroke
}

pub fn button_index(&self) -> u32 {
self.button_index
}

pub fn button_background_type(&self) -> StreamDeckButtonBackgroundType {
self.button_background_type
}

pub fn button_foreground_type(&self) -> StreamDeckButtonForegroundType {
self.button_foreground_type
}

pub fn reaper_source_type(&self) -> ReaperSourceType {
self.reaper_source_type
}
Expand All @@ -408,7 +448,7 @@ impl SourceModel {
Midi => self.midi_source_type.supports_control(),
Osc => self.osc_arg_type_tag.supports_control(),
Reaper => self.reaper_source_type.supports_control(),
Virtual | Keyboard => true,
Virtual | Keyboard | StreamDeck => true,
// Main use case: Group interaction (follow-only).
Never => true,
}
Expand All @@ -420,7 +460,7 @@ impl SourceModel {
Midi => self.midi_source_type.supports_feedback(),
Osc => self.osc_arg_type_tag.supports_feedback(),
Reaper => self.reaper_source_type.supports_feedback(),
Virtual => true,
StreamDeck | Virtual => true,
Keyboard | Never => false,
}
}
Expand Down Expand Up @@ -514,6 +554,10 @@ impl SourceModel {
MidiDeviceChanges | RealearnInstanceStart | Timer(_) | Speech(_) => {}
}
}
StreamDeck(s) => {
self.category = SourceCategory::StreamDeck;
self.button_index = s.button_index;
}
Never => {
self.category = SourceCategory::Never;
}
Expand Down Expand Up @@ -563,7 +607,9 @@ impl SourceModel {
DetailedSourceCharacter::RangeControl,
DetailedSourceCharacter::Relative,
],
CompoundMappingSource::Key(_) => vec![DetailedSourceCharacter::MomentaryOnOffButton],
CompoundMappingSource::Key(_) | CompoundMappingSource::StreamDeck(_) => {
vec![DetailedSourceCharacter::MomentaryOnOffButton]
}
}
}

Expand Down Expand Up @@ -683,12 +729,44 @@ impl SourceModel {
};
CompoundMappingSource::Reaper(reaper_source)
}
Never => CompoundMappingSource::Never,
Keyboard => CompoundMappingSource::Key(self.create_key_source()?),
StreamDeck => CompoundMappingSource::StreamDeck(self.create_stream_deck_source()),
Never => CompoundMappingSource::Never,
};
Some(source)
}

pub fn create_stream_deck_source(&self) -> StreamDeckSource {
StreamDeckSource::new(self.button_index, self.create_stream_deck_button_design())
}

pub fn create_stream_deck_button_design(&self) -> StreamDeckButtonDesign {
StreamDeckButtonDesign {
background: match self.button_background_type {
StreamDeckButtonBackgroundType::Solid => {
StreamDeckButtonBackground::Solid(Default::default())
}
StreamDeckButtonBackgroundType::Image => {
StreamDeckButtonBackground::Image(Default::default())
}
},
foreground: match self.button_foreground_type {
StreamDeckButtonForegroundType::Solid => {
StreamDeckButtonForeground::Solid(Default::default())
}
StreamDeckButtonForegroundType::Image => {
StreamDeckButtonForeground::Image(Default::default())
}
StreamDeckButtonForegroundType::Bar => {
StreamDeckButtonForeground::Bar(Default::default())
}
StreamDeckButtonForegroundType::Arc => {
StreamDeckButtonForeground::Arc(Default::default())
}
},
}
}

pub fn create_key_source(&self) -> Option<KeySource> {
Some(KeySource::new(self.keystroke?))
}
Expand Down Expand Up @@ -951,6 +1029,10 @@ impl Display for SourceModel {
.unwrap_or_else(|| Cow::Borrowed(KEY_UNDEFINED_LABEL));
vec![text]
}
StreamDeck => {
let text = self.create_stream_deck_source().to_string();
vec![Cow::Owned(text)]
}
};
let non_empty_lines: Vec<_> = lines.into_iter().filter(|l| !l.is_empty()).collect();
write!(f, "{}", non_empty_lines.join("\n"))
Expand Down Expand Up @@ -991,6 +1073,9 @@ pub enum SourceCategory {
#[serde(rename = "reaper")]
#[display(fmt = "REAPER")]
Reaper,
#[serde(rename = "stream-deck")]
#[display(fmt = "Stream Deck")]
StreamDeck,
#[serde(rename = "virtual")]
#[display(fmt = "Virtual")]
Virtual,
Expand All @@ -1014,6 +1099,7 @@ impl SourceCategory {
Osc => true,
Reaper => true,
Keyboard => true,
StreamDeck => true,
Virtual => false,
},
CompartmentKind::Main => true,
Expand Down Expand Up @@ -1223,6 +1309,48 @@ impl ReaperSourceType {
}
}

#[derive(
Clone, Copy, Debug, PartialEq, Eq, Default, EnumIter, TryFromPrimitive, IntoPrimitive, Display,
)]
#[repr(usize)]
pub enum StreamDeckButtonBackgroundType {
#[default]
Solid,
Image,
}

impl From<&StreamDeckButtonBackground> for StreamDeckButtonBackgroundType {
fn from(value: &StreamDeckButtonBackground) -> Self {
match value {
StreamDeckButtonBackground::Solid(_) => Self::Solid,
StreamDeckButtonBackground::Image(_) => Self::Image,
}
}
}

#[derive(
Clone, Copy, Debug, PartialEq, Eq, Default, EnumIter, TryFromPrimitive, IntoPrimitive, Display,
)]
#[repr(usize)]
pub enum StreamDeckButtonForegroundType {
#[default]
Solid,
Image,
Bar,
Arc,
}

impl From<&StreamDeckButtonForeground> for StreamDeckButtonForegroundType {
fn from(value: &StreamDeckButtonForeground) -> Self {
match value {
StreamDeckButtonForeground::Solid(_) => Self::Solid,
StreamDeckButtonForeground::Image(_) => Self::Image,
StreamDeckButtonForeground::Arc(_) => Self::Arc,
StreamDeckButtonForeground::Bar(_) => Self::Bar,
}
}
}

pub fn parse_osc_feedback_args(text: &str) -> Vec<String> {
text.split_whitespace().map(|s| s.to_owned()).collect()
}
Expand Down
Loading

0 comments on commit 09f8d6a

Please sign in to comment.