Merge branch 'audio-devices' into 'master'

Audio device selection

Closes #105, #95, and #100

See merge request veloren/veloren!156

Former-commit-id: 59c6cb0bc5eab295985853a3945874d96db9e894
This commit is contained in:
Joshua Barretto 2019-05-22 11:39:14 +00:00
commit 5636a5ac73
7 changed files with 163 additions and 76 deletions

BIN
assets/voxygen/audio/soundtrack/field_grazing.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,3 +1,4 @@
use crate::settings::AudioSettings;
use common::assets; use common::assets;
use rand::prelude::*; use rand::prelude::*;
use rodio::{Decoder, Device, Source, SpatialSink}; use rodio::{Decoder, Device, Source, SpatialSink};
@ -5,6 +6,7 @@ use std::{
collections::HashMap, collections::HashMap,
fs::File, fs::File,
io::BufReader, io::BufReader,
iter::{Filter, Iterator},
path::PathBuf, path::PathBuf,
sync::mpsc::{channel, Receiver, Sender, TryRecvError}, sync::mpsc::{channel, Receiver, Sender, TryRecvError},
thread, thread,
@ -15,30 +17,31 @@ use vek::*;
pub struct AudioFrontend { pub struct AudioFrontend {
device: Device, device: Device,
// Performance optimisation, iterating through available audio devices takes time
devices: Vec<Device>,
// streams: HashMap<String, SpatialSink>, //always use SpatialSink even if no possition is used for now // streams: HashMap<String, SpatialSink>, //always use SpatialSink even if no possition is used for now
stream: SpatialSink, stream: SpatialSink,
} }
impl AudioFrontend { impl AudioFrontend {
pub fn new() -> Self { pub fn new(settings: &AudioSettings) -> Self {
let mut device = rodio::default_output_device().unwrap(); let mut device = match &settings.audio_device {
Some(dev) => rodio::output_devices()
for d in rodio::devices() { .find(|x| &x.name() == dev)
if d.name().contains("jack") { .or_else(rodio::default_output_device)
continue; .expect("No Audio devices found"),
} None => rodio::default_output_device().expect("No audio devices found"),
};
device = d;
break;
}
let mut sink = let mut sink =
rodio::SpatialSink::new(&device, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]); rodio::SpatialSink::new(&device, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]);
sink.set_volume(settings.music_volume);
AudioFrontend { AudioFrontend {
device, device,
// streams: HashMap::<String, SpatialSink>::new(), // streams: HashMap::<String, SpatialSink>::new(),
stream: sink, stream: sink,
devices: AudioFrontend::list_devices_raw(),
} }
} }
@ -46,27 +49,20 @@ impl AudioFrontend {
let bufreader = assets::load_from_path(path).unwrap(); let bufreader = assets::load_from_path(path).unwrap();
let src = Decoder::new(bufreader).unwrap(); let src = Decoder::new(bufreader).unwrap();
let mut sink = rodio::SpatialSink::new( // TODO: stop previous audio from playing. Sink has this ability, but
&self.device, // SpatialSink does not for some reason. This means that we will
[0.0, 0.0, 0.0], // probably want to use sinks for music, and SpatialSink for sfx.
[1.0, 0.0, 0.0], self.stream.append(src);
[-1.0, 0.0, 0.0],
);
sink.append(src);
// self.streams.insert(path.to_string(), sink);
self.stream = sink;
} }
pub fn maintain(&mut self) { pub fn maintain(&mut self) {
let music = [ let music = [
"voxygen/audio/soundtrack/Ethereal_Bonds.ogg", "voxygen/audio/soundtrack/Ethereal_Bonds.ogg",
"voxygen/audio/soundtrack/Field_Grazing.mp3", "voxygen/audio/soundtrack/field_grazing.ogg",
"voxygen/audio/soundtrack/fiesta_del_pueblo.ogg", "voxygen/audio/soundtrack/fiesta_del_pueblo.ogg",
"voxygen/audio/soundtrack/library_theme_with_harpsichord.ogg", "voxygen/audio/soundtrack/library_theme_with_harpsichord.ogg",
"voxygen/audio/soundtrack/Mineral_Deposits.ogg", "voxygen/audio/soundtrack/Mineral_Deposits.ogg",
"voxygen/audio/soundtrack/Ruination.ogg", //"voxygen/audio/soundtrack/Ruination.ogg",
"voxygen/audio/soundtrack/sacred_temple.ogg", "voxygen/audio/soundtrack/sacred_temple.ogg",
"voxygen/audio/soundtrack/Snowtop.ogg", "voxygen/audio/soundtrack/Snowtop.ogg",
"voxygen/audio/soundtrack/veloren_title_tune-3.ogg", "voxygen/audio/soundtrack/veloren_title_tune-3.ogg",
@ -80,4 +76,49 @@ impl AudioFrontend {
pub fn set_volume(&mut self, volume: f32) { pub fn set_volume(&mut self, volume: f32) {
self.stream.set_volume(volume.min(1.0).max(0.0)) self.stream.set_volume(volume.min(1.0).max(0.0))
} }
/// Returns a vec of the audio devices available.
/// Does not return rodio Device struct in case our audio backend changes.
pub fn list_device_names(&self) -> Vec<String> {
self.devices.iter().map(|x| x.name()).collect()
}
/// Returns vec of devices
fn list_devices_raw() -> Vec<Device> {
rodio::output_devices().collect()
}
/// Caches vec of devices for later reference
fn maintain_devices(&mut self) {
self.devices = AudioFrontend::list_devices_raw()
}
/// Returns the default audio device.
/// Does not return rodio Device struct in case our audio backend changes.
pub fn get_default_device() -> String {
rodio::default_output_device()
.expect("No audio output devices detected.")
.name()
}
/// Returns the name of the current audio device.
/// Does not return rodio Device struct in case our audio backend changes.
pub fn get_device_name(&self) -> String {
self.device.name()
}
/// Sets the current audio device from a string.
/// Does not use the rodio Device struct in case that detail changes.
/// If the string is an invalid audio device, then no change is made.
pub fn set_device(&mut self, name: String) {
if let Some(dev) = rodio::output_devices().find(|x| x.name() == name) {
self.device = dev;
self.stream = rodio::SpatialSink::new(
&self.device,
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[-1.0, 0.0, 0.0],
);
}
}
} }

View File

@ -92,6 +92,7 @@ pub enum Event {
SendMessage(String), SendMessage(String),
AdjustViewDistance(u32), AdjustViewDistance(u32),
AdjustVolume(f32), AdjustVolume(f32),
ChangeAudioDevice(String),
Logout, Logout,
Quit, Quit,
} }
@ -207,15 +208,11 @@ pub struct Hud {
inventory_space: u32, inventory_space: u32,
show: Show, show: Show,
to_focus: Option<Option<widget::Id>>, to_focus: Option<Option<widget::Id>>,
settings: Settings,
force_ungrab: bool, force_ungrab: bool,
// TODO: move to settings
current_vd: u32,
current_volume: f32,
} }
impl Hud { impl Hud {
pub fn new(window: &mut Window, settings: Settings) -> Self { pub fn new(window: &mut Window) -> Self {
let mut ui = Ui::new(window).unwrap(); let mut ui = Ui::new(window).unwrap();
// TODO: Adjust/remove this, right now it is used to demonstrate window scaling functionality. // TODO: Adjust/remove this, right now it is used to demonstrate window scaling functionality.
ui.scaling_mode(ScaleMode::RelativeToWindow([1920.0, 1080.0].into())); ui.scaling_mode(ScaleMode::RelativeToWindow([1920.0, 1080.0].into()));
@ -246,14 +243,11 @@ impl Hud {
want_grab: true, want_grab: true,
}, },
to_focus: None, to_focus: None,
settings,
force_ungrab: false, force_ungrab: false,
current_vd: 5,
current_volume: 0.5,
} }
} }
fn update_layout(&mut self, tps: f64) -> Vec<Event> { fn update_layout(&mut self, tps: f64, global_state: &GlobalState) -> Vec<Event> {
let mut events = Vec::new(); let mut events = Vec::new();
let ref mut ui_widgets = self.ui.set_widgets(); let ref mut ui_widgets = self.ui.set_widgets();
let version = env!("CARGO_PKG_VERSION"); let version = env!("CARGO_PKG_VERSION");
@ -302,7 +296,7 @@ impl Hud {
.top_left_with_margins_on(ui_widgets.window, 3.0, 3.0) .top_left_with_margins_on(ui_widgets.window, 3.0, 3.0)
.w_h(300.0, 190.0) .w_h(300.0, 190.0)
.set(self.ids.help_bg, ui_widgets); .set(self.ids.help_bg, ui_widgets);
Text::new(get_help_text(&self.settings.controls).as_str()) Text::new(get_help_text(&global_state.settings.controls).as_str())
.color(TEXT_COLOR) .color(TEXT_COLOR)
.top_left_with_margins_on(self.ids.help_bg, 20.0, 20.0) .top_left_with_margins_on(self.ids.help_bg, 20.0, 20.0)
.font_id(self.fonts.opensans) .font_id(self.fonts.opensans)
@ -379,14 +373,8 @@ impl Hud {
// Settings // Settings
if let Windows::Settings = self.show.open_windows { if let Windows::Settings = self.show.open_windows {
for event in SettingsWindow::new( for event in SettingsWindow::new(&self.show, &self.imgs, &self.fonts, &global_state)
&self.show, .set(self.ids.settings_window, ui_widgets)
&self.imgs,
&self.fonts,
self.current_vd,
self.current_volume,
)
.set(self.ids.settings_window, ui_widgets)
{ {
match event { match event {
settings_window::Event::ToggleHelp => self.show.toggle_help(), settings_window::Event::ToggleHelp => self.show.toggle_help(),
@ -396,13 +384,14 @@ impl Hud {
settings_window::Event::ToggleDebug => self.show.debug = !self.show.debug, settings_window::Event::ToggleDebug => self.show.debug = !self.show.debug,
settings_window::Event::Close => self.show.open_windows = Windows::None, settings_window::Event::Close => self.show.open_windows = Windows::None,
settings_window::Event::AdjustViewDistance(view_distance) => { settings_window::Event::AdjustViewDistance(view_distance) => {
self.current_vd = view_distance;
events.push(Event::AdjustViewDistance(view_distance)); events.push(Event::AdjustViewDistance(view_distance));
} }
settings_window::Event::AdjustVolume(volume) => { settings_window::Event::AdjustVolume(volume) => {
self.current_volume = volume;
events.push(Event::AdjustVolume(volume)); events.push(Event::AdjustVolume(volume));
} }
settings_window::Event::ChangeAudioDevice(name) => {
events.push(Event::ChangeAudioDevice(name));
}
} }
} }
} }
@ -483,10 +472,6 @@ impl Hud {
pub fn handle_event(&mut self, event: WinEvent, global_state: &mut GlobalState) -> bool { pub fn handle_event(&mut self, event: WinEvent, global_state: &mut GlobalState) -> bool {
let cursor_grabbed = global_state.window.is_cursor_grabbed(); let cursor_grabbed = global_state.window.is_cursor_grabbed();
let handled = match event { let handled = match event {
WinEvent::SettingsChanged => {
self.settings = global_state.settings.clone();
false
}
WinEvent::Ui(event) => { WinEvent::Ui(event) => {
if (self.typing() && event.is_keyboard() && self.show.ui) if (self.typing() && event.is_keyboard() && self.show.ui)
|| !(cursor_grabbed && event.is_keyboard_or_mouse()) || !(cursor_grabbed && event.is_keyboard_or_mouse())
@ -581,12 +566,12 @@ impl Hud {
handled handled
} }
pub fn maintain(&mut self, renderer: &mut Renderer, tps: f64) -> Vec<Event> { pub fn maintain(&mut self, global_state: &mut GlobalState, tps: f64) -> Vec<Event> {
if let Some(maybe_id) = self.to_focus.take() { if let Some(maybe_id) = self.to_focus.take() {
self.ui.focus_widget(maybe_id); self.ui.focus_widget(maybe_id);
} }
let events = self.update_layout(tps); let events = self.update_layout(tps, &global_state);
self.ui.maintain(renderer); self.ui.maintain(&mut global_state.window.renderer_mut());
events events
} }

View File

@ -7,10 +7,11 @@ use crate::{
ImageSlider, ScaleMode, ToggleButton, Ui, ImageSlider, ScaleMode, ToggleButton, Ui,
}, },
window::Window, window::Window,
AudioFrontend, GlobalState,
}; };
use conrod_core::{ use conrod_core::{
color, color,
widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget::{self, Button, DropDownList, Image, List, Rectangle, Scrollbar, Text},
widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
}; };
widget_ids! { widget_ids! {
@ -45,6 +46,8 @@ widget_ids! {
vd_slider_text, vd_slider_text,
audio_volume_slider, audio_volume_slider,
audio_volume_text, audio_volume_text,
audio_device_list,
audio_device_text,
} }
} }
@ -63,8 +66,7 @@ pub struct SettingsWindow<'a> {
imgs: &'a Imgs, imgs: &'a Imgs,
fonts: &'a Fonts, fonts: &'a Fonts,
current_vd: u32, global_state: &'a GlobalState,
current_volume: f32,
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
@ -75,15 +77,13 @@ impl<'a> SettingsWindow<'a> {
show: &'a Show, show: &'a Show,
imgs: &'a Imgs, imgs: &'a Imgs,
fonts: &'a Fonts, fonts: &'a Fonts,
current_vd: u32, global_state: &'a GlobalState,
current_volume: f32,
) -> Self { ) -> Self {
Self { Self {
show, show,
imgs, imgs,
fonts, fonts,
current_vd, global_state,
current_volume,
common: widget::CommonBuilder::default(), common: widget::CommonBuilder::default(),
} }
} }
@ -102,6 +102,7 @@ pub enum Event {
Close, Close,
AdjustViewDistance(u32), AdjustViewDistance(u32),
AdjustVolume(f32), AdjustVolume(f32),
ChangeAudioDevice(String),
} }
impl<'a> Widget for SettingsWindow<'a> { impl<'a> Widget for SettingsWindow<'a> {
@ -490,7 +491,7 @@ impl<'a> Widget for SettingsWindow<'a> {
.set(state.ids.vd_slider_text, ui); .set(state.ids.vd_slider_text, ui);
if let Some(new_val) = ImageSlider::discrete( if let Some(new_val) = ImageSlider::discrete(
self.current_vd, self.global_state.settings.graphics.view_distance,
1, 1,
25, 25,
self.imgs.slider_indicator, self.imgs.slider_indicator,
@ -535,6 +536,7 @@ impl<'a> Widget for SettingsWindow<'a> {
} }
// Contents // Contents
if let SettingsTab::Sound = state.settings_tab { if let SettingsTab::Sound = state.settings_tab {
// Volume Slider ----------------------------------------------------
Text::new("Volume") Text::new("Volume")
.top_left_with_margins_on(state.ids.settings_content, 10.0, 10.0) .top_left_with_margins_on(state.ids.settings_content, 10.0, 10.0)
.font_size(14) .font_size(14)
@ -543,7 +545,7 @@ impl<'a> Widget for SettingsWindow<'a> {
.set(state.ids.audio_volume_text, ui); .set(state.ids.audio_volume_text, ui);
if let Some(new_val) = ImageSlider::continuous( if let Some(new_val) = ImageSlider::continuous(
self.current_volume, self.global_state.settings.audio.music_volume,
0.0, 0.0,
1.0, 1.0,
self.imgs.slider_indicator, self.imgs.slider_indicator,
@ -558,6 +560,29 @@ impl<'a> Widget for SettingsWindow<'a> {
{ {
events.push(Event::AdjustVolume(new_val)); events.push(Event::AdjustVolume(new_val));
} }
// Audio Device Selector --------------------------------------------
let device = self.global_state.audio.get_device_name();
let device_list = self.global_state.audio.list_device_names();
Text::new("Volume")
.down_from(state.ids.audio_volume_slider, 10.0)
.font_size(14)
.font_id(self.fonts.opensans)
.color(TEXT_COLOR)
.set(state.ids.audio_device_text, ui);
// Get which device is currently selected
let selected = device_list.iter().position(|x| x.contains(&device));
if let Some(clicked) = DropDownList::new(&device_list, selected)
.w_h(400.0, 22.0)
.down_from(state.ids.audio_device_text, 10.0)
.label_font_id(self.fonts.opensans)
.set(state.ids.audio_device_list, ui)
{
let new_val = device_list[clicked].clone();
events.push(Event::ChangeAudioDevice(new_val));
}
} }
events events

View File

@ -79,7 +79,7 @@ pub trait PlayState {
fn main() { fn main() {
// Set up the global state. // Set up the global state.
let settings = Settings::load(); let mut settings = Settings::load();
let window = Window::new(&settings).expect("Failed to create window!"); let window = Window::new(&settings).expect("Failed to create window!");
// Initialize logging. // Initialize logging.
@ -159,15 +159,15 @@ fn main() {
default_hook(panic_info); default_hook(panic_info);
})); }));
let mut global_state = GlobalState { if settings.audio.audio_device == None {
settings, settings.audio.audio_device = Some(AudioFrontend::get_default_device());
window, }
audio: AudioFrontend::new(),
};
// TODO: Remove this when the volume setting can be saved let mut global_state = GlobalState {
// Lower the volume to 50% audio: AudioFrontend::new(&settings.audio),
global_state.audio.set_volume(0.5); window,
settings,
};
// Set up the initial play state. // Set up the initial play state.
let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(MainMenuState::new(&mut global_state))]; let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(MainMenuState::new(&mut global_state))];

View File

@ -32,7 +32,7 @@ impl SessionState {
scene, scene,
client, client,
key_state: KeyState::new(), key_state: KeyState::new(),
hud: Hud::new(window, settings), hud: Hud::new(window),
input_events: Vec::new(), input_events: Vec::new(),
} }
} }
@ -173,14 +173,11 @@ impl PlayState for SessionState {
// Maintain the scene. // Maintain the scene.
self.scene.maintain( self.scene.maintain(
global_state.window.renderer_mut(), global_state.window.renderer_mut(),
&mut self.client.borrow_mut(), &self.client.borrow_mut(),
); );
// Maintain the UI. // Maintain the UI.
for event in self for event in self.hud.maintain(global_state, clock.get_tps()) {
.hud
.maintain(global_state.window.renderer_mut(), clock.get_tps())
{
match event { match event {
HudEvent::SendMessage(msg) => { HudEvent::SendMessage(msg) => {
// TODO: Handle result // TODO: Handle result
@ -191,10 +188,22 @@ impl PlayState for SessionState {
return PlayStateResult::Shutdown; return PlayStateResult::Shutdown;
} }
HudEvent::AdjustViewDistance(view_distance) => { HudEvent::AdjustViewDistance(view_distance) => {
self.client.borrow_mut().set_view_distance(view_distance) self.client.borrow_mut().set_view_distance(view_distance);
global_state.settings.graphics.view_distance = view_distance;
global_state.settings.save_to_file();
} }
HudEvent::AdjustVolume(volume) => { HudEvent::AdjustVolume(volume) => {
global_state.audio.set_volume(volume); global_state.audio.set_volume(volume);
global_state.settings.audio.music_volume = volume;
global_state.settings.save_to_file();
}
HudEvent::ChangeAudioDevice(name) => {
global_state.audio.set_device(name.clone());
global_state.settings.audio.audio_device = Some(name);
global_state.settings.save_to_file();
} }
} }
} }

View File

@ -12,6 +12,8 @@ pub struct Settings {
pub controls: ControlSettings, pub controls: ControlSettings,
pub networking: NetworkingSettings, pub networking: NetworkingSettings,
pub log: Log, pub log: Log,
pub graphics: GraphicsSettings,
pub audio: AudioSettings,
} }
/// `ControlSettings` contains keybindings. /// `ControlSettings` contains keybindings.
@ -52,6 +54,22 @@ pub struct Log {
pub file: PathBuf, pub file: PathBuf,
} }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GraphicsSettings {
pub view_distance: u32,
}
/// AudioSettings controls the volume of different audio subsystems and which
/// device is used.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AudioSettings {
pub music_volume: f32,
pub sfx_volume: f32,
/// Audio Device that Voxygen will use to play audio.
pub audio_device: Option<String>,
}
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Settings { Settings {
@ -86,6 +104,12 @@ impl Default for Settings {
log: Log { log: Log {
file: "voxygen.log".into(), file: "voxygen.log".into(),
}, },
graphics: GraphicsSettings { view_distance: 5 },
audio: AudioSettings {
music_volume: 0.5,
sfx_volume: 0.5,
audio_device: None,
},
} }
} }
} }