Initial Commit

This commit is contained in:
Tsiry Sandratraina
2022-09-07 23:59:24 +03:00
commit dfb6008ceb
27 changed files with 3643 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.vscode

1514
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

47
src/config.rs Normal file
View 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
View 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
View 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())
}
}

View 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
View 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
View 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
View 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
View 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);
}
};
}
}
}

View File

@@ -0,0 +1 @@
pub use librespot_protocol::metadata::AudioFile_Format as AudioFileFormat;

View File

@@ -0,0 +1,3 @@
mod file;
pub use file::AudioFileFormat;

1
src/metadata/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod audio;

180
src/output.rs Normal file
View 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
View 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>;