Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Installation Editor menu for dynamically updating OSC outputs #45

Merged
merged 3 commits into from
Jan 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions assets/installations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"WrappedInSpectrum": {
"socket": "127.0.0.1:9002",
"osc_addr": "wrap"
},
"TurbulentEncounters": {
"socket": "127.0.0.1:9002",
"osc_addr": "turb"
},
"WavesAtWork": {
"socket": "127.0.0.1:9002",
"osc_addr": "wave"
},
"EnergeticVibrationsAudioVisualiser": {
"socket": "127.0.0.1:9002",
"osc_addr": "enav"
},
"Cacophony": {
"socket": "127.0.0.1:9002",
"osc_addr": "caco"
},
"EnergeticVibrationsProjectionMapping": {
"socket": "127.0.0.1:9002",
"osc_addr": "enpm"
},
"RipplesInSpacetime": {
"socket": "127.0.0.1:9002",
"osc_addr": "ripp"
}
}
2 changes: 1 addition & 1 deletion assets/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
}
},
"role": "Soundscape",
"spread": 2.722478151321411,
"spread": 2.391113996505738,
"radians": 0.0
},
"id": 3
Expand Down
10 changes: 5 additions & 5 deletions assets/speakers.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"next_id": 9,
"next_id": 10,
"speakers": [
{
"audio": {
Expand Down Expand Up @@ -59,13 +59,13 @@
{
"audio": {
"point": {
"x": 15.082670033805812,
"y": 24.881144848668194
"x": 15.243809456998122,
"y": 24.86865506510584
},
"channel": 5
},
"name": "S5",
"id": 5
"name": "S9",
"id": 9
}
]
}
7 changes: 4 additions & 3 deletions src/lib/audio/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use gui;
use installation::Installation;
use metres::Metres;
use nannou;
use nannou::audio::Buffer;
Expand Down Expand Up @@ -92,7 +93,7 @@ pub struct Model {
/// Channel for sending sound analysis data to the OSC output thread.
osc_output_msg_tx: mpsc::Sender<osc::output::Message>,
/// An analysis per installation to re-use for sending to the OSC output thread.
installation_analyses: HashMap<osc::output::Installation, Vec<SpeakerAnalysis>>,
installation_analyses: HashMap<Installation, Vec<SpeakerAnalysis>>,
/// A buffer to re-use for DBAP speaker calculations.
///
/// The index of the speaker is its channel.
Expand Down Expand Up @@ -150,7 +151,7 @@ impl Model {
};

// TODO: Update `installation_analyses` if speaker's installation is new.
let installation = osc::output::Installation::Cacophony;
let installation = Installation::Cacophony;
self.installation_analyses.entry(installation).or_insert_with(Vec::new);

let speaker = ActiveSpeaker { speaker, detector };
Expand Down Expand Up @@ -323,7 +324,7 @@ pub fn render(mut model: Model, mut buffer: Buffer) -> (Model, Buffer) {

// Sum the rms and peak.
// TODO: Get installation associated with speaker.
let installation = osc::output::Installation::Cacophony;
let installation = Installation::Cacophony;
//let installation = active.speaker.installation;
if let Some(speakers) = installation_analyses.get_mut(&installation) {
sum_peak += peak;
Expand Down
250 changes: 250 additions & 0 deletions src/lib/gui/installation_editor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
use gui::{collapsible_area, Channels, Gui, State};
use gui::{ITEM_HEIGHT, SMALL_FONT_SIZE};
use installation::{self, Installation};
use nannou;
use nannou::osc::Connected;
use nannou::ui;
use nannou::ui::prelude::*;
use osc;
use std::collections::HashMap;
use std::net;

#[derive(Clone, Debug)]
#[derive(Deserialize, Serialize)]
pub struct Address {
// The IP address of the target installation computer.
pub socket: net::SocketAddrV4,
// The OSC address string.
pub osc_addr: String,
}

pub type AddressMap = HashMap<Installation, Address>;

pub struct InstallationEditor {
pub is_open: bool,
pub selected: Option<Selected>,
pub address_map: AddressMap,
}

pub struct Selected {
index: usize,
socket_string: String,
osc_addr: String,
}

pub fn set(gui: &mut Gui) -> widget::Id {
let &mut Gui {
ref mut ui,
ref ids,
channels,
state: &mut State {
installation_editor: InstallationEditor {
ref mut is_open,
ref mut selected,
ref mut address_map,
},
..
},
..
} = gui;

// The height of the list of installations.
const LIST_HEIGHT: Scalar = ITEM_HEIGHT * 5.0;
const PAD: Scalar = 6.0;
const TEXT_PAD: Scalar = PAD * 2.0;

// The height of the canvas displaying options for the selected installation.
//
// These options include:
//
// - Music Data OSC Output (Text and TextBox)
let osc_canvas_h = PAD * 2.0 + ITEM_HEIGHT * 3.0;
let selected_canvas_h = osc_canvas_h + PAD * 2.0;

// The total height of the installation editor as a sum of the previous heights plus necessary
// padding..
let installation_editor_h = LIST_HEIGHT + selected_canvas_h;

let (area, event) = collapsible_area(*is_open, "Installation Editor", ids.side_menu)
.mid_top_of(ids.side_menu)
.set(ids.installation_editor, ui);
if let Some(event) = event {
*is_open = event.is_open();
}

// If the area is open, continue. If its closed, return the editor id as the last id.
let area = match area {
Some(area) => area,
None => return ids.installation_editor,
};

// The canvas on which the installation editor widgets will be placed.
let canvas = widget::Canvas::new()
.pad(0.0)
.h(installation_editor_h);
area.set(canvas, ui);

// Display the installation list.
let num_items = installation::ALL.len();
let (mut events, scrollbar) = widget::ListSelect::single(num_items)
.item_size(ITEM_HEIGHT)
.h(LIST_HEIGHT)
.align_middle_x_of(area.id)
.align_top_of(area.id)
.scrollbar_color(color::LIGHT_CHARCOAL)
.scrollbar_next_to()
.set(ids.installation_editor_list, ui);

while let Some(event) = events.next(ui, |i| selected.as_ref().map(|s| s.index) == Some(i)) {
use self::ui::widget::list_select::Event;
match event {
// Instantiate a button for each installation.
Event::Item(item) => {
let installation = Installation::from_usize(item.i).expect("no installation for index");
let is_selected = selected.as_ref().map(|s| s.index) == Some(item.i);
// Blue if selected, gray otherwise.
let color = if is_selected { color::BLUE } else { color::CHARCOAL };
let label = installation.display_str();

// Use `Button`s for the selectable items.
let button = widget::Button::new()
.label(&label)
.label_font_size(SMALL_FONT_SIZE)
.label_x(position::Relative::Place(position::Place::Start(Some(10.0))))
.color(color);
item.set(button, ui);
},

// Update the selected source.
Event::Selection(index) => {
let installation = Installation::from_usize(index).expect("no installation for index");
let (socket_string, osc_addr) = {
let address = &address_map[&installation];
(format!("{}", address.socket), address.osc_addr.clone())
};
*selected = Some(Selected { index, socket_string, osc_addr });
},

_ => (),
}
}

if let Some(scrollbar) = scrollbar {
scrollbar.set(ui);
}

let area_rect = ui.rect_of(area.id).unwrap();
let start = area_rect.y.start;
let end = start + selected_canvas_h;
let selected_canvas_y = ui::Range { start, end };

widget::Canvas::new()
.pad(PAD)
.w_of(ids.side_menu)
.h(selected_canvas_h)
.y(selected_canvas_y.middle())
.align_middle_x_of(ids.side_menu)
.set(ids.installation_editor_selected_canvas, ui);
let selected_canvas_kid_area = ui.kid_area_of(ids.installation_editor_selected_canvas).unwrap();

let selected = match *selected {
Some(ref mut selected) => selected,
None => return area.id,
};

let installation = Installation::from_usize(selected.index).expect("no installation for index");

// The canvas for displaying the osc output address editor.
widget::Canvas::new()
.mid_top_of(ids.installation_editor_selected_canvas)
.color(color::CHARCOAL)
.w(selected_canvas_kid_area.w())
.h(osc_canvas_h)
.pad(PAD)
.set(ids.installation_editor_osc_canvas, ui);

// OSC output address header.
widget::Text::new("Audio Data OSC Output")
.font_size(SMALL_FONT_SIZE)
.top_left_of(ids.installation_editor_osc_canvas)
.set(ids.installation_editor_osc_text, ui);

fn osc_sender(socket: &net::SocketAddrV4) -> nannou::osc::Sender<Connected> {
nannou::osc::sender()
.expect("failed to create OSC sender")
.connect(socket)
.expect("failed to connect OSC sender")
}

fn update_addr(
installation: Installation,
selected: &Selected,
channels: &Channels,
address_map: &mut AddressMap,
) {
let socket = match selected.socket_string.parse() {
Ok(s) => s,
Err(_) => return,
};
let osc_tx = osc_sender(&socket);
let osc_addr = selected.osc_addr.clone();
let add = osc::output::OscTarget::Add(installation, osc_tx, osc_addr.clone());
let msg = osc::output::Message::Osc(add);
if channels.osc_out_msg_tx.send(msg).ok().is_some() {
let addr = Address { socket, osc_addr };
address_map.insert(installation, addr);
}
}

// The textbox for editing the OSC output IP address.
let color = match selected.socket_string.parse::<net::SocketAddrV4>() {
Ok(socket) => match address_map[&installation].socket == socket {
true => color::BLACK,
false => color::DARK_GREEN.with_luminance(0.1),
},
Err(_) => color::DARK_RED.with_luminance(0.1),
};
for event in widget::TextBox::new(&selected.socket_string)
.align_middle_x_of(ids.installation_editor_osc_canvas)
.down(TEXT_PAD)
.parent(ids.installation_editor_osc_canvas)
.kid_area_w_of(ids.installation_editor_osc_canvas)
.h(ITEM_HEIGHT)
.font_size(SMALL_FONT_SIZE)
.color(color)
.set(ids.installation_editor_osc_ip_text_box, ui)
{
use nannou::ui::conrod::widget::text_box::Event;
match event {
Event::Enter => {
update_addr(installation, &selected, channels, address_map);
},
Event::Update(new_string) => {
selected.socket_string = new_string;
},
}
}

// The textbox for editing the OSC output address.
for event in widget::TextBox::new(&selected.osc_addr)
.align_middle_x_of(ids.installation_editor_osc_canvas)
.down(PAD)
.parent(ids.installation_editor_osc_canvas)
.kid_area_w_of(ids.installation_editor_osc_canvas)
.h(ITEM_HEIGHT)
.font_size(SMALL_FONT_SIZE)
.set(ids.installation_editor_osc_address_text_box, ui)
{
use nannou::ui::conrod::widget::text_box::Event;
match event {
Event::Enter => {
update_addr(installation, &selected, channels, address_map);
},
Event::Update(new_string) => {
selected.osc_addr = new_string;
},
}
}

area.id
}
48 changes: 48 additions & 0 deletions src/lib/gui/interaction_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use gui::{collapsible_area, info_text, Gui};
use nannou::ui::prelude::*;

pub fn set(last_area_id: widget::Id, gui: &mut Gui) -> widget::Id {
let is_open = gui.state.interaction_log_is_open;
let log_canvas_h = 200.0;
let (area, event) = collapsible_area(is_open, "Interaction Log", gui.ids.side_menu)
.align_middle_x_of(gui.ids.side_menu)
.down_from(last_area_id, 0.0)
.set(gui.ids.interaction_log, gui);
if let Some(event) = event {
gui.state.interaction_log_is_open = event.is_open();
}

if let Some(area) = area {
// The canvas on which the log will be placed.
let canvas = widget::Canvas::new()
.scroll_kids()
.pad(10.0)
.h(log_canvas_h);
area.set(canvas, gui);

// The text widget used to display the log.
let log_string = match gui.state.interaction_log.len() {
0 => format!("No interactions received yet.\nListening on port {}...",
gui.state.config.osc_input_port),
_ => gui.state.interaction_log.format(),
};
info_text(&log_string)
.top_left_of(area.id)
.kid_area_w_of(area.id)
.set(gui.ids.interaction_log_text, gui);

// Scrollbars.
widget::Scrollbar::y_axis(area.id)
.color(color::LIGHT_CHARCOAL)
.auto_hide(false)
.set(gui.ids.interaction_log_scrollbar_y, gui);
widget::Scrollbar::x_axis(area.id)
.color(color::LIGHT_CHARCOAL)
.auto_hide(true)
.set(gui.ids.interaction_log_scrollbar_x, gui);

area.id
} else {
gui.ids.interaction_log
}
}
Loading