Skip to content

Commit

Permalink
Add simple tests and item issues
Browse files Browse the repository at this point in the history
  • Loading branch information
viluon committed Dec 19, 2022
1 parent 2842f28 commit f1c6df6
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 64 deletions.
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ tracing = "0.1.37"
tracing-subscriber = "0.3.16"
tracing-timing = "0.6.0"

[dev-dependencies]
tempfile = "3.3.0"

[profile.release]
codegen-units = 1
lto = true
Expand Down
99 changes: 58 additions & 41 deletions src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,33 +75,21 @@ fn create_item(tx: Sender<ImportMessage>, id: u64, path: String, name: String) -
let static_sound = match StaticSoundData::from_file(&path, StaticSoundSettings::new()) {
Ok(sound) => sound,
Err(e) => {
let msg = report_import_error(e);
let (msg, _) = classify_from_file_err(&e);
warn!("failed to load {}: {}", path, msg);
tx.send(ImportMessage::Update(id, ItemImportStatus::Failed(msg)))
.unwrap();
return None;
}
};
let duration = static_sound.frames.len() as f64 / static_sound.sample_rate as f64;
let mut i = Item {
let mut i = Item::with_default_stem(
id,
name,
stems: vec![Stem {
tag: "default".to_string(),
path,
}],
current_stem: 0,
volume: 1.0,
muted: false,
looped: false,
status: ItemStatus::Stopped,
colour: PALETTE[id as usize % PALETTE.len()],
bars: vec![],
position: 0.0,
target_position: 0.0,
path,
PALETTE[id as usize % PALETTE.len()],
duration,
issues: vec![],
};
);
i.bars = visualise_samples(&static_sound.frames);
tx.send(ImportMessage::Update(id, ItemImportStatus::Finished))
.unwrap();
Expand Down Expand Up @@ -169,40 +157,69 @@ fn visualise_samples(frames: &[kira::dsp::Frame]) -> Vec<u8> {
.collect()
}

