-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
mod sample; | ||
|
||
use std::path::PathBuf; | ||
|
||
use audio_engine::PlayerHandle; | ||
use iced::widget::{column, row, scrollable, slider, text, Space}; | ||
use iced::{Alignment, Command, Length}; | ||
|
||
use crate::screen::main_panel::Entries; | ||
use crate::widget::{waveform_view::WaveformViewer, Element}; | ||
use crate::widget::{Button, Container, Row}; | ||
use crate::{icon, theme}; | ||
|
||
use sample::{SamplePack, SampleResult}; | ||
|
||
const MAX_VOLUME: f32 = 1.25; | ||
const MIN_VOLUME: f32 = 0.0; | ||
|
||
#[derive(Debug, Clone)] | ||
pub enum Message { | ||
Select(usize), | ||
Play, | ||
Pause, | ||
Stop, | ||
SetPlayOnSelection(bool), | ||
SetVolume(f32), | ||
AddEntry(PathBuf), | ||
Loaded(Result<SamplePack, String>), | ||
} | ||
|
||
/// The state of the sample player | ||
pub enum State { | ||
/// Nothing has been loaded | ||
None, | ||
/// Currently loading | ||
Loading, | ||
/// Could not load samples | ||
Failed { path: PathBuf, reason: String }, | ||
/// Successfully loaded samples | ||
Loaded { | ||
path: PathBuf, | ||
module_name: String, | ||
selected: Option<usize>, | ||
samples: SamplePack, | ||
}, | ||
} | ||
|
||
#[derive(Debug, Default)] | ||
pub struct MediaSettings { | ||
pub volume: f32, | ||
pub play_on_selection: bool, | ||
pub enable_looping: bool, | ||
} | ||
|
||
/// Sample player instance | ||
pub struct Instance { | ||
state: State, | ||
player: PlayerHandle, | ||
settings: MediaSettings, | ||
hovered: bool, | ||
progress: Option<f32>, | ||
} | ||
|
||
impl Instance { | ||
pub fn new(player: PlayerHandle, path: PathBuf) -> (Self, Command<Message>) { | ||
let mut instance = Self::new_empty(player); | ||
let command = instance.load_samples(path); | ||
|
||
(instance, command) | ||
} | ||
|
||
pub fn new_empty(player: PlayerHandle) -> Self { | ||
Self { | ||
state: State::None, | ||
player, | ||
settings: MediaSettings::default(), | ||
hovered: false, | ||
progress: None, | ||
} | ||
} | ||
|
||
pub fn settings(mut self, settings: MediaSettings) -> Self { | ||
self.settings = settings; | ||
self | ||
} | ||
|
||
pub fn update(&mut self, entries: &mut Entries, message: Message) -> Command<Message> { | ||
match message { | ||
Message::Select(index) => match &self.state { | ||
State::Loaded { selected, .. } => *selected = Some(index), | ||
_ => (), | ||
}, | ||
Message::Play => todo!(), | ||
Message::Pause => todo!(), | ||
Message::Stop => todo!(), | ||
Message::SetPlayOnSelection(_) => todo!(), | ||
Message::AddEntry(_) => todo!(), | ||
Message::Loaded(_) => todo!(), | ||
Message::SetVolume(_) => todo!(), | ||
} | ||
Command::none() | ||
} | ||
|
||
pub fn load_samples(&mut self, module_path: PathBuf) -> Command<Message> { | ||
let mut load = |path: &PathBuf| { | ||
self.state = State::Loading; | ||
return todo!(); | ||
}; | ||
|
||
match &self.state { | ||
State::None => load(&module_path), | ||
State::Loading => Command::none(), | ||
State::Failed { path, .. } | State::Loaded { path, .. } => match path == &module_path { | ||
true => Command::none(), | ||
false => load(&module_path), | ||
}, | ||
} | ||
} | ||
|
||
pub fn view(&self, entries: &Entries) -> Element<Message> { | ||
todo!() | ||
} | ||
|
||
pub fn title(&self) -> String { | ||
todo!() | ||
} | ||
|
||
/// top left quadrant | ||
fn view_sample_info(&self) -> Element<Message> { | ||
match &self.state { | ||
State::None => todo!(), | ||
State::Loading => todo!(), | ||
State::Failed { reason, .. } => todo!(), | ||
State::Loaded { | ||
selected, samples, .. | ||
} => match selected { | ||
Some(_) => todo!(), | ||
None => todo!(), | ||
}, | ||
} | ||
} | ||
|
||
/// List out the samples | ||
fn view_samples(&self) -> Element<Message> { | ||
match self.state { | ||
State::None => todo!(), | ||
State::Loading => todo!(), | ||
State::Failed { path, reason } => todo!(), | ||
State::Loaded { samples, .. } => { | ||
let samples = samples | ||
.inner() | ||
.iter() | ||
.enumerate() | ||
.map(|(index, result)| result.view_sample(index)) | ||
.collect(); | ||
|
||
let content = column(samples).spacing(10).padding(4); | ||
|
||
scrollable(content).into() | ||
} | ||
} | ||
} | ||
|
||
fn view_waveform(&self) -> Element<Message> { | ||
WaveformViewer::new_maybe(match &self.state { | ||
State::Loaded { | ||
path, | ||
module_name, | ||
selected, | ||
samples, | ||
} => selected.and_then(|index| samples.waveform(index)), | ||
_ => None, | ||
}) | ||
.into() | ||
} | ||
|
||
fn media_buttons(&self) -> Element<Message> { | ||
let media_controls = media_button([ | ||
(icon::play().size(18), Message::Play), | ||
(icon::stop().size(18), Message::Stop), | ||
(icon::pause().size(18), Message::Pause), | ||
// (icon::repeat().size(18), Message::Stop), | ||
]); | ||
|
||
let volume_slider = column![ | ||
text(format!("Volume: {}%", (self.settings.volume * 100.0).round())), | ||
slider(MIN_VOLUME..=MAX_VOLUME, self.settings.volume, Message::SetVolume).step(0.01) | ||
] | ||
.align_items(Alignment::Start); | ||
|
||
Container::new(row![media_controls, volume_slider].spacing(8)) | ||
.padding(8) | ||
.style(theme::Container::Black) | ||
.width(Length::Fill) | ||
.height(Length::Shrink) | ||
.center_x() | ||
.into() | ||
} | ||
} | ||
|
||
|
||
fn media_button<'a, Label, R, Message>(rows: R) -> Element<'a, Message> | ||
where | ||
Message: Clone + 'a, | ||
Label: Into<Element<'a, Message>>, | ||
R: IntoIterator<Item = (Label, Message)>, | ||
{ | ||
let mut media_row: Row<'a, Message> = Row::new().spacing(4.0); | ||
let elements: Vec<(Label, Message)> = rows.into_iter().collect(); | ||
let end_indx = elements.len() - 1; | ||
|
||
for (idx, (label, message)) in elements.into_iter().enumerate() { | ||
let style = if idx == 0 { | ||
theme::Button::MediaStart | ||
} else if idx == end_indx { | ||
theme::Button::MediaEnd | ||
} else { | ||
theme::Button::MediaMiddle | ||
}; | ||
let button = Button::new(label).padding(8.0).on_press(message).style(style); | ||
media_row = media_row.push(button); | ||
} | ||
|
||
media_row.into() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
use std::time::Duration; | ||
|
||
use crate::icon; | ||
use crate::theme; | ||
use crate::widget::helpers::centered_container; | ||
use crate::widget::waveform_view::WaveData; | ||
use crate::widget::{Button, Collection, Container, Element, Row}; | ||
|
||
use audio_engine; | ||
|
||
use iced::widget::{button, horizontal_rule, row, text, Space, column}; | ||
use iced::{Alignment, Length}; | ||
|
||
use super::Message; | ||
|
||
|
||
#[derive(Debug, Clone)] | ||
pub struct SamplePack(Vec<SampleResult>); | ||
|
||
impl SamplePack { | ||
pub fn waveform(&self, index: usize) -> Option<&WaveData> { | ||
self.0.get(index).and_then(SampleResult::waveform) | ||
} | ||
|
||
pub fn inner(&self) -> &[SampleResult] { | ||
&self.0 | ||
} | ||
|
||
pub fn view_sample_info(&self, index: usize) -> Element<Message> { | ||
self.inner()[index].view_sample_info() | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub enum SampleResult { | ||
Invalid(String), | ||
Valid { | ||
metadata: audio_engine::Metadata, | ||
buffer: (), | ||
waveform: WaveData, | ||
}, | ||
} | ||
|
||
impl SampleResult { | ||
pub fn waveform(&self) -> Option<&WaveData> { | ||
match self { | ||
SampleResult::Invalid(_) => None, | ||
SampleResult::Valid { waveform, .. } => Some(waveform), | ||
} | ||
} | ||
|
||
pub fn title(&self) -> String { | ||
match self { | ||
SampleResult::Invalid(_) => todo!(), | ||
SampleResult::Valid { metadata, .. } => todo!(), | ||
} | ||
} | ||
|
||
pub fn is_invalid(&self) -> bool { | ||
matches!(self, Self::Invalid(_)) | ||
} | ||
|
||
pub fn view_sample(&self, index: usize) -> Element<Message> { | ||
let error_icon = || { | ||
row![] | ||
.push(Space::with_width(Length::Fill)) | ||
.push(icon::warning()) | ||
.align_items(iced::Alignment::Center) | ||
}; | ||
|
||
let title = row![] | ||
.push(text(match self.title() { | ||
title if title.is_empty() => format!("{}", index + 1), | ||
title => format!("{} - {}", index + 1, title), | ||
})) | ||
.push_maybe(self.is_invalid().then_some(error_icon())) | ||
.spacing(5); | ||
|
||
let theme = match self.is_invalid() { | ||
true => theme::Button::EntryError, | ||
false => theme::Button::Entry, | ||
}; | ||
|
||
row![ | ||
button(title) | ||
.width(Length::Fill) | ||
.style(theme) | ||
.on_press(Message::Select(index)), | ||
Space::with_width(15) | ||
] | ||
.into() | ||
} | ||
|
||
pub fn view_sample_info(&self) -> Element<Message> { | ||
match self { | ||
SampleResult::Invalid(_) => todo!(), | ||
SampleResult::Valid { metadata, .. } => { | ||
let smp = metadata; | ||
|
||
let sample_name = | ||
(!smp.name.trim().is_empty()).then_some(text(format!("Name: {}", smp.name.trim()))); | ||
|
||
let sample_filename = smp | ||
.filename | ||
.as_ref() | ||
.map(|s| s.trim()) | ||
.and_then(|s| (!s.is_empty()).then_some(text(format!("File Name: {}", s)))); | ||
|
||
let metadata = text(format!( | ||
"{} Hz, {}-bit ({}), {}", | ||
smp.rate, | ||
smp.bits(), | ||
if smp.is_signed() { "Signed" } else { "Unsigned" }, | ||
if smp.is_stereo() { "Stereo" } else { "Mono" }, | ||
)); | ||
|
||
let round_100th = |x: f32| (x * 100.0).round() / 100.0; | ||
|
||
let duration = Duration::from_micros( | ||
((smp.length_frames() as f64 / smp.rate as f64) * 1_000_000.0) as u64, | ||
); | ||
let duration_secs = round_100th(duration.as_secs_f32()); | ||
let plural = if duration_secs == 1.0 { "" } else { "s" }; | ||
let duration = text(format!("Duration: {} sec{plural}", duration_secs)); | ||
|
||
let size = match smp.length { | ||
l if l < 1000 => format!("{} bytes", l), | ||
l if l < 1_000_000 => format!("{} KB", round_100th(l as f32 / 1000.0)), | ||
l => format!("{} MB", round_100th(l as f32 / 1_000_000.0)), | ||
}; | ||
|
||
let info = column![] | ||
.push_maybe(sample_name) | ||
.push_maybe(sample_filename) | ||
.push(duration) | ||
.push(text(format!("Size: {}", size))) | ||
.push(text(format!("Loop type: {:#?}", smp.looping.kind()))) | ||
.push(text(format!("Internal Index: {}", smp.index_raw()))) | ||
.push(horizontal_rule(1)) | ||
.push(metadata) | ||
.push(horizontal_rule(1)) | ||
.spacing(5) | ||
.align_items(Alignment::Center); | ||
centered_container(info).into() | ||
} | ||
} | ||
} | ||
} |