mirror of
https://github.com/tsirysndr/music-player.git
synced 2026-01-08 20:58:07 -05:00
Initial Commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.vscode
|
||||
1514
Cargo.lock
generated
Normal file
1514
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "music-player"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = "3.2.20"
|
||||
cpal = "0.13.0"
|
||||
futures-util = "0.3.24"
|
||||
lazy_static = "1.4.0"
|
||||
librespot-protocol = "0.4.2"
|
||||
log = "0.4.17"
|
||||
parking_lot = "0.12.1"
|
||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||
rand_distr = "0.4.3"
|
||||
rb = "0.4.1"
|
||||
rodio = { version = "0.15" }
|
||||
symphonia = { version = "0.5.1", features = ["aac", "alac", "mp3"] }
|
||||
thiserror = "1.0.34"
|
||||
tokio = { version = "1.21.0", features = ["full"] }
|
||||
zerocopy = "0.6.1"
|
||||
14
README.md
Normal file
14
README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## Music Player (written in Rust)
|
||||
|
||||
<p style="margin-top: 50px; margin-bottom: 50px;">
|
||||
<img src="./cover.svg" height="300" />
|
||||
</p>
|
||||
|
||||
Note: This is a work in progress.
|
||||
|
||||
This is a simple music player that I made for my own use. It is not intended to be a full-featured music player, but rather a simple one that I can use to play music from my local hard drive.
|
||||
|
||||
### Features
|
||||
|
||||
- Play music from local hard drive
|
||||
- Play music from a folder
|
||||
69
cover.svg
Normal file
69
cover.svg
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 331.839 331.839" style="enable-background:new 0 0 331.839 331.839;" xml:space="preserve">
|
||||
<g>
|
||||
<path style="fill:#FFE600;" d="M292.849,115.401l-93.833-45.802c-3.587-1.859-7.869,0.746-7.869,4.785v91.605
|
||||
c0,4.041,4.282,6.646,7.869,4.785l93.833-45.802C296.73,122.961,296.73,117.411,292.849,115.401z"/>
|
||||
<path style="fill:#FFE600;" d="M196.57,178.23c-2.193,0-4.403-0.601-6.383-1.804c-3.673-2.237-5.868-6.137-5.868-10.436V74.383
|
||||
c0-4.301,2.193-8.201,5.868-10.436c3.669-2.231,8.153-2.386,11.967-0.413l93.689,45.734c0.049,0.024,0.101,0.046,0.147,0.073
|
||||
c4.07,2.106,6.596,6.263,6.596,10.846c0,4.582-2.526,8.74-6.596,10.846l-93.979,45.877
|
||||
C200.317,177.789,198.451,178.23,196.57,178.23z M195.927,164.689c-0.017,0.007-0.034,0.017-0.046,0.024L195.927,164.689z
|
||||
M197.973,76.683v87.006l89.12-43.503L197.973,76.683z M196.024,75.729c0.007,0.007,0.014,0.01,0.024,0.014L196.024,75.729z"/>
|
||||
</g>
|
||||
<path style="fill:#FFFFFF;" d="M61.44,128.695v181.982c0,10.182,10.788,16.742,19.828,12.059l236.407-115.398
|
||||
c9.779-5.065,9.779-19.052,0-24.119L81.268,67.822c-9.04-4.683-19.828,1.877-19.828,12.059"/>
|
||||
<path style="fill:#602F75;" d="M75.047,331.12c-3.663,0-7.337-1.004-10.631-3.007c-6.141-3.734-9.803-10.25-9.803-17.437v-181.98
|
||||
c0-3.77,3.057-6.827,6.827-6.827s6.827,3.057,6.827,6.827v181.98c0,3.43,2.27,5.18,3.243,5.774c0.976,0.59,3.574,1.804,6.617,0.227
|
||||
l236.556-115.47c2.137-1.109,3.5-3.354,3.5-5.927s-1.364-4.816-3.647-6.001L78.275,73.956c-3.19-1.637-5.78-0.433-6.764,0.154
|
||||
c-0.973,0.594-3.243,2.343-3.243,5.774c0,3.77-3.057,6.827-6.827,6.827c-3.77,0-6.827-3.057-6.827-6.827
|
||||
c0-7.187,3.662-13.703,9.803-17.437c6.134-3.736,13.604-3.994,19.99-0.683l236.264,115.323c6.943,3.593,11.167,10.537,11.167,18.193
|
||||
c0,7.656-4.224,14.601-11.02,18.12L84.263,328.869C81.37,330.369,78.213,331.12,75.047,331.12z"/>
|
||||
<g>
|
||||
<path style="fill:#EA3457;" d="M217.755,190.494l-93.833-45.802c-3.587-1.859-7.869,0.746-7.869,4.785v91.605
|
||||
c0,4.041,4.282,6.646,7.869,4.785l93.833-45.802C221.636,198.054,221.636,192.504,217.755,190.494z"/>
|
||||
<path style="fill:#EA3457;" d="M121.477,253.323c-2.193,0-4.403-0.601-6.383-1.804c-3.673-2.237-5.868-6.137-5.868-10.436v-91.607
|
||||
c0-4.301,2.193-8.201,5.868-10.436c3.673-2.234,8.146-2.383,11.967-0.413l93.689,45.734c0.05,0.024,0.101,0.046,0.147,0.073
|
||||
c4.07,2.106,6.596,6.263,6.596,10.846s-2.526,8.74-6.596,10.846l-93.979,45.877C125.223,252.883,123.358,253.323,121.477,253.323z
|
||||
M120.834,239.782c-0.017,0.007-0.034,0.017-0.046,0.024L120.834,239.782z M122.88,151.776v87.006L212,195.279L122.88,151.776z
|
||||
M120.931,150.822c0.007,0.007,0.014,0.01,0.024,0.014L120.931,150.822z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#00E7FF;" d="M47.787,48.506c-1.746,0-3.494-0.667-4.826-2L25.894,29.439c-2.666-2.666-2.666-6.987,0-9.653
|
||||
s6.987-2.666,9.653,0l17.067,17.067c2.666,2.666,2.666,6.987,0,9.653C51.28,47.839,49.534,48.506,47.787,48.506z"/>
|
||||
<path style="fill:#00E7FF;" d="M81.92,41.679c-3.77,0-6.827-3.057-6.827-6.827V7.546c0-3.77,3.057-6.827,6.827-6.827
|
||||
c3.77,0,6.827,3.057,6.827,6.827v27.307C88.747,38.623,85.69,41.679,81.92,41.679z"/>
|
||||
<path style="fill:#00E7FF;" d="M34.133,75.813H6.827C3.057,75.813,0,72.756,0,68.986s3.057-6.827,6.827-6.827h27.307
|
||||
c3.77,0,6.827,3.057,6.827,6.827S37.903,75.813,34.133,75.813z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
7
src/addon/datpiff.rs
Normal file
7
src/addon/datpiff.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use super::{Addon, StreamingAddon};
|
||||
|
||||
pub struct DatPiff {}
|
||||
|
||||
impl Addon for DatPiff {}
|
||||
|
||||
impl StreamingAddon for DatPiff {}
|
||||
7
src/addon/deezer.rs
Normal file
7
src/addon/deezer.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use super::{Addon, StreamingAddon};
|
||||
|
||||
pub struct Deezer {}
|
||||
|
||||
impl Addon for Deezer {}
|
||||
|
||||
impl StreamingAddon for Deezer {}
|
||||
7
src/addon/genius.rs
Normal file
7
src/addon/genius.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use super::{Addon, LyricsAddon};
|
||||
|
||||
pub struct Genius {}
|
||||
|
||||
impl Addon for Genius {}
|
||||
|
||||
impl LyricsAddon for Genius {}
|
||||
7
src/addon/local.rs
Normal file
7
src/addon/local.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use super::{Addon, StreamingAddon};
|
||||
|
||||
pub struct Local {}
|
||||
|
||||
impl Addon for Local {}
|
||||
|
||||
impl StreamingAddon for Local {}
|
||||
11
src/addon/mod.rs
Normal file
11
src/addon/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod datpiff;
|
||||
mod deezer;
|
||||
mod genius;
|
||||
mod local;
|
||||
mod tononkira;
|
||||
|
||||
pub trait Addon {}
|
||||
|
||||
pub trait StreamingAddon {}
|
||||
|
||||
pub trait LyricsAddon {}
|
||||
7
src/addon/tononkira.rs
Normal file
7
src/addon/tononkira.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use super::{Addon, LyricsAddon};
|
||||
|
||||
pub struct Tononkira {}
|
||||
|
||||
impl Addon for Tononkira {}
|
||||
|
||||
impl LyricsAddon for Tononkira {}
|
||||
66
src/audio_backend/mod.rs
Normal file
66
src/audio_backend/mod.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{config::AudioFormat, convert::Converter, decoder::AudioPacket};
|
||||
|
||||
use self::rodio::RodioSink;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SinkError {
|
||||
#[error("Audio Sink Error Not Connected: {0}")]
|
||||
NotConnected(String),
|
||||
#[error("Audio Sink Error Connection Refused: {0}")]
|
||||
ConnectionRefused(String),
|
||||
#[error("Audio Sink Error On Write: {0}")]
|
||||
OnWrite(String),
|
||||
#[error("Audio Sink Error Invalid Parameters: {0}")]
|
||||
InvalidParams(String),
|
||||
#[error("Audio Sink Error Changing State: {0}")]
|
||||
StateChange(String),
|
||||
}
|
||||
|
||||
pub type SinkResult<T> = Result<T, SinkError>;
|
||||
|
||||
pub trait Open {
|
||||
fn open(_: Option<String>, format: AudioFormat) -> Self;
|
||||
}
|
||||
|
||||
pub trait Sink {
|
||||
fn start(&mut self) -> SinkResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn stop(&mut self) -> SinkResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write(
|
||||
&mut self,
|
||||
packet: AudioPacket,
|
||||
channels: u16,
|
||||
sample_rate: u32,
|
||||
converter: &mut Converter,
|
||||
) -> SinkResult<()>;
|
||||
}
|
||||
|
||||
pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>;
|
||||
|
||||
fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
|
||||
Box::new(S::open(device, format))
|
||||
}
|
||||
|
||||
pub mod rodio;
|
||||
|
||||
pub mod sdl;
|
||||
|
||||
pub const BACKENDS: &[(&str, SinkBuilder)] = &[
|
||||
(RodioSink::NAME, rodio::mk_rodio), // default goes first
|
||||
];
|
||||
|
||||
pub fn find(name: Option<String>) -> Option<SinkBuilder> {
|
||||
if let Some(name) = name {
|
||||
BACKENDS
|
||||
.iter()
|
||||
.find(|backend| name == backend.0)
|
||||
.map(|backend| backend.1)
|
||||
} else {
|
||||
BACKENDS.first().map(|backend| backend.1)
|
||||
}
|
||||
}
|
||||
222
src/audio_backend/rodio.rs
Normal file
222
src/audio_backend/rodio.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use std::process::exit;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use log::*;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{Sink, SinkError, SinkResult};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
|
||||
pub fn mk_rodio(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
|
||||
Box::new(open(cpal::default_host(), device, format))
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RodioError {
|
||||
#[error("<RodioSink> No Device Available")]
|
||||
NoDeviceAvailable,
|
||||
#[error("<RodioSink> device \"{0}\" is Not Available")]
|
||||
DeviceNotAvailable(String),
|
||||
#[error("<RodioSink> Play Error: {0}")]
|
||||
PlayError(#[from] rodio::PlayError),
|
||||
#[error("<RodioSink> Stream Error: {0}")]
|
||||
StreamError(#[from] rodio::StreamError),
|
||||
#[error("<RodioSink> Cannot Get Audio Devices: {0}")]
|
||||
DevicesError(#[from] cpal::DevicesError),
|
||||
#[error("<RodioSink> {0}")]
|
||||
Samples(String),
|
||||
}
|
||||
|
||||
impl From<RodioError> for SinkError {
|
||||
fn from(e: RodioError) -> SinkError {
|
||||
use RodioError::*;
|
||||
let es = e.to_string();
|
||||
match e {
|
||||
StreamError(_) | PlayError(_) | Samples(_) => SinkError::OnWrite(es),
|
||||
NoDeviceAvailable | DeviceNotAvailable(_) => SinkError::ConnectionRefused(es),
|
||||
DevicesError(_) => SinkError::InvalidParams(es),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RodioSink {
|
||||
rodio_sink: rodio::Sink,
|
||||
format: AudioFormat,
|
||||
_stream: rodio::OutputStream,
|
||||
}
|
||||
|
||||
fn list_formats(device: &rodio::Device) {
|
||||
match device.default_output_config() {
|
||||
Ok(cfg) => {
|
||||
debug!(" Default config:");
|
||||
debug!(" {:?}", cfg);
|
||||
}
|
||||
Err(e) => {
|
||||
// Use loglevel debug, since even the output is only debug
|
||||
debug!("Error getting default rodio::Sink config: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
match device.supported_output_configs() {
|
||||
Ok(mut cfgs) => {
|
||||
if let Some(first) = cfgs.next() {
|
||||
debug!(" Available configs:");
|
||||
debug!(" {:?}", first);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
for cfg in cfgs {
|
||||
debug!(" {:?}", cfg);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Error getting supported rodio::Sink configs: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> {
|
||||
let mut default_device_name = None;
|
||||
|
||||
if let Some(default_device) = host.default_output_device() {
|
||||
default_device_name = default_device.name().ok();
|
||||
println!(
|
||||
"Default Audio Device:\n {}",
|
||||
default_device_name.as_deref().unwrap_or("[unknown name]")
|
||||
);
|
||||
|
||||
list_formats(&default_device);
|
||||
|
||||
println!("Other Available Audio Devices:");
|
||||
} else {
|
||||
warn!("No default device was found");
|
||||
}
|
||||
|
||||
for device in host.output_devices()? {
|
||||
match device.name() {
|
||||
Ok(name) if Some(&name) == default_device_name.as_ref() => (),
|
||||
Ok(name) => {
|
||||
println!(" {}", name);
|
||||
list_formats(&device);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Cannot get device name: {}", e);
|
||||
println!(" [unknown name]");
|
||||
list_formats(&device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_sink(
|
||||
host: &cpal::Host,
|
||||
device: Option<String>,
|
||||
) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> {
|
||||
let rodio_device = match device.as_deref() {
|
||||
Some("?") => match list_outputs(host) {
|
||||
Ok(()) => exit(0),
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
exit(1);
|
||||
}
|
||||
},
|
||||
Some(device_name) => {
|
||||
host.output_devices()?
|
||||
.find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails
|
||||
.ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))?
|
||||
}
|
||||
None => host
|
||||
.default_output_device()
|
||||
.ok_or(RodioError::NoDeviceAvailable)?,
|
||||
};
|
||||
|
||||
let name = rodio_device.name().ok();
|
||||
info!(
|
||||
"Using audio device: {}",
|
||||
name.as_deref().unwrap_or("[unknown name]")
|
||||
);
|
||||
|
||||
let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?;
|
||||
let sink = rodio::Sink::try_new(&handle)?;
|
||||
Ok((sink, stream))
|
||||
}
|
||||
|
||||
pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> RodioSink {
|
||||
info!(
|
||||
"Using Rodio sink with format {:?} and cpal host: {}",
|
||||
format,
|
||||
host.id().name()
|
||||
);
|
||||
|
||||
if format != AudioFormat::S16 && format != AudioFormat::F32 {
|
||||
unimplemented!("Rodio currently only supports F32 and S16 formats");
|
||||
}
|
||||
|
||||
let (sink, stream) = create_sink(&host, device).unwrap();
|
||||
|
||||
debug!("Rodio sink was created");
|
||||
RodioSink {
|
||||
rodio_sink: sink,
|
||||
format,
|
||||
_stream: stream,
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for RodioSink {
|
||||
fn start(&mut self) -> SinkResult<()> {
|
||||
self.rodio_sink.play();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> SinkResult<()> {
|
||||
self.rodio_sink.sleep_until_end();
|
||||
self.rodio_sink.pause();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
packet: AudioPacket,
|
||||
channels: u16,
|
||||
sample_rate: u32,
|
||||
converter: &mut Converter,
|
||||
) -> SinkResult<()> {
|
||||
let samples = packet
|
||||
.samples()
|
||||
.map_err(|e| RodioError::Samples(e.to_string()))?;
|
||||
match self.format {
|
||||
AudioFormat::F32 => {
|
||||
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
||||
let source = rodio::buffer::SamplesBuffer::new(channels, sample_rate, samples_f32);
|
||||
self.rodio_sink.append(source);
|
||||
}
|
||||
AudioFormat::S16 => {
|
||||
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
|
||||
let source = rodio::buffer::SamplesBuffer::new(channels, sample_rate, samples_s16);
|
||||
self.rodio_sink.append(source);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Chunk sizes seem to be about 256 to 3000 ish items long.
|
||||
// Assuming they're on average 1628 then a half second buffer is:
|
||||
// 44100 elements --> about 27 chunks
|
||||
while self.rodio_sink.len() > 26 {
|
||||
// sleep and wait for rodio to drain a bit
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RodioSink {
|
||||
#[allow(dead_code)]
|
||||
pub const NAME: &'static str = "rodio";
|
||||
}
|
||||
0
src/audio_backend/sdl.rs
Normal file
0
src/audio_backend/sdl.rs
Normal file
47
src/config.rs
Normal file
47
src/config.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::mem;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub enum AudioFormat {
|
||||
F64,
|
||||
F32,
|
||||
S32,
|
||||
S24,
|
||||
S24_3,
|
||||
S16,
|
||||
}
|
||||
|
||||
impl FromStr for AudioFormat {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_uppercase().as_ref() {
|
||||
"F64" => Ok(Self::F64),
|
||||
"F32" => Ok(Self::F32),
|
||||
"S32" => Ok(Self::S32),
|
||||
"S24" => Ok(Self::S24),
|
||||
"S24_3" => Ok(Self::S24_3),
|
||||
"S16" => Ok(Self::S16),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AudioFormat {
|
||||
fn default() -> Self {
|
||||
Self::S16
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFormat {
|
||||
// not used by all backends
|
||||
#[allow(dead_code)]
|
||||
pub fn size(&self) -> usize {
|
||||
match self {
|
||||
Self::F64 => mem::size_of::<f64>(),
|
||||
Self::F32 => mem::size_of::<f32>(),
|
||||
Self::S24_3 => mem::size_of::<i64>(),
|
||||
Self::S16 => mem::size_of::<i16>(),
|
||||
_ => mem::size_of::<i32>(), // S32 and S24 are both stored in i32
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/convert.rs
Normal file
121
src/convert.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use crate::dither::{Ditherer, DithererBuilder};
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
#[derive(AsBytes, Copy, Clone, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(transparent)]
|
||||
pub struct i24([u8; 3]);
|
||||
impl i24 {
|
||||
fn from_s24(sample: i32) -> Self {
|
||||
// trim the padding in the most significant byte
|
||||
#[allow(unused_variables)]
|
||||
let [a, b, c, d] = sample.to_ne_bytes();
|
||||
#[cfg(target_endian = "little")]
|
||||
return Self([a, b, c]);
|
||||
#[cfg(target_endian = "big")]
|
||||
return Self([b, c, d]);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Converter {
|
||||
ditherer: Option<Box<dyn Ditherer>>,
|
||||
}
|
||||
|
||||
impl Converter {
|
||||
pub fn new(dither_config: Option<DithererBuilder>) -> Self {
|
||||
match dither_config {
|
||||
Some(ditherer_builder) => {
|
||||
let ditherer = (ditherer_builder)();
|
||||
// info!("Converting with ditherer: {}", ditherer.name());
|
||||
Self {
|
||||
ditherer: Some(ditherer),
|
||||
}
|
||||
}
|
||||
None => Self { ditherer: None },
|
||||
}
|
||||
}
|
||||
|
||||
/// To convert PCM samples from floating point normalized as `-1.0..=1.0`
|
||||
/// to 32-bit signed integer, multiply by 2147483648 (0x80000000) and
|
||||
/// saturate at the bounds of `i32`.
|
||||
const SCALE_S32: f64 = 2147483648.;
|
||||
|
||||
/// To convert PCM samples from floating point normalized as `-1.0..=1.0`
|
||||
/// to 24-bit signed integer, multiply by 8388608 (0x800000) and saturate
|
||||
/// at the bounds of `i24`.
|
||||
const SCALE_S24: f64 = 8388608.;
|
||||
|
||||
/// To convert PCM samples from floating point normalized as `-1.0..=1.0`
|
||||
/// to 16-bit signed integer, multiply by 32768 (0x8000) and saturate at
|
||||
/// the bounds of `i16`. When the samples were encoded using the same
|
||||
/// scaling factor, like the reference Vorbis encoder does, this makes
|
||||
/// conversions transparent.
|
||||
const SCALE_S16: f64 = 32768.;
|
||||
|
||||
pub fn scale(&mut self, sample: f64, factor: f64) -> f64 {
|
||||
// From the many float to int conversion methods available, match what
|
||||
// the reference Vorbis implementation uses: sample * 32768 (for 16 bit)
|
||||
|
||||
// Casting float to integer rounds towards zero by default, i.e. it
|
||||
// truncates, and that generates larger error than rounding to nearest.
|
||||
match self.ditherer.as_mut() {
|
||||
Some(d) => (sample * factor + d.noise()).round(),
|
||||
None => (sample * factor).round(),
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for samples packed in a word of greater bit depth (e.g.
|
||||
// S24): clamp between min and max to ensure that the most significant
|
||||
// byte is zero. Otherwise, dithering may cause an overflow. This is not
|
||||
// necessary for other formats, because casting to integer will saturate
|
||||
// to the bounds of the primitive.
|
||||
pub fn clamping_scale(&mut self, sample: f64, factor: f64) -> f64 {
|
||||
let int_value = self.scale(sample, factor);
|
||||
|
||||
// In two's complement, there are more negative than positive values.
|
||||
let min = -factor;
|
||||
let max = factor - 1.0;
|
||||
|
||||
if int_value < min {
|
||||
min
|
||||
} else if int_value > max {
|
||||
max
|
||||
} else {
|
||||
int_value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec<f32> {
|
||||
samples.iter().map(|sample| *sample as f32).collect()
|
||||
}
|
||||
|
||||
pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec<i32> {
|
||||
samples
|
||||
.iter()
|
||||
.map(|sample| self.scale(*sample, Self::SCALE_S32) as i32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// S24 is 24-bit PCM packed in an upper 32-bit word
|
||||
pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec<i32> {
|
||||
samples
|
||||
.iter()
|
||||
.map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// S24_3 is 24-bit PCM in a 3-byte array
|
||||
pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec<i24> {
|
||||
samples
|
||||
.iter()
|
||||
.map(|sample| i24::from_s24(self.clamping_scale(*sample, Self::SCALE_S24) as i32))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec<i16> {
|
||||
samples
|
||||
.iter()
|
||||
.map(|sample| self.scale(*sample, Self::SCALE_S16) as i16)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
76
src/decoder/mod.rs
Normal file
76
src/decoder/mod.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::ops::Deref;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod symphonia_decoder;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecoderError {
|
||||
#[error("Symphonia Decoder Error: {0}")]
|
||||
SymphoniaDecoder(String),
|
||||
}
|
||||
|
||||
pub type DecoderResult<T> = Result<T, DecoderError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AudioPacketError {
|
||||
#[error("Decoder Raw Error: Can't return Raw on Samples")]
|
||||
Raw,
|
||||
#[error("Decoder Samples Error: Can't return Samples on Raw")]
|
||||
Samples,
|
||||
}
|
||||
|
||||
pub type AudioPacketResult<T> = Result<T, AudioPacketError>;
|
||||
|
||||
pub enum AudioPacket {
|
||||
Samples(Vec<f64>),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
impl AudioPacket {
|
||||
pub fn samples(&self) -> AudioPacketResult<&[f64]> {
|
||||
match self {
|
||||
AudioPacket::Samples(s) => Ok(s),
|
||||
AudioPacket::Raw(_) => Err(AudioPacketError::Raw),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> AudioPacketResult<&[u8]> {
|
||||
match self {
|
||||
AudioPacket::Raw(d) => Ok(d),
|
||||
AudioPacket::Samples(_) => Err(AudioPacketError::Samples),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
AudioPacket::Samples(s) => s.is_empty(),
|
||||
AudioPacket::Raw(d) => d.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioPacketPosition {
|
||||
pub position_ms: u32,
|
||||
pub skipped: bool,
|
||||
}
|
||||
|
||||
impl Deref for AudioPacketPosition {
|
||||
type Target = u32;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.position_ms
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AudioDecoder {
|
||||
fn seek(&mut self, position_ms: u32) -> Result<u32, DecoderError>;
|
||||
fn next_packet(
|
||||
&mut self,
|
||||
) -> DecoderResult<Option<(AudioPacketPosition, AudioPacket, u16, u32)>>;
|
||||
}
|
||||
|
||||
impl From<symphonia::core::errors::Error> for DecoderError {
|
||||
fn from(err: symphonia::core::errors::Error) -> Self {
|
||||
Self::SymphoniaDecoder(err.to_string())
|
||||
}
|
||||
}
|
||||
215
src/decoder/symphonia_decoder.rs
Normal file
215
src/decoder/symphonia_decoder.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use std::io;
|
||||
|
||||
use log::warn;
|
||||
use symphonia::core::{
|
||||
audio::SampleBuffer,
|
||||
codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL},
|
||||
errors::Error,
|
||||
formats::{FormatOptions, FormatReader, SeekMode, SeekTo, Track},
|
||||
io::{MediaSource, MediaSourceStream},
|
||||
meta::{MetadataOptions, Visual},
|
||||
probe::{Hint, ProbeResult},
|
||||
units::{Time, TimeBase},
|
||||
};
|
||||
|
||||
use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult};
|
||||
|
||||
use crate::PAGES_PER_MS;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct PlayTrackOptions {
|
||||
track_id: u32,
|
||||
seek_ts: u64,
|
||||
}
|
||||
|
||||
fn first_supported_track(tracks: &[Track]) -> Option<&Track> {
|
||||
tracks
|
||||
.iter()
|
||||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
}
|
||||
|
||||
pub struct SymphoniaDecoder {
|
||||
format: Box<dyn FormatReader>,
|
||||
decoder: Box<dyn Decoder>,
|
||||
sample_buffer: Option<SampleBuffer<f64>>,
|
||||
}
|
||||
|
||||
impl SymphoniaDecoder {
|
||||
pub fn new<R>(input: R, hint: Hint) -> DecoderResult<Self>
|
||||
where
|
||||
R: MediaSource + 'static,
|
||||
{
|
||||
// Create the media source stream using the boxed media source from above.
|
||||
let mss = MediaSourceStream::new(Box::new(input), Default::default());
|
||||
|
||||
// Use the default options for format readers other than for gapless playback.
|
||||
let format_opts = FormatOptions {
|
||||
enable_gapless: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Use the default options for metadata readers.
|
||||
let metadata_opts: MetadataOptions = Default::default();
|
||||
|
||||
let track: Option<usize> = None;
|
||||
|
||||
// Probe the media source stream for metadata and get the format reader.
|
||||
match symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts) {
|
||||
Ok(probed) => {
|
||||
// Playback mode.
|
||||
// print_format(song, &mut probed);
|
||||
|
||||
// Set the decoder options.
|
||||
let decode_opts = DecoderOptions {
|
||||
verify: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Play it!
|
||||
// play(probed.format, track, seek_time, &decode_opts, no_progress);
|
||||
|
||||
// If the user provided a track number, select that track if it exists, otherwise, select the
|
||||
// first track with a known codec.
|
||||
let track = track
|
||||
.and_then(|t| probed.format.tracks().get(t))
|
||||
.or_else(|| first_supported_track(probed.format.tracks()));
|
||||
|
||||
let track_id = match track {
|
||||
Some(track) => track.id,
|
||||
_ => {
|
||||
return Err(DecoderError::SymphoniaDecoder(
|
||||
"No supported tracks found".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let seek_ts = 0;
|
||||
|
||||
let track_info = PlayTrackOptions { track_id, seek_ts };
|
||||
|
||||
// Get the selected track using the track ID.
|
||||
let track = match probed
|
||||
.format
|
||||
.tracks()
|
||||
.iter()
|
||||
.find(|track| track.id == track_info.track_id)
|
||||
{
|
||||
Some(track) => track,
|
||||
_ => {
|
||||
return Err(DecoderError::SymphoniaDecoder(
|
||||
"No supported tracks found".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Create a decoder for the track.
|
||||
let decoder =
|
||||
symphonia::default::get_codecs().make(&track.codec_params, &decode_opts)?;
|
||||
return Ok(SymphoniaDecoder {
|
||||
format: probed.format,
|
||||
decoder,
|
||||
sample_buffer: None,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
// The input was not supported by any format reader.
|
||||
panic!("file not supported. reason? {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ts_to_ms(&self, ts: u64) -> u32 {
|
||||
let time_base = self.decoder.codec_params().time_base;
|
||||
let seeked_to_ms = match time_base {
|
||||
Some(time_base) => {
|
||||
let time = time_base.calc_time(ts);
|
||||
(time.seconds as f64 + time.frac) * 1000.
|
||||
}
|
||||
// Fallback in the unexpected case that the format has no base time set.
|
||||
None => ts as f64 * PAGES_PER_MS,
|
||||
};
|
||||
seeked_to_ms as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioDecoder for SymphoniaDecoder {
|
||||
fn seek(&mut self, position_ms: u32) -> Result<u32, DecoderError> {
|
||||
let seconds = position_ms as u64 / 1000;
|
||||
let frac = (position_ms as f64 % 1000.) / 1000.;
|
||||
let time = Time::new(seconds, frac);
|
||||
|
||||
// `track_id: None` implies the default track ID (of the container, not of Spotify).
|
||||
let seeked_to_ts = self.format.seek(
|
||||
SeekMode::Accurate,
|
||||
SeekTo::Time {
|
||||
time,
|
||||
track_id: None,
|
||||
},
|
||||
)?;
|
||||
|
||||
// Seeking is a `FormatReader` operation, so the decoder cannot reliably
|
||||
// know when a seek took place. Reset it to avoid audio glitches.
|
||||
self.decoder.reset();
|
||||
|
||||
Ok(self.ts_to_ms(seeked_to_ts.actual_ts))
|
||||
}
|
||||
|
||||
fn next_packet(
|
||||
&mut self,
|
||||
) -> DecoderResult<Option<(AudioPacketPosition, AudioPacket, u16, u32)>> {
|
||||
let mut skipped = false;
|
||||
|
||||
loop {
|
||||
let packet = match self.format.next_packet() {
|
||||
Ok(packet) => packet,
|
||||
Err(Error::IoError(err)) => {
|
||||
if err.kind() == io::ErrorKind::UnexpectedEof {
|
||||
return Ok(None);
|
||||
} else {
|
||||
return Err(DecoderError::SymphoniaDecoder(err.to_string()));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
let position_ms = self.ts_to_ms(packet.ts());
|
||||
let packet_position = AudioPacketPosition {
|
||||
position_ms,
|
||||
skipped,
|
||||
};
|
||||
|
||||
match self.decoder.decode(&packet) {
|
||||
Ok(decoded) => {
|
||||
let spec = *decoded.spec();
|
||||
let sample_buffer = match self.sample_buffer.as_mut() {
|
||||
Some(buffer) => buffer,
|
||||
None => {
|
||||
let duration = decoded.capacity() as u64;
|
||||
self.sample_buffer.insert(SampleBuffer::new(duration, spec))
|
||||
}
|
||||
};
|
||||
|
||||
sample_buffer.copy_interleaved_ref(decoded);
|
||||
let samples = AudioPacket::Samples(sample_buffer.samples().to_vec());
|
||||
|
||||
return Ok(Some((
|
||||
packet_position,
|
||||
samples,
|
||||
spec.channels.count() as u16,
|
||||
spec.rate,
|
||||
)));
|
||||
}
|
||||
Err(Error::DecodeError(_)) => {
|
||||
// The packet failed to decode due to corrupted or invalid data, get a new
|
||||
// packet and try again.
|
||||
warn!("Skipping malformed audio packet at {} ms", position_ms);
|
||||
skipped = true;
|
||||
continue;
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
150
src/dither.rs
Normal file
150
src/dither.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::SeedableRng;
|
||||
use rand_distr::{Distribution, Normal, Triangular, Uniform};
|
||||
use std::fmt;
|
||||
|
||||
use crate::NUM_CHANNELS;
|
||||
|
||||
// Dithering lowers digital-to-analog conversion ("requantization") error,
|
||||
// linearizing output, lowering distortion and replacing it with a constant,
|
||||
// fixed noise level, which is more pleasant to the ear than the distortion.
|
||||
//
|
||||
// Guidance:
|
||||
//
|
||||
// * On S24, S24_3 and S24, the default is to use triangular dithering.
|
||||
// Depending on personal preference you may use Gaussian dithering instead;
|
||||
// it's not as good objectively, but it may be preferred subjectively if
|
||||
// you are looking for a more "analog" sound akin to tape hiss.
|
||||
//
|
||||
// * Advanced users who know that they have a DAC without noise shaping have
|
||||
// a third option: high-passed dithering, which is like triangular dithering
|
||||
// except that it moves dithering noise up in frequency where it is less
|
||||
// audible. Note: 99% of DACs are of delta-sigma design with noise shaping,
|
||||
// so unless you have a multibit / R2R DAC, or otherwise know what you are
|
||||
// doing, this is not for you.
|
||||
//
|
||||
// * Don't dither or shape noise on S32 or F32. On F32 it's not supported
|
||||
// anyway (there are no integer conversions and so no rounding errors) and
|
||||
// on S32 the noise level is so far down that it is simply inaudible even
|
||||
// after volume normalisation and control.
|
||||
//
|
||||
pub trait Ditherer {
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
fn name(&self) -> &'static str;
|
||||
fn noise(&mut self) -> f64;
|
||||
}
|
||||
|
||||
impl fmt::Display for dyn Ditherer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.name())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_rng() -> SmallRng {
|
||||
SmallRng::from_entropy()
|
||||
}
|
||||
|
||||
pub struct TriangularDitherer {
|
||||
cached_rng: SmallRng,
|
||||
distribution: Triangular<f64>,
|
||||
}
|
||||
|
||||
impl Ditherer for TriangularDitherer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cached_rng: create_rng(),
|
||||
// 2 LSB peak-to-peak needed to linearize the response:
|
||||
distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn noise(&mut self) -> f64 {
|
||||
self.distribution.sample(&mut self.cached_rng)
|
||||
}
|
||||
}
|
||||
|
||||
impl TriangularDitherer {
|
||||
pub const NAME: &'static str = "tpdf";
|
||||
}
|
||||
|
||||
pub struct GaussianDitherer {
|
||||
cached_rng: SmallRng,
|
||||
distribution: Normal<f64>,
|
||||
}
|
||||
|
||||
impl Ditherer for GaussianDitherer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cached_rng: create_rng(),
|
||||
// 1/2 LSB RMS needed to linearize the response:
|
||||
distribution: Normal::new(0.0, 0.5).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn noise(&mut self) -> f64 {
|
||||
self.distribution.sample(&mut self.cached_rng)
|
||||
}
|
||||
}
|
||||
|
||||
impl GaussianDitherer {
|
||||
pub const NAME: &'static str = "gpdf";
|
||||
}
|
||||
|
||||
pub struct HighPassDitherer {
|
||||
active_channel: usize,
|
||||
previous_noises: [f64; NUM_CHANNELS as usize],
|
||||
cached_rng: SmallRng,
|
||||
distribution: Uniform<f64>,
|
||||
}
|
||||
|
||||
impl Ditherer for HighPassDitherer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
active_channel: 0,
|
||||
previous_noises: [0.0; NUM_CHANNELS as usize],
|
||||
cached_rng: create_rng(),
|
||||
distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn noise(&mut self) -> f64 {
|
||||
let new_noise = self.distribution.sample(&mut self.cached_rng);
|
||||
let high_passed_noise = new_noise - self.previous_noises[self.active_channel];
|
||||
self.previous_noises[self.active_channel] = new_noise;
|
||||
self.active_channel ^= 1;
|
||||
high_passed_noise
|
||||
}
|
||||
}
|
||||
|
||||
impl HighPassDitherer {
|
||||
pub const NAME: &'static str = "tpdf_hp";
|
||||
}
|
||||
|
||||
pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
|
||||
Box::new(D::new())
|
||||
}
|
||||
|
||||
pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
|
||||
|
||||
pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
|
||||
match name.as_deref() {
|
||||
Some(TriangularDitherer::NAME) => Some(mk_ditherer::<TriangularDitherer>),
|
||||
Some(GaussianDitherer::NAME) => Some(mk_ditherer::<GaussianDitherer>),
|
||||
Some(HighPassDitherer::NAME) => Some(mk_ditherer::<HighPassDitherer>),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
301
src/formatter.rs
Normal file
301
src/formatter.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
use symphonia::core::formats::{Cue, FormatOptions, Track};
|
||||
use symphonia::core::io::MediaSourceStream;
|
||||
use symphonia::core::meta::{ColorMode, MetadataOptions, MetadataRevision, Tag, Value, Visual};
|
||||
use symphonia::core::probe::Hint;
|
||||
use symphonia::core::units::TimeBase;
|
||||
|
||||
use log::info;
|
||||
|
||||
pub fn print_format(path: &str) {
|
||||
let mut hint = Hint::new();
|
||||
|
||||
let source = Box::new(File::open(Path::new(path)).unwrap());
|
||||
|
||||
// Provide the file extension as a hint.
|
||||
if let Some(extension) = Path::new(path).extension() {
|
||||
if let Some(extension_str) = extension.to_str() {
|
||||
hint.with_extension(extension_str);
|
||||
}
|
||||
}
|
||||
let mss = MediaSourceStream::new(source, Default::default());
|
||||
|
||||
let format_opts = FormatOptions {
|
||||
enable_gapless: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let metadata_opts: MetadataOptions = Default::default();
|
||||
|
||||
let mut probed = symphonia::default::get_probe()
|
||||
.format(&hint, mss, &format_opts, &metadata_opts)
|
||||
.unwrap();
|
||||
|
||||
println!("+ {}", path);
|
||||
print_tracks(probed.format.tracks());
|
||||
|
||||
// Prefer metadata that's provided in the container format, over other tags found during the
|
||||
// probe operation.
|
||||
if let Some(metadata_rev) = probed.format.metadata().current() {
|
||||
print_tags(metadata_rev.tags());
|
||||
print_visuals(metadata_rev.visuals());
|
||||
|
||||
// Warn that certain tags are preferred.
|
||||
if probed.metadata.get().as_ref().is_some() {
|
||||
info!("tags that are part of the container format are preferentially printed.");
|
||||
info!("not printing additional tags that were found while probing.");
|
||||
}
|
||||
} else if let Some(metadata_rev) = probed.metadata.get().as_ref().and_then(|m| m.current()) {
|
||||
print_tags(metadata_rev.tags());
|
||||
print_visuals(metadata_rev.visuals());
|
||||
}
|
||||
|
||||
print_cues(probed.format.cues());
|
||||
println!(":");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_update(rev: &MetadataRevision) {
|
||||
print_tags(rev.tags());
|
||||
print_visuals(rev.visuals());
|
||||
println!(":");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_tracks(tracks: &[Track]) {
|
||||
if !tracks.is_empty() {
|
||||
println!("|");
|
||||
println!("| // Tracks //");
|
||||
|
||||
for (idx, track) in tracks.iter().enumerate() {
|
||||
let params = &track.codec_params;
|
||||
|
||||
print!("| [{:0>2}] Codec: ", idx + 1);
|
||||
|
||||
if let Some(codec) = symphonia::default::get_codecs().get_codec(params.codec) {
|
||||
println!("{} ({})", codec.long_name, codec.short_name);
|
||||
} else {
|
||||
println!("Unknown (#{})", params.codec);
|
||||
}
|
||||
|
||||
if let Some(sample_rate) = params.sample_rate {
|
||||
println!("| Sample Rate: {}", sample_rate);
|
||||
}
|
||||
if params.start_ts > 0 {
|
||||
if let Some(tb) = params.time_base {
|
||||
println!(
|
||||
"| Start Time: {} ({})",
|
||||
fmt_time(params.start_ts, tb),
|
||||
params.start_ts
|
||||
);
|
||||
} else {
|
||||
println!("| Start Time: {}", params.start_ts);
|
||||
}
|
||||
}
|
||||
if let Some(n_frames) = params.n_frames {
|
||||
if let Some(tb) = params.time_base {
|
||||
println!(
|
||||
"| Duration: {} ({})",
|
||||
fmt_time(n_frames, tb),
|
||||
n_frames
|
||||
);
|
||||
} else {
|
||||
println!("| Frames: {}", n_frames);
|
||||
}
|
||||
}
|
||||
if let Some(tb) = params.time_base {
|
||||
println!("| Time Base: {}", tb);
|
||||
}
|
||||
if let Some(padding) = params.delay {
|
||||
println!("| Encoder Delay: {}", padding);
|
||||
}
|
||||
if let Some(padding) = params.padding {
|
||||
println!("| Encoder Padding: {}", padding);
|
||||
}
|
||||
if let Some(sample_format) = params.sample_format {
|
||||
println!("| Sample Format: {:?}", sample_format);
|
||||
}
|
||||
if let Some(bits_per_sample) = params.bits_per_sample {
|
||||
println!("| Bits per Sample: {}", bits_per_sample);
|
||||
}
|
||||
if let Some(channels) = params.channels {
|
||||
println!("| Channel(s): {}", channels.count());
|
||||
println!("| Channel Map: {}", channels);
|
||||
}
|
||||
if let Some(channel_layout) = params.channel_layout {
|
||||
println!("| Channel Layout: {:?}", channel_layout);
|
||||
}
|
||||
if let Some(language) = &track.language {
|
||||
println!("| Language: {}", language);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_cues(cues: &[Cue]) {
|
||||
if !cues.is_empty() {
|
||||
println!("|");
|
||||
println!("| // Cues //");
|
||||
|
||||
for (idx, cue) in cues.iter().enumerate() {
|
||||
println!("| [{:0>2}] Track: {}", idx + 1, cue.index);
|
||||
println!("| Timestamp: {}", cue.start_ts);
|
||||
|
||||
// Print tags associated with the Cue.
|
||||
if !cue.tags.is_empty() {
|
||||
println!("| Tags:");
|
||||
|
||||
for (tidx, tag) in cue.tags.iter().enumerate() {
|
||||
if let Some(std_key) = tag.std_key {
|
||||
println!(
|
||||
"{}",
|
||||
print_tag_item(tidx + 1, &format!("{:?}", std_key), &tag.value, 21)
|
||||
);
|
||||
} else {
|
||||
println!("{}", print_tag_item(tidx + 1, &tag.key, &tag.value, 21));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print any sub-cues.
|
||||
if !cue.points.is_empty() {
|
||||
println!("| Sub-Cues:");
|
||||
|
||||
for (ptidx, pt) in cue.points.iter().enumerate() {
|
||||
println!(
|
||||
"| [{:0>2}] Offset: {:?}",
|
||||
ptidx + 1,
|
||||
pt.start_offset_ts
|
||||
);
|
||||
|
||||
// Start the number of sub-cue tags, but don't print them.
|
||||
if !pt.tags.is_empty() {
|
||||
println!(
|
||||
"| Sub-Tags: {} (not listed)",
|
||||
pt.tags.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tags(tags: &[Tag]) {
|
||||
if !tags.is_empty() {
|
||||
println!("|");
|
||||
println!("| // Tags //");
|
||||
|
||||
let mut idx = 1;
|
||||
|
||||
// Print tags with a standard tag key first, these are the most common tags.
|
||||
for tag in tags.iter().filter(|tag| tag.is_known()) {
|
||||
if let Some(std_key) = tag.std_key {
|
||||
println!(
|
||||
"{}",
|
||||
print_tag_item(idx, &format!("{:?}", std_key), &tag.value, 4)
|
||||
);
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// Print the remaining tags with keys truncated to 26 characters.
|
||||
for tag in tags.iter().filter(|tag| !tag.is_known()) {
|
||||
println!("{}", print_tag_item(idx, &tag.key, &tag.value, 4));
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_visuals(visuals: &[Visual]) {
|
||||
if !visuals.is_empty() {
|
||||
println!("|");
|
||||
println!("| // Visuals //");
|
||||
|
||||
for (idx, visual) in visuals.iter().enumerate() {
|
||||
if let Some(usage) = visual.usage {
|
||||
println!("| [{:0>2}] Usage: {:?}", idx + 1, usage);
|
||||
println!("| Media Type: {}", visual.media_type);
|
||||
} else {
|
||||
println!("| [{:0>2}] Media Type: {}", idx + 1, visual.media_type);
|
||||
}
|
||||
if let Some(dimensions) = visual.dimensions {
|
||||
println!(
|
||||
"| Dimensions: {} px x {} px",
|
||||
dimensions.width, dimensions.height
|
||||
);
|
||||
}
|
||||
if let Some(bpp) = visual.bits_per_pixel {
|
||||
println!("| Bits/Pixel: {}", bpp);
|
||||
}
|
||||
if let Some(ColorMode::Indexed(colors)) = visual.color_mode {
|
||||
println!("| Palette: {} colors", colors);
|
||||
}
|
||||
println!("| Size: {} bytes", visual.data.len());
|
||||
|
||||
// Print out tags similar to how regular tags are printed.
|
||||
if !visual.tags.is_empty() {
|
||||
println!("| Tags:");
|
||||
}
|
||||
|
||||
for (tidx, tag) in visual.tags.iter().enumerate() {
|
||||
if let Some(std_key) = tag.std_key {
|
||||
println!(
|
||||
"{}",
|
||||
print_tag_item(tidx + 1, &format!("{:?}", std_key), &tag.value, 21)
|
||||
);
|
||||
} else {
|
||||
println!("{}", print_tag_item(tidx + 1, &tag.key, &tag.value, 21));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tag_item(idx: usize, key: &str, value: &Value, indent: usize) -> String {
|
||||
let key_str = match key.len() {
|
||||
0..=28 => format!("| {:w$}[{:0>2}] {:<28} : ", "", idx, key, w = indent),
|
||||
_ => format!(
|
||||
"| {:w$}[{:0>2}] {:.<28} : ",
|
||||
"",
|
||||
idx,
|
||||
key.split_at(26).0,
|
||||
w = indent
|
||||
),
|
||||
};
|
||||
|
||||
let line_prefix = format!("\n| {:w$} : ", "", w = indent + 4 + 28 + 1);
|
||||
let line_wrap_prefix = format!("\n| {:w$} ", "", w = indent + 4 + 28 + 1);
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
out.push_str(&key_str);
|
||||
|
||||
for (wrapped, line) in value.to_string().lines().enumerate() {
|
||||
if wrapped > 0 {
|
||||
out.push_str(&line_prefix);
|
||||
}
|
||||
|
||||
let mut chars = line.chars();
|
||||
let split = (0..)
|
||||
.map(|_| chars.by_ref().take(72).collect::<String>())
|
||||
.take_while(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
out.push_str(&split.join(&line_wrap_prefix));
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn fmt_time(ts: u64, tb: TimeBase) -> String {
|
||||
let time = tb.calc_time(ts);
|
||||
|
||||
let hours = time.seconds / (60 * 60);
|
||||
let mins = (time.seconds % (60 * 60)) / 60;
|
||||
let secs = f64::from((time.seconds % 60) as u32) + time.frac;
|
||||
|
||||
format!("{}:{:0>2}:{:0>6.3}", hours, mins, secs)
|
||||
}
|
||||
14
src/lib.rs
Normal file
14
src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub mod addon;
|
||||
pub mod audio_backend;
|
||||
pub mod config;
|
||||
pub mod convert;
|
||||
pub mod decoder;
|
||||
pub mod dither;
|
||||
pub mod metadata;
|
||||
pub mod player;
|
||||
|
||||
pub const SAMPLE_RATE: u32 = 44100;
|
||||
pub const NUM_CHANNELS: u8 = 2;
|
||||
pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32;
|
||||
pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0;
|
||||
pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64;
|
||||
109
src/main.rs
Normal file
109
src/main.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::fs::File;
|
||||
|
||||
use clap::Command;
|
||||
use music_player::{
|
||||
audio_backend::{self, rodio::RodioSink},
|
||||
config::AudioFormat,
|
||||
convert::Converter,
|
||||
decoder::{symphonia_decoder::SymphoniaDecoder, AudioDecoder},
|
||||
dither::{mk_ditherer, TriangularDitherer},
|
||||
};
|
||||
use std::path::Path;
|
||||
use symphonia::core::io::MediaSourceStream;
|
||||
use symphonia::core::probe::Hint;
|
||||
mod formatter;
|
||||
mod output;
|
||||
|
||||
use log::error;
|
||||
|
||||
type Decoder = Box<dyn AudioDecoder + Send>;
|
||||
|
||||
fn cli() -> Command<'static> {
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
Command::new("music-player")
|
||||
.version(VERSION)
|
||||
.author("Tsiry Sandratraina <tsiry.sndr@aol.com>")
|
||||
.about("A simple music player written in Rust")
|
||||
.subcommand(
|
||||
Command::new("play")
|
||||
.about("Play a song")
|
||||
.arg_from_usage("<song> 'The path to the song'"),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let matches = cli().get_matches();
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("play") {
|
||||
let song = matches.value_of("song").unwrap();
|
||||
|
||||
formatter::print_format(song);
|
||||
|
||||
let audio_format = AudioFormat::default();
|
||||
let backend = audio_backend::find(Some(RodioSink::NAME.to_string())).unwrap();
|
||||
|
||||
// Create a hint to help the format registry guess what format reader is appropriate.
|
||||
let mut hint = Hint::new();
|
||||
|
||||
let path = Path::new(song);
|
||||
|
||||
// Provide the file extension as a hint.
|
||||
if let Some(extension) = path.extension() {
|
||||
if let Some(extension_str) = extension.to_str() {
|
||||
hint.with_extension(extension_str);
|
||||
}
|
||||
}
|
||||
|
||||
let source = Box::new(File::open(path).unwrap());
|
||||
|
||||
// Create the media source stream using the boxed media source from above.
|
||||
let mss = MediaSourceStream::new(source, Default::default());
|
||||
|
||||
let symphonia_decoder = |mss: MediaSourceStream, hint| {
|
||||
SymphoniaDecoder::new(mss, hint).map(|mut decoder| {
|
||||
// For formats other that Vorbis, we'll try getting normalisation data from
|
||||
// ReplayGain metadata fields, if present.
|
||||
Box::new(decoder) as Decoder
|
||||
})
|
||||
};
|
||||
|
||||
let decoder_type = symphonia_decoder(mss, hint);
|
||||
|
||||
let mut decoder = match decoder_type {
|
||||
Ok(decoder) => decoder,
|
||||
Err(e) => {
|
||||
error!("Failed to create decoder: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut sink = backend(None, audio_format);
|
||||
|
||||
sink.start();
|
||||
|
||||
loop {
|
||||
match decoder.next_packet() {
|
||||
Ok(result) => {
|
||||
if let Some((ref _packet_position, packet, channels, sample_rate)) = result {
|
||||
match packet.samples() {
|
||||
Ok(_) => {
|
||||
// println!("packet_position: {:?}", packet_position);
|
||||
// println!("packet: {:?}", packet.samples());
|
||||
let mut converter =
|
||||
Converter::new(Some(mk_ditherer::<TriangularDitherer>));
|
||||
sink.write(packet, channels, sample_rate, &mut converter);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to decode packet: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to decode packet: {}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/metadata/audio/file.rs
Normal file
1
src/metadata/audio/file.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub use librespot_protocol::metadata::AudioFile_Format as AudioFileFormat;
|
||||
3
src/metadata/audio/mod.rs
Normal file
3
src/metadata/audio/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod file;
|
||||
|
||||
pub use file::AudioFileFormat;
|
||||
1
src/metadata/mod.rs
Normal file
1
src/metadata/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod audio;
|
||||
180
src/output.rs
Normal file
180
src/output.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use std::result;
|
||||
|
||||
use symphonia::core::audio::{AudioBufferRef, SignalSpec};
|
||||
use symphonia::core::units::Duration;
|
||||
|
||||
pub trait AudioOutput {
|
||||
fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()>;
|
||||
fn flush(&mut self);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug)]
|
||||
pub enum AudioOutputError {
|
||||
OpenStreamError,
|
||||
PlayStreamError,
|
||||
StreamClosedError,
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, AudioOutputError>;
|
||||
|
||||
mod cpal {
|
||||
use super::{AudioOutput, AudioOutputError, Result};
|
||||
|
||||
use symphonia::core::audio::{AudioBufferRef, RawSample, SampleBuffer, SignalSpec};
|
||||
use symphonia::core::conv::ConvertibleSample;
|
||||
use symphonia::core::units::Duration;
|
||||
|
||||
use cpal;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use rb::*;
|
||||
|
||||
use log::error;
|
||||
|
||||
pub struct CpalAudioOutput;
|
||||
|
||||
trait AudioOutputSample:
|
||||
cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static
|
||||
{
|
||||
}
|
||||
|
||||
impl AudioOutputSample for f32 {}
|
||||
impl AudioOutputSample for i16 {}
|
||||
impl AudioOutputSample for u16 {}
|
||||
|
||||
impl CpalAudioOutput {
|
||||
pub fn try_open(spec: SignalSpec, duration: Duration) -> Result<Box<dyn AudioOutput>> {
|
||||
// Get default host.
|
||||
let host = cpal::default_host();
|
||||
|
||||
// Get the default audio output device.
|
||||
let device = match host.default_output_device() {
|
||||
Some(device) => device,
|
||||
_ => {
|
||||
error!("failed to get default audio output device");
|
||||
return Err(AudioOutputError::OpenStreamError);
|
||||
}
|
||||
};
|
||||
|
||||
let config = match device.default_output_config() {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error!("failed to get default audio output device config: {}", err);
|
||||
return Err(AudioOutputError::OpenStreamError);
|
||||
}
|
||||
};
|
||||
|
||||
// Select proper playback routine based on sample format.
|
||||
match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => {
|
||||
CpalAudioOutputImpl::<f32>::try_open(spec, duration, &device)
|
||||
}
|
||||
cpal::SampleFormat::I16 => {
|
||||
CpalAudioOutputImpl::<i16>::try_open(spec, duration, &device)
|
||||
}
|
||||
cpal::SampleFormat::U16 => {
|
||||
CpalAudioOutputImpl::<u16>::try_open(spec, duration, &device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CpalAudioOutputImpl<T: AudioOutputSample>
|
||||
where
|
||||
T: AudioOutputSample,
|
||||
{
|
||||
ring_buf_producer: rb::Producer<T>,
|
||||
sample_buf: SampleBuffer<T>,
|
||||
stream: cpal::Stream,
|
||||
}
|
||||
|
||||
impl<T: AudioOutputSample> CpalAudioOutputImpl<T> {
|
||||
pub fn try_open(
|
||||
spec: SignalSpec,
|
||||
duration: Duration,
|
||||
device: &cpal::Device,
|
||||
) -> Result<Box<dyn AudioOutput>> {
|
||||
let num_channels = spec.channels.count();
|
||||
|
||||
// Output audio stream config.
|
||||
let config = cpal::StreamConfig {
|
||||
channels: num_channels as cpal::ChannelCount,
|
||||
sample_rate: cpal::SampleRate(spec.rate),
|
||||
buffer_size: cpal::BufferSize::Default,
|
||||
};
|
||||
|
||||
// Create a ring buffer with a capacity for up-to 200ms of audio.
|
||||
let ring_len = ((200 * spec.rate as usize) / 1000) * num_channels;
|
||||
|
||||
let ring_buf = SpscRb::new(ring_len);
|
||||
let (ring_buf_producer, ring_buf_consumer) = (ring_buf.producer(), ring_buf.consumer());
|
||||
|
||||
let stream_result = device.build_output_stream(
|
||||
&config,
|
||||
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
// Write out as many samples as possible from the ring buffer to the audio
|
||||
// output.
|
||||
let written = ring_buf_consumer.read(data).unwrap_or(0);
|
||||
// Mute any remaining samples.
|
||||
data[written..].iter_mut().for_each(|s| *s = T::MID);
|
||||
},
|
||||
move |err| error!("audio output error: {}", err),
|
||||
);
|
||||
|
||||
if let Err(err) = stream_result {
|
||||
error!("audio output stream open error: {}", err);
|
||||
|
||||
return Err(AudioOutputError::OpenStreamError);
|
||||
}
|
||||
|
||||
let stream = stream_result.unwrap();
|
||||
|
||||
// Start the output stream.
|
||||
if let Err(err) = stream.play() {
|
||||
error!("audio output stream play error: {}", err);
|
||||
|
||||
return Err(AudioOutputError::PlayStreamError);
|
||||
}
|
||||
|
||||
let sample_buf = SampleBuffer::<T>::new(duration, spec);
|
||||
|
||||
Ok(Box::new(CpalAudioOutputImpl {
|
||||
ring_buf_producer,
|
||||
sample_buf,
|
||||
stream,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AudioOutputSample> AudioOutput for CpalAudioOutputImpl<T> {
|
||||
fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()> {
|
||||
// Do nothing if there are no audio frames.
|
||||
if decoded.frames() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Audio samples must be interleaved for cpal. Interleave the samples in the audio
|
||||
// buffer into the sample buffer.
|
||||
self.sample_buf.copy_interleaved_ref(decoded);
|
||||
|
||||
// Write all the interleaved samples to the ring buffer.
|
||||
let mut samples = self.sample_buf.samples();
|
||||
|
||||
while let Some(written) = self.ring_buf_producer.write_blocking(samples) {
|
||||
samples = &samples[written..];
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
// Flush is best-effort, ignore the returned result.
|
||||
let _ = self.stream.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_open(spec: SignalSpec, duration: Duration) -> Result<Box<dyn AudioOutput>> {
|
||||
cpal::CpalAudioOutput::try_open(spec, duration)
|
||||
}
|
||||
469
src/player.rs
Normal file
469
src/player.rs
Normal file
@@ -0,0 +1,469 @@
|
||||
use futures_util::{future::FusedFuture, Future};
|
||||
use log::{error, trace, warn};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
mem,
|
||||
path::Path,
|
||||
pin::Pin,
|
||||
process::exit,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
thread,
|
||||
};
|
||||
use symphonia::core::{
|
||||
codecs::{DecoderOptions, CODEC_TYPE_NULL},
|
||||
errors::Error,
|
||||
formats::FormatOptions,
|
||||
io::MediaSourceStream,
|
||||
meta::MetadataOptions,
|
||||
probe::Hint,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
audio_backend::Sink,
|
||||
decoder::{symphonia_decoder::SymphoniaDecoder, AudioDecoder},
|
||||
metadata::audio::AudioFileFormat,
|
||||
};
|
||||
|
||||
const PRELOAD_NEXT_TRACK_BEFORE_END: u64 = 30000;
|
||||
|
||||
pub type PlayerResult = Result<(), Error>;
|
||||
|
||||
pub struct Player {
|
||||
commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
|
||||
thread_handle: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new<F>(sink_builder: F) -> (Player, PlayerEventChannel)
|
||||
where
|
||||
F: FnOnce() -> Box<dyn Sink> + Send + 'static,
|
||||
{
|
||||
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||
let (event_sender, event_receiver) = mpsc::unbounded_channel();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let internal = PlayerInternal {
|
||||
commands: cmd_rx,
|
||||
load_handles: Arc::new(Mutex::new(HashMap::new())),
|
||||
sink: sink_builder(),
|
||||
state: PlayerState::Stopped,
|
||||
preload: PlayerPreload::None,
|
||||
sink_status: SinkStatus::Closed,
|
||||
sink_event_callback: None,
|
||||
event_senders: [event_sender].to_vec(),
|
||||
};
|
||||
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||
runtime.block_on(internal);
|
||||
});
|
||||
(
|
||||
Player {
|
||||
commands: Some(cmd_tx),
|
||||
thread_handle: Some(handle),
|
||||
},
|
||||
event_receiver,
|
||||
)
|
||||
}
|
||||
|
||||
fn command(&self, cmd: PlayerCommand) {
|
||||
if let Some(commands) = self.commands.as_ref() {
|
||||
if let Err(e) = commands.send(cmd) {
|
||||
error!("Player Commands Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self, track_id: &str, start_playing: bool, position_ms: u32) {
|
||||
self.command(PlayerCommand::Load {
|
||||
track_id: track_id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn preload(&self, track_id: &str) {
|
||||
self.command(PlayerCommand::Preload);
|
||||
}
|
||||
|
||||
pub fn play(&self) {
|
||||
self.command(PlayerCommand::Play)
|
||||
}
|
||||
|
||||
pub fn pause(&self) {
|
||||
self.command(PlayerCommand::Pause)
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
self.command(PlayerCommand::Stop)
|
||||
}
|
||||
|
||||
pub fn seek(&self, position_ms: u32) {
|
||||
self.command(PlayerCommand::Seek(position_ms));
|
||||
}
|
||||
|
||||
pub fn get_player_event_channel(&self) -> PlayerEventChannel {
|
||||
let (event_sender, event_receiver) = mpsc::unbounded_channel();
|
||||
self.command(PlayerCommand::AddEventSender(event_sender));
|
||||
event_receiver
|
||||
}
|
||||
|
||||
pub async fn await_end_of_track(&self) {
|
||||
let mut channel = self.get_player_event_channel();
|
||||
while let Some(event) = channel.recv().await {
|
||||
if matches!(
|
||||
event,
|
||||
PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. }
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
pub enum SinkStatus {
|
||||
Running,
|
||||
Closed,
|
||||
TemporarilyClosed,
|
||||
}
|
||||
|
||||
pub type SinkEventCallback = Box<dyn Fn(SinkStatus) + Send>;
|
||||
|
||||
struct PlayerInternal {
|
||||
commands: mpsc::UnboundedReceiver<PlayerCommand>,
|
||||
load_handles: Arc<Mutex<HashMap<thread::ThreadId, thread::JoinHandle<()>>>>,
|
||||
|
||||
state: PlayerState,
|
||||
preload: PlayerPreload,
|
||||
sink: Box<dyn Sink>,
|
||||
sink_status: SinkStatus,
|
||||
sink_event_callback: Option<SinkEventCallback>,
|
||||
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
||||
}
|
||||
|
||||
impl Future for PlayerInternal {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
loop {
|
||||
let mut all_futures_completed_or_not_ready = true;
|
||||
|
||||
// process commands that were sent to us
|
||||
let cmd = match self.commands.poll_recv(cx) {
|
||||
Poll::Ready(None) => return Poll::Ready(()), // client has disconnected - shut down.
|
||||
Poll::Ready(Some(cmd)) => {
|
||||
all_futures_completed_or_not_ready = false;
|
||||
Some(cmd)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(cmd) = cmd {
|
||||
if let Err(e) = self.handle_command(cmd) {
|
||||
// error!("Error handling command: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerInternal {
|
||||
fn ensure_sink_running(&mut self) {
|
||||
if self.sink_status != SinkStatus::Running {
|
||||
trace!("== Starting sink ==");
|
||||
if let Some(callback) = &mut self.sink_event_callback {
|
||||
callback(SinkStatus::Running);
|
||||
}
|
||||
match self.sink.start() {
|
||||
Ok(()) => self.sink_status = SinkStatus::Running,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_sink_stopped(&mut self, temporarily: bool) {
|
||||
match self.sink_status {
|
||||
SinkStatus::Running => {
|
||||
trace!("== Stopping sink ==");
|
||||
match self.sink.stop() {
|
||||
Ok(()) => {
|
||||
self.sink_status = if temporarily {
|
||||
SinkStatus::TemporarilyClosed
|
||||
} else {
|
||||
SinkStatus::Closed
|
||||
};
|
||||
if let Some(callback) = &mut self.sink_event_callback {
|
||||
callback(self.sink_status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
SinkStatus::TemporarilyClosed => {
|
||||
if !temporarily {
|
||||
self.sink_status = SinkStatus::Closed;
|
||||
if let Some(callback) = &mut self.sink_event_callback {
|
||||
callback(SinkStatus::Closed);
|
||||
}
|
||||
}
|
||||
}
|
||||
SinkStatus::Closed => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult {
|
||||
match cmd {
|
||||
PlayerCommand::Load { track_id } => {
|
||||
self.handle_command_load(&track_id);
|
||||
}
|
||||
PlayerCommand::Preload => {
|
||||
self.handle_command_preload();
|
||||
}
|
||||
PlayerCommand::Play => {
|
||||
self.handle_play();
|
||||
}
|
||||
PlayerCommand::Pause => {
|
||||
self.handle_pause();
|
||||
}
|
||||
PlayerCommand::Stop => {
|
||||
self.handle_player_stop();
|
||||
}
|
||||
PlayerCommand::Seek(position_ms) => {
|
||||
self.handle_command_seek();
|
||||
}
|
||||
PlayerCommand::SetSinkEventCallback => {
|
||||
self.sink_event_callback = None;
|
||||
}
|
||||
PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_track(&self, song: &str) -> Option<PlayerLoadedTrackData> {
|
||||
// Create a hint to help the format registry guess what format reader is appropriate.
|
||||
let mut hint = Hint::new();
|
||||
|
||||
let path = Path::new(song);
|
||||
|
||||
// Provide the file extension as a hint.
|
||||
if let Some(extension) = path.extension() {
|
||||
if let Some(extension_str) = extension.to_str() {
|
||||
hint.with_extension(extension_str);
|
||||
}
|
||||
}
|
||||
|
||||
let source = Box::new(File::open(path).unwrap());
|
||||
|
||||
// Create the media source stream using the boxed media source from above.
|
||||
let mss = MediaSourceStream::new(source, Default::default());
|
||||
|
||||
let symphonia_decoder = |mss: MediaSourceStream, hint| {
|
||||
SymphoniaDecoder::new(mss, hint).map(|mut decoder| {
|
||||
// For formats other that Vorbis, we'll try getting normalisation data from
|
||||
// ReplayGain metadata fields, if present.
|
||||
Box::new(decoder) as Decoder
|
||||
})
|
||||
};
|
||||
|
||||
let decoder_type = symphonia_decoder(mss, hint);
|
||||
|
||||
let mut decoder = match decoder_type {
|
||||
Ok(decoder) => decoder,
|
||||
Err(e) => {
|
||||
panic!("Failed to create decoder: {}", e);
|
||||
}
|
||||
};
|
||||
return Some(PlayerLoadedTrackData {
|
||||
decoder,
|
||||
bytes_per_second: 0,
|
||||
duration_ms: 0,
|
||||
stream_position_ms: 0,
|
||||
is_explicit: false,
|
||||
});
|
||||
}
|
||||
|
||||
fn start_playback(&mut self, track_id: &str, loaded_track: PlayerLoadedTrackData) {
|
||||
self.ensure_sink_running();
|
||||
self.send_event(PlayerEvent::Playing {
|
||||
track_id: track_id.to_string(),
|
||||
});
|
||||
|
||||
self.state = PlayerState::Playing {
|
||||
track_id: track_id.to_string(),
|
||||
decoder: loaded_track.decoder,
|
||||
};
|
||||
}
|
||||
|
||||
fn send_event(&mut self, event: PlayerEvent) {
|
||||
self.event_senders
|
||||
.retain(|sender| sender.send(event.clone()).is_ok());
|
||||
}
|
||||
|
||||
fn handle_command_load(&mut self, track_id: &str) {
|
||||
println!("load track {}", track_id);
|
||||
self.load_track(track_id);
|
||||
|
||||
//
|
||||
//
|
||||
}
|
||||
|
||||
fn handle_command_preload(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn handle_play(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn handle_player_stop(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn handle_pause(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn handle_command_seek(&self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerLoadedTrackData {
|
||||
decoder: Decoder,
|
||||
bytes_per_second: usize,
|
||||
duration_ms: u32,
|
||||
stream_position_ms: u32,
|
||||
is_explicit: bool,
|
||||
}
|
||||
|
||||
enum PlayerPreload {
|
||||
None,
|
||||
Loading {
|
||||
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
||||
track_id: String,
|
||||
},
|
||||
Ready {
|
||||
loaded_track: Box<PlayerLoadedTrackData>,
|
||||
},
|
||||
}
|
||||
|
||||
type Decoder = Box<dyn AudioDecoder + Send>;
|
||||
|
||||
enum PlayerState {
|
||||
Stopped,
|
||||
Loading {
|
||||
track_id: String,
|
||||
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
||||
},
|
||||
Paused {
|
||||
decoder: Decoder,
|
||||
},
|
||||
Playing {
|
||||
decoder: Decoder,
|
||||
track_id: String,
|
||||
},
|
||||
EndOfTrack {
|
||||
loaded_track: PlayerLoadedTrackData,
|
||||
},
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl PlayerState {
|
||||
fn is_playing(&self) -> bool {
|
||||
use self::PlayerState::*;
|
||||
match *self {
|
||||
Stopped | EndOfTrack { .. } | Paused { .. } | Loading { .. } => false,
|
||||
Playing { .. } => true,
|
||||
Invalid => {
|
||||
// "PlayerState::is_playing in invalid state"
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn is_stopped(&self) -> bool {
|
||||
use self::PlayerState::*;
|
||||
matches!(self, Stopped)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn is_loading(&self) -> bool {
|
||||
use self::PlayerState::*;
|
||||
matches!(self, Loading { .. })
|
||||
}
|
||||
|
||||
fn decoder(&mut self) -> Option<&mut Decoder> {
|
||||
use self::PlayerState::*;
|
||||
match *self {
|
||||
Stopped | EndOfTrack { .. } | Loading { .. } => None,
|
||||
Paused {
|
||||
ref mut decoder, ..
|
||||
}
|
||||
| Playing {
|
||||
ref mut decoder, ..
|
||||
} => Some(decoder),
|
||||
Invalid => {
|
||||
// error!("PlayerState::decoder in invalid state");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerTrackLoader {}
|
||||
|
||||
impl PlayerTrackLoader {
|
||||
fn stream_data_rate(&self, format: AudioFileFormat) -> usize {
|
||||
let kbps = match format {
|
||||
AudioFileFormat::OGG_VORBIS_96 => 12,
|
||||
AudioFileFormat::OGG_VORBIS_160 => 20,
|
||||
AudioFileFormat::OGG_VORBIS_320 => 40,
|
||||
AudioFileFormat::MP3_256 => 32,
|
||||
AudioFileFormat::MP3_320 => 40,
|
||||
AudioFileFormat::MP3_160 => 20,
|
||||
AudioFileFormat::MP3_96 => 12,
|
||||
AudioFileFormat::MP3_160_ENC => 20,
|
||||
AudioFileFormat::MP4_128_DUAL => todo!(),
|
||||
AudioFileFormat::OTHER3 => todo!(),
|
||||
AudioFileFormat::AAC_160 => todo!(),
|
||||
AudioFileFormat::AAC_320 => todo!(),
|
||||
AudioFileFormat::MP4_128 => todo!(),
|
||||
AudioFileFormat::OTHER5 => todo!(),
|
||||
};
|
||||
kbps * 1024
|
||||
}
|
||||
}
|
||||
|
||||
enum PlayerCommand {
|
||||
Load { track_id: String },
|
||||
Preload,
|
||||
Play,
|
||||
Pause,
|
||||
Stop,
|
||||
Seek(u32),
|
||||
AddEventSender(mpsc::UnboundedSender<PlayerEvent>),
|
||||
SetSinkEventCallback,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PlayerEvent {
|
||||
Stopped,
|
||||
Started,
|
||||
Loading { track_id: String },
|
||||
Preloading,
|
||||
Playing { track_id: String },
|
||||
Paused,
|
||||
TimeToPreloadNextTrack,
|
||||
EndOfTrack,
|
||||
VolumeSet { volume: u16 },
|
||||
}
|
||||
|
||||
pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
|
||||
Reference in New Issue
Block a user