Revamp AudioFrontend

audio::base had a lot of unnescessary abstractions and constructs.
The current code simplifies the API in a way that makes sense and
that will allow sound effects and fading to be added in an incremental
way.
This commit is contained in:
Louis Pearson 2019-08-31 00:37:09 -06:00
parent 0fe5b66dce
commit 9dc1f8f549
5 changed files with 200 additions and 96 deletions

View File

@ -1,41 +1,173 @@
pub mod base;
use base::{Genre, Jukebox};
use common::assets;
use rodio::{Decoder, Device, Sink, SpatialSink};
const LEFT_EAR : [f32; 3] = [1.0, 0.0, 0.0];
const RIGHT_EAR : [f32; 3] = [-1.0, 0.0, 0.0];
#[derive(PartialEq)]
enum AudioType {
Sfx,
Music,
}
struct Channel {
id: usize,
sink: SpatialSink,
audio_type: AudioType,
}
pub struct AudioFrontend {
pub(crate) model: Jukebox,
pub(crate) default_device: String,
pub(crate) device_list: Vec<String>,
pub device: String,
pub device_list: Vec<String>,
audio_device: Option<Device>,
channels: Vec<Channel>,
next_channel_id: usize,
sfx_volume: f32,
music_volume: f32,
}
impl AudioFrontend {
pub(crate) fn new() -> Self {
/// Construct with given device
pub fn new(device: String) -> Self {
Self {
model: Jukebox::new(Genre::Bgm),
default_device: base::get_default_device(),
device_list: base::list_devices(),
device: device.clone(),
device_list: list_devices(),
audio_device: get_device_raw(device),
channels: Vec::new(),
next_channel_id: 0,
sfx_volume: 1.0,
music_volume: 1.0,
}
}
/// Play audio.
pub(crate) fn play(&mut self) {
let path = base::select_random_music(&Genre::Bgm);
match self.model.player.is_paused() {
true => match self.model.get_genre() {
Genre::Bgm => self.model.player.resume(),
Genre::Sfx => unimplemented!(), // TODO: add support for sound effects.
Genre::None => (),
},
false => self.model.player.load(&path),
/// Construct in `no-audio` mode for debugging
pub fn no_audio() -> Self {
Self {
device: "none".to_string(),
device_list: list_devices(),
audio_device: None,
channels: Vec::new(),
next_channel_id: 0,
sfx_volume: 1.0,
music_volume: 1.0,
}
}
/// Construct in `no-audio` mode for debugging.
pub(crate) fn no_audio() -> Self {
Self {
model: Jukebox::new(Genre::None),
default_device: "None".to_owned(),
device_list: Vec::new(),
/// Maintain audio
pub fn maintain(&mut self) {
let mut stopped_channels = Vec::<usize>::new();
for (i, channel) in self.channels.iter().enumerate() {
if channel.sink.empty() {
stopped_channels.push(i);
}
}
for i in stopped_channels.iter().rev() {
self.channels.remove(*i);
}
}
/// Play specfied sound file.
///```ignore
///audio.play_sound("voxygen.audio.sfx.step");
///```
pub fn play_sound(&mut self, sound: String) -> usize {
let id = self.next_channel_id;
self.next_channel_id += 1;
if let Some(device) = &self.audio_device {
let sink = SpatialSink::new(device, [0.0, 0.0, 0.0], LEFT_EAR, RIGHT_EAR);
let file = assets::load_file(&sound, &["wav", "ogg"]).unwrap();
let sound = rodio::Decoder::new(file).unwrap();
sink.append(sound);
self.channels.push(Channel {
id,
sink,
audio_type: AudioType::Music,
});
}
id
}
pub fn get_sfx_volume(&self) -> f32 {
self.sfx_volume
}
pub fn get_music_volume(&self) -> f32 {
self.music_volume
}
pub fn set_sfx_volume(&mut self, volume: f32) {
self.sfx_volume = volume;
for channel in self.channels.iter() {
if channel.audio_type == AudioType::Sfx {
channel.sink.set_volume(volume);
}
}
}
pub fn set_music_volume(&mut self, volume: f32) {
self.music_volume = volume;
for channel in self.channels.iter() {
if channel.audio_type == AudioType::Music {
channel.sink.set_volume(volume);
}
}
}
pub fn set_device(&mut self, name: String) {
self.device = name.clone();
self.audio_device = get_device_raw(name);
}
}
pub fn select_random_music() -> String {
let soundtracks = load_soundtracks();
let index = rand::random::<usize>() % soundtracks.len();
soundtracks[index].clone()
}
/// 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()
}
/// Load the audio file directory selected by genre.
pub fn load_soundtracks() -> Vec<String> {
let assets = assets::read_dir("voxygen.audio.soundtrack").unwrap();
let soundtracks = assets
.filter_map(|entry| {
entry.ok().map(|f| {
let path = f.path();
path.to_string_lossy().into_owned()
})
})
.collect::<Vec<String>>();
soundtracks
}
/// Returns a vec of the audio devices available.
/// Does not return rodio Device struct in case our audio backend changes.
pub fn list_devices() -> Vec<String> {
list_devices_raw().iter().map(|x| x.name()).collect()
}
/// Returns vec of devices
fn list_devices_raw() -> Vec<Device> {
rodio::output_devices().collect()
}
fn get_device_raw(device: String) -> Option<Device> {
rodio::output_devices().find(|d| d.name() == device)
}

View File

@ -2,7 +2,6 @@ use super::{
img_ids::Imgs, BarNumbers, CrosshairType, Fonts, ShortcutNumbers, Show, XpBar, TEXT_COLOR,
};
use crate::{
audio::base::Genre,
ui::{ImageSlider, ScaleMode, ToggleButton},
GlobalState,
};
@ -1251,39 +1250,26 @@ impl<'a> Widget for SettingsWindow<'a> {
}
// Audio Device Selector --------------------------------------------
match self.global_state.audio.model.get_genre() {
Genre::Bgm => {
let device = &self.global_state.audio.default_device;
let device_list = &self.global_state.audio.device_list;
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);
let device = &self.global_state.audio.device;
let device_list = &self.global_state.audio.device_list;
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));
// 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));
}
}
Genre::Sfx => unimplemented!(),
Genre::None => {
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);
}
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));
}
}