fn report_import_error(e: FromFileError) -> String {
pub fn classify_from_file_err(e: &FromFileError) -> (String, IssueType) {
use std::io::ErrorKind;
use symphonia::core::errors;
use IssueType::*;

match e {
FromFileError::NoDefaultTrack => "the file doesn't have a default track".to_string(),
FromFileError::UnknownSampleRate => "the sample rate could not be determined".to_string(),
FromFileError::UnsupportedChannelConfiguration => {
"the channel configuration of the file is not supported".to_string()
fn describe_io_error(kind: ErrorKind) -> (String, IssueType) {
match kind {
ErrorKind::NotFound => ("the file could not be found".to_string(), MissingFile),
ErrorKind::PermissionDenied => (
"permission to read the file was denied".to_string(),
InaccessibleFile,
),
kind => (format!("an IO error occurred: {}", kind), OtherError),
}
FromFileError::IoError(io_err) => match io_err.kind() {
ErrorKind::NotFound => "the file could not be found".to_string(),
ErrorKind::PermissionDenied => "permission to read the file was denied".to_string(),
kind => format!("an IO error occurred: {}", kind),
},
}

match e {
FromFileError::NoDefaultTrack => (
"the file doesn't have a default track".to_string(),
PlaybackProblem,
),
FromFileError::UnknownSampleRate => (
"the sample rate could not be determined".to_string(),
PlaybackProblem,
),
FromFileError::UnsupportedChannelConfiguration => (
"the channel configuration of the file is not supported".to_string(),
PlaybackProblem,
),
FromFileError::IoError(io_err) => describe_io_error(io_err.kind()),
FromFileError::SymphoniaError(symphonia_err) => match symphonia_err {
errors::Error::IoError(e) => format!("symphonia encountered an I/O error: {}", e),
errors::Error::DecodeError(e) => format!("symphonia could not decode the file: {}", e),
errors::Error::IoError(e) => describe_io_error(e.kind()),
errors::Error::DecodeError(e) => (
format!("symphonia could not decode the file: {}", e),
PlaybackProblem,
),
errors::Error::SeekError(e) => match e {
errors::SeekErrorKind::Unseekable => "this file is not seekable".to_string(),
errors::SeekErrorKind::ForwardOnly => {
"this file can only be seeked forward".to_string()
errors::SeekErrorKind::Unseekable => {
("this file is not seekable".to_string(), PlaybackProblem)
}
errors::SeekErrorKind::OutOfRange => {
"the seek timestamp is out of range".to_string()
errors::SeekErrorKind::ForwardOnly => (
"this file can only be seeked forward".to_string(),
PlaybackProblem,
),
errors::SeekErrorKind::OutOfRange => (
"the seek timestamp is out of range".to_string(),
PlaybackProblem,
),
errors::SeekErrorKind::InvalidTrack => {
("the track ID is invalid".to_string(), PlaybackProblem)
}
errors::SeekErrorKind::InvalidTrack => "the track ID is invalid".to_string(),
},
errors::Error::Unsupported(e) => {
format!("symphonia does not support this format: {}", e)
errors::Error::Unsupported(e) => (
format!("symphonia does not support this format: {}", e),
PlaybackProblem,
),
errors::Error::LimitError(e) => {
(format!("a limit error occurred: {}", e), PlaybackProblem)
}
errors::Error::ResetRequired => {
("symphonia requires a reset".to_string(), PlaybackProblem)
}
errors::Error::LimitError(e) => format!("a limit error occurred: {}", e),
errors::Error::ResetRequired => "symphonia requires a reset".to_string(),
},
_ => "an unknown error occurred".to_string(),
_ => ("an unknown error occurred".to_string(), OtherError),
}
}
54 changes: 54 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use std::sync::Arc;
use tracing::{info, warn, Level};
use tracing_subscriber::FmtSubscriber;

use crate::import::classify_from_file_err;

fn main() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
Expand Down Expand Up @@ -305,6 +307,8 @@ fn begin_playback(
Err(err) => {
edit_item(id, &mut |item| {
item.status = ItemStatus::Stopped;
let (msg, typ) = classify_from_file_err(&err);
item.issues.push((typ, msg));
String::new()
});
return Err(err.into());
Expand All @@ -313,3 +317,53 @@ fn begin_playback(
info!("passing {} to manager", file);
Ok(manager.play(sound)?)
}

#[cfg(test)]
mod test {
use super::*;
use eframe::epaint::Color32;

#[test]
fn file_not_found() -> Result<()> {
// create a temporary directory and try to play a nonexistent file from it
let path = {
let tempdir = tempfile::tempdir()?;
let path = tempdir
.path()
.join("nonexistent.wav")
.to_str()
.unwrap()
.to_string();
tempdir.close()?;
path
};
let model = Model {
items: vec![Item::with_default_stem(
0,
"test".to_string(),
path,
Color32::BLACK,
1.0,
)],
..Model::default()
};
let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let mut handles = HashMap::new();

let msg = ControlMessage::Play(0);

let model = Arc::new(RwLock::new(model));
let (rx, _tx) = channel();
#[allow(unused_must_use)]
{
process_message(msg, &rx, &mut manager, &mut handles, &model);
}

let model = &*model.read();

assert_eq!(model.items[0].status, ItemStatus::Stopped);
assert_eq!(model.items[0].issues.len(), 1);
assert_eq!(model.items[0].issues[0].0, IssueType::MissingFile);
Ok(())
}
}
55 changes: 39 additions & 16 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,16 @@ pub enum ItemStatus {
Paused,
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub enum Issue {
FileNotFound(String),
}
pub type Issue = (IssueType, String);

impl PartialOrd for Issue {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(match (self, other) {
(Issue::FileNotFound(a), Issue::FileNotFound(b)) => a.cmp(b),
})
}
}

impl Ord for Issue {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap()
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Serialize, Deserialize)]
pub enum IssueType {
MissingFile,
InaccessibleFile,
PlaybackProblem,
LicensingIssue,
OtherError,
OtherWarning,
}

#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -102,6 +95,36 @@ pub struct Item {
pub issues: Vec<Issue>,
}

impl Item {
pub fn with_default_stem(
id: u64,
name: String,
path: String,
colour: Color32,
duration: f64,
) -> Item {
Item {
id,
name,
stems: vec![Stem {
tag: "default".to_string(),
path,
}],
current_stem: 0,
volume: 1.0,
muted: false,
looped: false,
status: ItemStatus::Stopped,
colour,
bars: vec![],
position: 0.0,
target_position: 0.0,
duration,
issues: vec![],
}
}
}

#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct Model {
pub search_query: String,
Expand Down
24 changes: 17 additions & 7 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,11 +493,15 @@ impl<'a> UIState<'a> {
let stop_resp = ui.add(
Button::new(RichText::new("⏹").heading().color(Color32::BLACK)).fill(Color32::RED),
);
let search_to_playlist_resp = ui.add(
Button::new(RichText::new("into playlist")),
);

[import_button_resp, play_resp, pause_resp, stop_resp, search_to_playlist_resp]
let search_to_playlist_resp = ui.add(Button::new(RichText::new("into playlist")));

[
import_button_resp,
play_resp,
pause_resp,
stop_resp,
search_to_playlist_resp,
]
}

fn handle_playback_control_buttons(
Expand All @@ -507,7 +511,9 @@ impl<'a> UIState<'a> {
stop_resp: egui::Response,
) {
if let Some(id) = self.model.selected_playlist {
self.channel.send(ControlMessage::PlayFromPlaylist(id)).unwrap();
self.channel
.send(ControlMessage::PlayFromPlaylist(id))
.unwrap();
}
if pause_resp.clicked() {
self.channel.send(ControlMessage::GlobalPause).unwrap();
Expand All @@ -524,7 +530,11 @@ impl<'a> UIState<'a> {
id: self.model.fresh_id(),
name: "new playlist".to_string(),
description: "".to_string(),
items: self.process_search().into_iter().map(|(_, item_id)| item_id).collect(),
items: self
.process_search()
.into_iter()
.map(|(_, item_id)| item_id)
.collect(),
});
}
}
Expand Down

0 comments on commit f1c6df6

Please sign in to comment.