View File

@ -32,7 +32,7 @@ pub mod window;
pub use crate::error::Error;
use crate::{
audio::base::Genre, audio::AudioFrontend, menu::main::MainMenuState, settings::Settings,
audio::AudioFrontend, menu::main::MainMenuState, settings::Settings,
window::Window,
};
use heaptrack::track_mem;
@ -59,8 +59,7 @@ impl GlobalState {
}
pub fn maintain(&mut self) {
// TODO: Maintain both `Bgm` and `Sfx` audio threads.
self.audio.play();
self.audio.maintain();
}
}
@ -104,10 +103,28 @@ lazy_static! {
fn main() {
// Load the settings
let settings = Settings::load();
<<<<<<< HEAD
// Save settings to add new fields or create the file if it is not already there
if let Err(err) = settings.save_to_file() {
panic!("Failed to save settings: {:?}", err);
}
=======
let audio_device = match &settings.audio.audio_device {
Some(d) => d.to_string(),
None => audio::get_default_device(),
};
let audio = if settings.audio.audio_on {
AudioFrontend::new(audio_device)
} else {
AudioFrontend::no_audio()
};
let mut global_state = GlobalState {
audio,
window: Window::new(&settings).expect("Failed to create window!"),
settings,
};
>>>>>>> Revamp AudioFrontend
// Initialize logging.
let term_log_level = std::env::var_os("VOXYGEN_LOG")
@ -186,39 +203,6 @@ fn main() {
default_hook(panic_info);
}));
// Set up the global state.
let audio = if settings.audio.audio_on {
AudioFrontend::new()
} else {
AudioFrontend::no_audio()
};
let mut global_state = GlobalState {
audio,
window: Window::new(&settings).expect("Failed to create window!"),
settings,
};
// Initialize discord. (lazy_static initalise lazily...)
#[cfg(feature = "discord")]
{
match DISCORD_INSTANCE.lock() {
Ok(_disc) => {
//great
}
Err(e) => log::error!("Couldn't init discord: {}", e),
}
}
match global_state.audio.model.get_genre() {
Genre::Bgm => {
global_state.settings.audio.audio_device =
Some(crate::audio::base::get_default_device())
}
Genre::Sfx => unimplemented!(),
Genre::None => global_state.settings.audio.audio_device = None,
}
// Set up the initial play state.
let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(MainMenuState::new(&mut global_state))];
states

View File

@ -34,6 +34,8 @@ impl PlayState for MainMenuState {
// Used for client creation.
let mut client_init: Option<ClientInit> = None;
global_state.audio.play_sound("voxygen.audio.soundtrack.veloren_title_tune-3".to_string());
loop {
// Handle window events.
for event in global_state.window.fetch_events() {

View File

@ -357,13 +357,13 @@ impl PlayState for SessionState {
}
HudEvent::AdjustVolume(volume) => {
global_state.audio.model.player.set_volume(volume);
global_state.audio.set_music_volume(volume);
global_state.settings.audio.music_volume = volume;
global_state.settings.save_to_file_warn();
}
HudEvent::ChangeAudioDevice(name) => {
global_state.audio.model.player.set_device(&name.clone());
global_state.audio.set_device(name.clone());
global_state.settings.audio.audio_device = Some(name);
global_state.settings.save_to_file_warn();