2021-02-21 18:38:49 +00:00
use futures_util ::{ future , FutureExt , StreamExt } ;
2021-02-21 10:08:34 +00:00
use librespot_playback ::player ::PlayerEvent ;
use log ::{ error , info , warn } ;
use sha1 ::{ Digest , Sha1 } ;
2021-04-13 13:16:48 +00:00
use thiserror ::Error ;
2021-02-21 18:38:40 +00:00
use tokio ::sync ::mpsc ::UnboundedReceiver ;
2021-02-21 10:08:34 +00:00
use url ::Url ;
2021-03-01 02:37:22 +00:00
use librespot ::connect ::spirc ::Spirc ;
2021-02-10 21:40:33 +00:00
use librespot ::core ::authentication ::Credentials ;
2021-02-21 10:08:34 +00:00
use librespot ::core ::cache ::Cache ;
2021-05-24 13:53:32 +00:00
use librespot ::core ::config ::{ ConnectConfig , DeviceType , SessionConfig } ;
2021-02-21 10:08:34 +00:00
use librespot ::core ::session ::Session ;
use librespot ::core ::version ;
2021-05-24 13:53:32 +00:00
use librespot ::playback ::audio_backend ::{ self , SinkBuilder , BACKENDS } ;
2021-03-12 22:05:38 +00:00
use librespot ::playback ::config ::{
2021-05-24 13:53:32 +00:00
AudioFormat , Bitrate , NormalisationMethod , NormalisationType , PlayerConfig , VolumeCtrl ,
2021-03-12 22:05:38 +00:00
} ;
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
use librespot ::playback ::dither ;
2021-05-31 20:32:39 +00:00
#[ cfg(feature = " alsa-backend " ) ]
use librespot ::playback ::mixer ::alsamixer ::AlsaMixer ;
2021-05-24 13:53:32 +00:00
use librespot ::playback ::mixer ::mappings ::MappedCtrl ;
use librespot ::playback ::mixer ::{ self , MixerConfig , MixerFn } ;
use librespot ::playback ::player ::{ db_to_ratio , Player } ;
2021-02-21 10:08:34 +00:00
mod player_event_handler ;
use player_event_handler ::{ emit_sink_event , run_program_on_events } ;
2021-05-31 20:32:39 +00:00
use std ::env ;
use std ::io ::{ stderr , Write } ;
2021-03-01 02:37:22 +00:00
use std ::path ::Path ;
2021-05-31 20:32:39 +00:00
use std ::pin ::Pin ;
2021-03-01 02:37:22 +00:00
use std ::process ::exit ;
use std ::str ::FromStr ;
2021-05-31 20:32:39 +00:00
use std ::time ::Duration ;
use std ::time ::Instant ;
2021-02-24 20:39:42 +00:00
2021-02-21 10:08:34 +00:00
fn device_id ( name : & str ) -> String {
hex ::encode ( Sha1 ::digest ( name . as_bytes ( ) ) )
}
fn usage ( program : & str , opts : & getopts ::Options ) -> String {
let brief = format! ( " Usage: {} [options] " , program ) ;
opts . usage ( & brief )
}
fn setup_logging ( verbose : bool ) {
let mut builder = env_logger ::Builder ::new ( ) ;
match env ::var ( " RUST_LOG " ) {
Ok ( config ) = > {
builder . parse_filters ( & config ) ;
builder . init ( ) ;
if verbose {
warn! ( " `--verbose` flag overidden by `RUST_LOG` environment variable " ) ;
}
}
Err ( _ ) = > {
if verbose {
builder . parse_filters ( " libmdns=info,librespot=trace " ) ;
} else {
builder . parse_filters ( " libmdns=info,librespot=info " ) ;
}
builder . init ( ) ;
}
}
}
fn list_backends ( ) {
2021-05-24 13:53:32 +00:00
println! ( " Available backends : " ) ;
2021-02-21 10:08:34 +00:00
for ( & ( name , _ ) , idx ) in BACKENDS . iter ( ) . zip ( 0 .. ) {
if idx = = 0 {
println! ( " - {} (default) " , name ) ;
} else {
println! ( " - {} " , name ) ;
}
}
}
2021-02-10 21:40:33 +00:00
pub fn get_credentials < F : FnOnce ( & String ) -> Option < String > > (
username : Option < String > ,
password : Option < String > ,
cached_credentials : Option < Credentials > ,
prompt : F ,
) -> Option < Credentials > {
if let Some ( username ) = username {
if let Some ( password ) = password {
return Some ( Credentials ::with_password ( username , password ) ) ;
}
match cached_credentials {
Some ( credentials ) if username = = credentials . username = > Some ( credentials ) ,
_ = > {
let password = prompt ( & username ) ? ;
Some ( Credentials ::with_password ( username , password ) )
}
}
} else {
cached_credentials
}
}
2021-04-13 13:16:48 +00:00
#[ derive(Debug, Error) ]
pub enum ParseFileSizeError {
#[ error( " empty argument " ) ]
EmptyInput ,
#[ error( " invalid suffix " ) ]
InvalidSuffix ,
#[ error( " invalid number: {0} " ) ]
InvalidNumber ( #[ from ] std ::num ::ParseFloatError ) ,
#[ error( " non-finite number specified " ) ]
NotFinite ( f64 ) ,
}
pub fn parse_file_size ( input : & str ) -> Result < u64 , ParseFileSizeError > {
use ParseFileSizeError ::* ;
let mut iter = input . chars ( ) ;
let mut suffix = iter . next_back ( ) . ok_or ( EmptyInput ) ? ;
let mut suffix_len = 0 ;
let iec = matches! ( suffix , 'i' | 'I' ) ;
if iec {
suffix_len + = 1 ;
suffix = iter . next_back ( ) . ok_or ( InvalidSuffix ) ? ;
}
let base : u64 = if iec { 1024 } else { 1000 } ;
suffix_len + = 1 ;
let exponent = match suffix . to_ascii_uppercase ( ) {
'0' ..= '9' if ! iec = > {
suffix_len - = 1 ;
0
}
'K' = > 1 ,
'M' = > 2 ,
'G' = > 3 ,
'T' = > 4 ,
'P' = > 5 ,
'E' = > 6 ,
'Z' = > 7 ,
'Y' = > 8 ,
_ = > return Err ( InvalidSuffix ) ,
} ;
let num = {
let mut iter = input . chars ( ) ;
for _ in ( & mut iter ) . rev ( ) . take ( suffix_len ) { }
iter . as_str ( ) . parse ::< f64 > ( ) ?
} ;
if ! num . is_finite ( ) {
return Err ( NotFinite ( num ) ) ;
}
Ok ( ( num * base . pow ( exponent ) as f64 ) as u64 )
}
2021-10-14 10:57:33 +00:00
fn get_version_string ( ) -> String {
#[ cfg(debug_assertions) ]
const BUILD_PROFILE : & str = " debug " ;
#[ cfg(not(debug_assertions)) ]
const BUILD_PROFILE : & str = " release " ;
format! (
" librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id}, Profile: {build_profile}) " ,
2021-02-23 18:35:57 +00:00
semver = version ::SEMVER ,
sha = version ::SHA_SHORT ,
build_date = version ::BUILD_DATE ,
2021-10-14 10:57:33 +00:00
build_id = version ::BUILD_ID ,
build_profile = BUILD_PROFILE
)
2021-02-23 18:35:57 +00:00
}
2021-02-21 10:08:34 +00:00
struct Setup {
2021-03-12 22:05:38 +00:00
format : AudioFormat ,
2021-05-24 13:53:32 +00:00
backend : SinkBuilder ,
2021-02-21 10:08:34 +00:00
device : Option < String > ,
2021-05-24 13:53:32 +00:00
mixer : MixerFn ,
2021-02-21 10:08:34 +00:00
cache : Option < Cache > ,
player_config : PlayerConfig ,
session_config : SessionConfig ,
connect_config : ConnectConfig ,
mixer_config : MixerConfig ,
credentials : Option < Credentials > ,
enable_discovery : bool ,
zeroconf_port : u16 ,
player_event_program : Option < String > ,
emit_sink_events : bool ,
}
2021-04-10 13:08:39 +00:00
fn get_setup ( args : & [ String ] ) -> Setup {
2021-05-31 20:32:39 +00:00
const AP_PORT : & str = " ap-port " ;
const AUTOPLAY : & str = " autoplay " ;
const BACKEND : & str = " backend " ;
const BITRATE : & str = " b " ;
const CACHE : & str = " c " ;
const CACHE_SIZE_LIMIT : & str = " cache-size-limit " ;
const DEVICE : & str = " device " ;
const DEVICE_TYPE : & str = " device-type " ;
const DISABLE_AUDIO_CACHE : & str = " disable-audio-cache " ;
const DISABLE_DISCOVERY : & str = " disable-discovery " ;
const DISABLE_GAPLESS : & str = " disable-gapless " ;
const DITHER : & str = " dither " ;
const EMIT_SINK_EVENTS : & str = " emit-sink-events " ;
const ENABLE_VOLUME_NORMALISATION : & str = " enable-volume-normalisation " ;
const FORMAT : & str = " format " ;
const HELP : & str = " h " ;
const INITIAL_VOLUME : & str = " initial-volume " ;
2021-07-09 18:12:44 +00:00
const MIXER_TYPE : & str = " mixer " ;
2021-08-26 20:35:45 +00:00
const ALSA_MIXER_DEVICE : & str = " alsa-mixer-device " ;
const ALSA_MIXER_INDEX : & str = " alsa-mixer-index " ;
const ALSA_MIXER_CONTROL : & str = " alsa-mixer-control " ;
2021-05-31 20:32:39 +00:00
const NAME : & str = " name " ;
const NORMALISATION_ATTACK : & str = " normalisation-attack " ;
const NORMALISATION_GAIN_TYPE : & str = " normalisation-gain-type " ;
const NORMALISATION_KNEE : & str = " normalisation-knee " ;
const NORMALISATION_METHOD : & str = " normalisation-method " ;
const NORMALISATION_PREGAIN : & str = " normalisation-pregain " ;
const NORMALISATION_RELEASE : & str = " normalisation-release " ;
const NORMALISATION_THRESHOLD : & str = " normalisation-threshold " ;
const ONEVENT : & str = " onevent " ;
const PASSTHROUGH : & str = " passthrough " ;
const PASSWORD : & str = " password " ;
const PROXY : & str = " proxy " ;
const SYSTEM_CACHE : & str = " system-cache " ;
const USERNAME : & str = " username " ;
const VERBOSE : & str = " verbose " ;
const VERSION : & str = " version " ;
const VOLUME_CTRL : & str = " volume-ctrl " ;
const VOLUME_RANGE : & str = " volume-range " ;
const ZEROCONF_PORT : & str = " zeroconf-port " ;
2021-02-21 10:08:34 +00:00
let mut opts = getopts ::Options ::new ( ) ;
2021-05-26 20:30:32 +00:00
opts . optflag (
2021-05-31 20:32:39 +00:00
HELP ,
2021-05-26 20:30:32 +00:00
" help " ,
" Print this help menu. " ,
) . optopt (
2021-05-31 20:32:39 +00:00
CACHE ,
2021-02-21 10:08:34 +00:00
" cache " ,
" Path to a directory where files will be cached. " ,
2021-05-26 20:30:32 +00:00
" PATH " ,
2021-02-21 10:08:34 +00:00
) . optopt (
" " ,
2021-05-31 20:32:39 +00:00
SYSTEM_CACHE ,
2021-10-21 22:24:02 +00:00
" Path to a directory where system files (credentials, volume) will be cached. May be different from the cache option value. " ,
2021-05-26 20:30:32 +00:00
" PATH " ,
2021-04-13 13:16:48 +00:00
) . optopt (
" " ,
2021-05-31 20:32:39 +00:00
CACHE_SIZE_LIMIT ,
2021-04-13 13:16:48 +00:00
" Limits the size of the cache for audio files. " ,
2021-05-26 20:30:32 +00:00
" SIZE "
2021-05-31 20:32:39 +00:00
) . optflag ( " " , DISABLE_AUDIO_CACHE , " Disable caching of the audio data. " )
. optopt ( " n " , NAME , " Device name. " , " NAME " )
2021-10-21 22:24:02 +00:00
. optopt ( " " , DEVICE_TYPE , " Displayed device type. Defaults to 'Speaker'. " , " TYPE " )
2021-05-26 20:30:32 +00:00
. optopt (
2021-05-31 20:32:39 +00:00
BITRATE ,
2021-05-26 20:30:32 +00:00
" bitrate " ,
" Bitrate (kbps) {96|160|320}. Defaults to 160. " ,
" BITRATE " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
ONEVENT ,
2021-05-26 20:30:32 +00:00
" Run PROGRAM when a playback event occurs. " ,
" PROGRAM " ,
)
2021-10-21 22:24:02 +00:00
. optflag ( " " , EMIT_SINK_EVENTS , " Run PROGRAM set by --onevent before sink is opened and after it is closed. " )
2021-05-31 20:32:39 +00:00
. optflag ( " v " , VERBOSE , " Enable verbose output. " )
. optflag ( " V " , VERSION , " Display librespot version string. " )
2021-10-21 22:24:02 +00:00
. optopt ( " u " , USERNAME , " Username used to sign in with. " , " USERNAME " )
. optopt ( " p " , PASSWORD , " Password used to sign in with. " , " PASSWORD " )
2021-05-31 20:32:39 +00:00
. optopt ( " " , PROXY , " HTTP proxy to use when connecting. " , " URL " )
2021-10-21 22:24:02 +00:00
. optopt ( " " , AP_PORT , " Connect to an AP with a specified port. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070. " , " PORT " )
. optflag ( " " , DISABLE_DISCOVERY , " Disable zeroconf discovery mode. " )
2021-05-26 20:30:32 +00:00
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
BACKEND ,
2021-05-26 20:30:32 +00:00
" Audio backend to use. Use '?' to list options. " ,
" NAME " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
DEVICE ,
2021-10-21 22:24:02 +00:00
" Audio device to use. Use '?' to list options if using alsa, portaudio or rodio. Defaults to the backend's default. " ,
2021-05-26 20:30:32 +00:00
" NAME " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
FORMAT ,
2021-05-30 18:09:39 +00:00
" Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16. " ,
2021-05-26 20:30:32 +00:00
" FORMAT " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
DITHER ,
2021-10-21 22:24:02 +00:00
" Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats. " ,
2021-05-26 20:30:32 +00:00
" DITHER " ,
)
2021-10-21 22:24:02 +00:00
. optopt ( " m " , MIXER_TYPE , " Mixer to use {alsa|softvol}. Defaults to softvol " , " MIXER " )
2021-05-26 20:30:32 +00:00
. optopt (
2021-08-26 20:35:45 +00:00
" " ,
" mixer-name " , // deprecated
" " ,
" " ,
)
. optopt (
" " ,
ALSA_MIXER_CONTROL ,
2021-10-21 22:24:02 +00:00
" Alsa mixer control, e.g. 'PCM', 'Master' or similar. Defaults to 'PCM'. " ,
2021-05-26 20:30:32 +00:00
" NAME " ,
)
. optopt (
" " ,
2021-08-26 20:35:45 +00:00
" mixer-card " , // deprecated
" " ,
" " ,
)
. optopt (
" " ,
ALSA_MIXER_DEVICE ,
" Alsa mixer device, e.g 'hw:0' or similar from `aplay -l`. Defaults to `--device` if specified, 'default' otherwise. " ,
" DEVICE " ,
)
. optopt (
" " ,
" mixer-index " , // deprecated
" " ,
" " ,
2021-05-26 20:30:32 +00:00
)
. optopt (
" " ,
2021-08-26 20:35:45 +00:00
ALSA_MIXER_INDEX ,
2021-05-26 20:30:32 +00:00
" Alsa index of the cards mixer. Defaults to 0. " ,
2021-08-26 20:35:45 +00:00
" NUMBER " ,
2021-05-26 20:30:32 +00:00
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
INITIAL_VOLUME ,
2021-05-26 20:30:32 +00:00
" Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume. " ,
" VOLUME " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
ZEROCONF_PORT ,
2021-10-21 22:24:02 +00:00
" The port the internal server advertises over zeroconf. " ,
2021-05-26 20:30:32 +00:00
" PORT " ,
)
. optflag (
" " ,
2021-05-31 20:32:39 +00:00
ENABLE_VOLUME_NORMALISATION ,
2021-10-21 22:24:02 +00:00
" Play all tracks at approximately the same apparent volume. " ,
2021-05-26 20:30:32 +00:00
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
NORMALISATION_METHOD ,
2021-05-26 20:30:32 +00:00
" Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic. " ,
" METHOD " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
NORMALISATION_GAIN_TYPE ,
2021-09-20 17:22:02 +00:00
" Specify the normalisation gain type to use {track|album|auto}. Defaults to auto. " ,
2021-05-26 20:30:32 +00:00
" TYPE " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
NORMALISATION_PREGAIN ,
2021-05-26 20:30:32 +00:00
" Pregain (dB) applied by volume normalisation. Defaults to 0. " ,
" PREGAIN " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
NORMALISATION_THRESHOLD ,
2021-10-21 22:24:02 +00:00
" Threshold (dBFS) at which the dynamic limiter engages to prevent clipping. Defaults to -2.0. " ,
2021-05-26 20:30:32 +00:00
" THRESHOLD " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
NORMALISATION_ATTACK ,
2021-10-21 22:24:02 +00:00
" Attack time (ms) in which the dynamic limiter reduces gain. Defaults to 5. " ,
2021-05-26 20:30:32 +00:00
" TIME " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
NORMALISATION_RELEASE ,
2021-10-21 22:24:02 +00:00
" Release or decay time (ms) in which the dynamic limiter restores gain. Defaults to 100. " ,
2021-05-26 20:30:32 +00:00
" TIME " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
NORMALISATION_KNEE ,
2021-05-26 20:30:32 +00:00
" Knee steepness of the dynamic limiter. Defaults to 1.0. " ,
" KNEE " ,
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
VOLUME_CTRL ,
2021-10-21 22:24:02 +00:00
" Volume control scale type {cubic|fixed|linear|log}. Defaults to log. " ,
2021-05-26 20:30:32 +00:00
" VOLUME_CTRL "
)
. optopt (
" " ,
2021-05-31 20:32:39 +00:00
VOLUME_RANGE ,
2021-05-26 20:30:32 +00:00
" Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports. " ,
" RANGE " ,
)
. optflag (
" " ,
2021-05-31 20:32:39 +00:00
AUTOPLAY ,
2021-05-26 20:30:32 +00:00
" Automatically play similar songs when your music ends. " ,
)
. optflag (
" " ,
2021-05-31 20:32:39 +00:00
DISABLE_GAPLESS ,
2021-05-26 20:30:32 +00:00
" Disable gapless playback. " ,
)
. optflag (
" " ,
2021-05-31 20:32:39 +00:00
PASSTHROUGH ,
2021-10-21 22:24:02 +00:00
" Pass a raw stream to the output. Only works with the pipe and subprocess backends. " ,
2021-05-26 20:30:32 +00:00
) ;
2021-02-21 10:08:34 +00:00
let matches = match opts . parse ( & args [ 1 .. ] ) {
Ok ( m ) = > m ,
Err ( f ) = > {
2021-05-26 20:30:32 +00:00
eprintln! (
" Error parsing command line options: {} \n {} " ,
f ,
usage ( & args [ 0 ] , & opts )
) ;
2021-02-21 10:08:34 +00:00
exit ( 1 ) ;
}
} ;
2021-05-31 20:32:39 +00:00
if matches . opt_present ( HELP ) {
2021-05-26 20:30:32 +00:00
println! ( " {} " , usage ( & args [ 0 ] , & opts ) ) ;
exit ( 0 ) ;
}
2021-05-31 20:32:39 +00:00
if matches . opt_present ( VERSION ) {
2021-10-14 10:57:33 +00:00
println! ( " {} " , get_version_string ( ) ) ;
2021-02-23 18:35:57 +00:00
exit ( 0 ) ;
}
2021-05-31 20:32:39 +00:00
let verbose = matches . opt_present ( VERBOSE ) ;
2021-02-21 10:08:34 +00:00
setup_logging ( verbose ) ;
2021-10-14 10:57:33 +00:00
info! ( " {} " , get_version_string ( ) ) ;
2021-02-21 10:08:34 +00:00
2021-05-31 20:32:39 +00:00
let backend_name = matches . opt_str ( BACKEND ) ;
2021-02-21 10:08:34 +00:00
if backend_name = = Some ( " ? " . into ( ) ) {
list_backends ( ) ;
exit ( 0 ) ;
}
let backend = audio_backend ::find ( backend_name ) . expect ( " Invalid backend " ) ;
2021-03-12 22:05:38 +00:00
let format = matches
2021-05-31 20:32:39 +00:00
. opt_str ( FORMAT )
. as_deref ( )
. map ( | format | AudioFormat ::from_str ( format ) . expect ( " Invalid output format " ) )
2021-04-10 12:06:41 +00:00
. unwrap_or_default ( ) ;
2021-03-12 22:05:38 +00:00
2021-05-31 20:32:39 +00:00
let device = matches . opt_str ( DEVICE ) ;
2021-02-21 10:08:34 +00:00
if device = = Some ( " ? " . into ( ) ) {
2021-03-12 22:05:38 +00:00
backend ( device , format ) ;
2021-02-21 10:08:34 +00:00
exit ( 0 ) ;
}
2021-07-09 18:12:44 +00:00
let mixer_type = matches . opt_str ( MIXER_TYPE ) ;
let mixer = mixer ::find ( mixer_type . as_deref ( ) ) . expect ( " Invalid mixer " ) ;
2021-02-21 10:08:34 +00:00
2021-05-24 13:53:32 +00:00
let mixer_config = {
2021-08-26 20:35:45 +00:00
let mixer_device = match matches . opt_str ( " mixer-card " ) {
Some ( card ) = > {
warn! ( " --mixer-card is deprecated and will be removed in a future release. " ) ;
warn! ( " Please use --alsa-mixer-device instead. " ) ;
card
}
None = > matches . opt_str ( ALSA_MIXER_DEVICE ) . unwrap_or_else ( | | {
if let Some ( ref device_name ) = device {
device_name . to_string ( )
} else {
MixerConfig ::default ( ) . device
}
} ) ,
} ;
let index = match matches . opt_str ( " mixer-index " ) {
Some ( index ) = > {
warn! ( " --mixer-index is deprecated and will be removed in a future release. " ) ;
warn! ( " Please use --alsa-mixer-index instead. " ) ;
index
. parse ::< u32 > ( )
. expect ( " Mixer index is not a valid number " )
2021-05-24 13:53:32 +00:00
}
2021-08-26 20:35:45 +00:00
None = > matches
. opt_str ( ALSA_MIXER_INDEX )
. map ( | index | {
index
. parse ::< u32 > ( )
. expect ( " Alsa mixer index is not a valid number " )
} )
. unwrap_or ( 0 ) ,
} ;
let control = match matches . opt_str ( " mixer-name " ) {
Some ( name ) = > {
warn! ( " --mixer-name is deprecated and will be removed in a future release. " ) ;
warn! ( " Please use --alsa-mixer-control instead. " ) ;
name
}
None = > matches
. opt_str ( ALSA_MIXER_CONTROL )
. unwrap_or_else ( | | MixerConfig ::default ( ) . control ) ,
} ;
2021-05-24 13:53:32 +00:00
let mut volume_range = matches
2021-05-31 20:32:39 +00:00
. opt_str ( VOLUME_RANGE )
2021-05-30 18:09:39 +00:00
. map ( | range | range . parse ::< f64 > ( ) . unwrap ( ) )
2021-07-09 18:12:44 +00:00
. unwrap_or_else ( | | match mixer_type . as_deref ( ) {
2021-05-31 20:32:39 +00:00
#[ cfg(feature = " alsa-backend " ) ]
Some ( AlsaMixer ::NAME ) = > 0.0 , // let Alsa query the control
2021-05-24 13:53:32 +00:00
_ = > VolumeCtrl ::DEFAULT_DB_RANGE ,
} ) ;
if volume_range < 0.0 {
// User might have specified range as minimum dB volume.
2021-05-25 18:17:28 +00:00
volume_range = - volume_range ;
2021-05-24 13:53:32 +00:00
warn! (
" Please enter positive volume ranges only, assuming {:.2} dB " ,
volume_range
) ;
}
let volume_ctrl = matches
2021-05-31 20:32:39 +00:00
. opt_str ( VOLUME_CTRL )
. as_deref ( )
2021-05-24 13:53:32 +00:00
. map ( | volume_ctrl | {
VolumeCtrl ::from_str_with_range ( volume_ctrl , volume_range )
. expect ( " Invalid volume control type " )
} )
. unwrap_or_else ( | | {
let mut volume_ctrl = VolumeCtrl ::default ( ) ;
volume_ctrl . set_db_range ( volume_range ) ;
volume_ctrl
} ) ;
MixerConfig {
2021-08-26 20:35:45 +00:00
device : mixer_device ,
2021-05-24 13:53:32 +00:00
control ,
index ,
volume_ctrl ,
}
2021-02-21 10:08:34 +00:00
} ;
let cache = {
let audio_dir ;
let system_dir ;
2021-05-31 20:32:39 +00:00
if matches . opt_present ( DISABLE_AUDIO_CACHE ) {
2021-02-21 10:08:34 +00:00
audio_dir = None ;
system_dir = matches
2021-05-31 20:32:39 +00:00
. opt_str ( SYSTEM_CACHE )
. or_else ( | | matches . opt_str ( CACHE ) )
2021-02-21 10:08:34 +00:00
. map ( | p | p . into ( ) ) ;
} else {
2021-05-31 20:32:39 +00:00
let cache_dir = matches . opt_str ( CACHE ) ;
2021-02-21 10:08:34 +00:00
audio_dir = cache_dir
. as_ref ( )
. map ( | p | AsRef ::< Path > ::as_ref ( p ) . join ( " files " ) ) ;
system_dir = matches
2021-05-31 20:32:39 +00:00
. opt_str ( SYSTEM_CACHE )
2021-02-21 10:08:34 +00:00
. or ( cache_dir )
. map ( | p | p . into ( ) ) ;
}
2021-04-13 13:16:48 +00:00
let limit = if audio_dir . is_some ( ) {
matches
2021-05-31 20:32:39 +00:00
. opt_str ( CACHE_SIZE_LIMIT )
2021-04-13 13:16:48 +00:00
. as_deref ( )
. map ( parse_file_size )
. map ( | e | {
e . unwrap_or_else ( | e | {
eprintln! ( " Invalid argument passed as cache size limit: {} " , e ) ;
exit ( 1 ) ;
} )
} )
} else {
None
} ;
match Cache ::new ( system_dir , audio_dir , limit ) {
2021-02-21 10:08:34 +00:00
Ok ( cache ) = > Some ( cache ) ,
Err ( e ) = > {
warn! ( " Cannot create cache: {} " , e ) ;
None
}
}
} ;
let initial_volume = matches
2021-05-31 20:32:39 +00:00
. opt_str ( INITIAL_VOLUME )
2021-05-24 13:53:32 +00:00
. map ( | initial_volume | {
let volume = initial_volume . parse ::< u16 > ( ) . unwrap ( ) ;
2021-02-21 10:08:34 +00:00
if volume > 100 {
2021-05-24 13:53:32 +00:00
error! ( " Initial volume must be in the range 0-100. " ) ;
// the cast will saturate, not necessary to take further action
2021-02-21 10:08:34 +00:00
}
2021-05-24 13:53:32 +00:00
( volume as f32 / 100.0 * VolumeCtrl ::MAX_VOLUME as f32 ) as u16
2021-02-21 10:08:34 +00:00
} )
2021-07-09 18:12:44 +00:00
. or_else ( | | match mixer_type . as_deref ( ) {
2021-05-31 20:32:39 +00:00
#[ cfg(feature = " alsa-backend " ) ]
Some ( AlsaMixer ::NAME ) = > None ,
2021-05-24 13:53:32 +00:00
_ = > cache . as_ref ( ) . and_then ( Cache ::volume ) ,
} ) ;
2021-02-21 10:08:34 +00:00
let zeroconf_port = matches
2021-05-31 20:32:39 +00:00
. opt_str ( ZEROCONF_PORT )
2021-02-21 10:08:34 +00:00
. map ( | port | port . parse ::< u16 > ( ) . unwrap ( ) )
. unwrap_or ( 0 ) ;
2021-03-31 18:05:32 +00:00
let name = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NAME )
2021-03-31 18:05:32 +00:00
. unwrap_or_else ( | | " Librespot " . to_string ( ) ) ;
2021-02-21 10:08:34 +00:00
let credentials = {
let cached_credentials = cache . as_ref ( ) . and_then ( Cache ::credentials ) ;
2021-02-10 21:40:33 +00:00
let password = | username : & String | -> Option < String > {
write! ( stderr ( ) , " Password for {}: " , username ) . ok ( ) ? ;
stderr ( ) . flush ( ) . ok ( ) ? ;
rpassword ::read_password ( ) . ok ( )
2021-02-21 10:08:34 +00:00
} ;
get_credentials (
2021-05-31 20:32:39 +00:00
matches . opt_str ( USERNAME ) ,
matches . opt_str ( PASSWORD ) ,
2021-02-21 10:08:34 +00:00
cached_credentials ,
password ,
)
} ;
let session_config = {
let device_id = device_id ( & name ) ;
SessionConfig {
2021-02-09 18:42:56 +00:00
user_agent : version ::VERSION_STRING . to_string ( ) ,
2021-03-10 21:39:01 +00:00
device_id ,
2021-05-31 20:32:39 +00:00
proxy : matches . opt_str ( PROXY ) . or_else ( | | std ::env ::var ( " http_proxy " ) . ok ( ) ) . map (
2021-02-21 10:08:34 +00:00
| s | {
match Url ::parse ( & s ) {
Ok ( url ) = > {
if url . host ( ) . is_none ( ) | | url . port_or_known_default ( ) . is_none ( ) {
2021-05-26 20:30:32 +00:00
panic! ( " Invalid proxy url, only URLs on the format \" http://host:port \" are allowed " ) ;
2021-02-21 10:08:34 +00:00
}
if url . scheme ( ) ! = " http " {
2021-05-25 18:17:28 +00:00
panic! ( " Only unsecure http:// proxies are supported " ) ;
2021-02-21 10:08:34 +00:00
}
url
} ,
2021-05-26 20:30:32 +00:00
Err ( err ) = > panic! ( " Invalid proxy URL: {} , only URLs in the format \" http://host:port \" are allowed " , err )
2021-02-21 10:08:34 +00:00
}
} ,
) ,
ap_port : matches
2021-05-31 20:32:39 +00:00
. opt_str ( AP_PORT )
2021-02-21 10:08:34 +00:00
. map ( | port | port . parse ::< u16 > ( ) . expect ( " Invalid port " ) ) ,
}
} ;
let player_config = {
let bitrate = matches
2021-05-31 20:32:39 +00:00
. opt_str ( BITRATE )
. as_deref ( )
2021-02-21 10:08:34 +00:00
. map ( | bitrate | Bitrate ::from_str ( bitrate ) . expect ( " Invalid bitrate " ) )
2021-03-10 21:32:24 +00:00
. unwrap_or_default ( ) ;
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
2021-05-31 20:32:39 +00:00
let gapless = ! matches . opt_present ( DISABLE_GAPLESS ) ;
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
2021-05-31 20:32:39 +00:00
let normalisation = matches . opt_present ( ENABLE_VOLUME_NORMALISATION ) ;
2021-05-24 13:53:32 +00:00
let normalisation_method = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NORMALISATION_METHOD )
. as_deref ( )
2021-05-25 18:17:28 +00:00
. map ( | method | {
NormalisationMethod ::from_str ( method ) . expect ( " Invalid normalisation method " )
2021-02-21 10:08:34 +00:00
} )
2021-03-10 21:32:24 +00:00
. unwrap_or_default ( ) ;
2021-05-24 13:53:32 +00:00
let normalisation_type = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NORMALISATION_GAIN_TYPE )
. as_deref ( )
2021-02-24 20:39:42 +00:00
. map ( | gain_type | {
2021-05-24 13:53:32 +00:00
NormalisationType ::from_str ( gain_type ) . expect ( " Invalid normalisation type " )
2021-02-24 20:39:42 +00:00
} )
2021-04-10 08:27:24 +00:00
. unwrap_or_default ( ) ;
2021-05-24 13:53:32 +00:00
let normalisation_pregain = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NORMALISATION_PREGAIN )
2021-05-30 18:09:39 +00:00
. map ( | pregain | pregain . parse ::< f64 > ( ) . expect ( " Invalid pregain float value " ) )
2021-05-24 13:53:32 +00:00
. unwrap_or ( PlayerConfig ::default ( ) . normalisation_pregain ) ;
let normalisation_threshold = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NORMALISATION_THRESHOLD )
2021-05-24 13:53:32 +00:00
. map ( | threshold | {
db_to_ratio (
threshold
2021-05-30 18:09:39 +00:00
. parse ::< f64 > ( )
2021-05-24 13:53:32 +00:00
. expect ( " Invalid threshold float value " ) ,
)
} )
. unwrap_or ( PlayerConfig ::default ( ) . normalisation_threshold ) ;
let normalisation_attack = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NORMALISATION_ATTACK )
. map ( | attack | {
Duration ::from_millis ( attack . parse ::< u64 > ( ) . expect ( " Invalid attack value " ) )
} )
2021-05-24 13:53:32 +00:00
. unwrap_or ( PlayerConfig ::default ( ) . normalisation_attack ) ;
let normalisation_release = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NORMALISATION_RELEASE )
. map ( | release | {
Duration ::from_millis ( release . parse ::< u64 > ( ) . expect ( " Invalid release value " ) )
} )
2021-05-24 13:53:32 +00:00
. unwrap_or ( PlayerConfig ::default ( ) . normalisation_release ) ;
let normalisation_knee = matches
2021-05-31 20:32:39 +00:00
. opt_str ( NORMALISATION_KNEE )
2021-05-30 18:09:39 +00:00
. map ( | knee | knee . parse ::< f64 > ( ) . expect ( " Invalid knee float value " ) )
2021-05-24 13:53:32 +00:00
. unwrap_or ( PlayerConfig ::default ( ) . normalisation_knee ) ;
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
2021-05-31 20:32:39 +00:00
let ditherer_name = matches . opt_str ( DITHER ) ;
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
let ditherer = match ditherer_name . as_deref ( ) {
// explicitly disabled on command line
Some ( " none " ) = > None ,
// explicitly set on command line
Some ( _ ) = > {
2021-05-30 18:09:39 +00:00
if format = = AudioFormat ::F64 | | format = = AudioFormat ::F32 {
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
unimplemented! ( " Dithering is not available on format {:?} " , format ) ;
}
Some ( dither ::find_ditherer ( ditherer_name ) . expect ( " Invalid ditherer " ) )
}
// nothing set on command line => use default
None = > match format {
AudioFormat ::S16 | AudioFormat ::S24 | AudioFormat ::S24_3 = > {
PlayerConfig ::default ( ) . ditherer
}
_ = > None ,
} ,
} ;
2021-05-31 20:32:39 +00:00
let passthrough = matches . opt_present ( PASSTHROUGH ) ;
2021-04-10 08:27:24 +00:00
2021-02-21 10:08:34 +00:00
PlayerConfig {
2021-03-10 21:39:01 +00:00
bitrate ,
2021-05-24 13:53:32 +00:00
gapless ,
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
passthrough ,
2021-05-24 13:53:32 +00:00
normalisation ,
normalisation_type ,
2021-04-10 12:06:41 +00:00
normalisation_method ,
2021-05-24 13:53:32 +00:00
normalisation_pregain ,
normalisation_threshold ,
normalisation_attack ,
normalisation_release ,
normalisation_knee ,
Implement dithering (#694)
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.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
ditherer ,
2021-02-21 10:08:34 +00:00
}
} ;
let connect_config = {
let device_type = matches
2021-05-31 20:32:39 +00:00
. opt_str ( DEVICE_TYPE )
. as_deref ( )
2021-02-21 10:08:34 +00:00
. map ( | device_type | DeviceType ::from_str ( device_type ) . expect ( " Invalid device type " ) )
2021-03-10 21:32:24 +00:00
. unwrap_or_default ( ) ;
2021-05-24 13:53:32 +00:00
let has_volume_ctrl = ! matches! ( mixer_config . volume_ctrl , VolumeCtrl ::Fixed ) ;
2021-05-31 20:32:39 +00:00
let autoplay = matches . opt_present ( AUTOPLAY ) ;
2021-02-21 10:08:34 +00:00
ConnectConfig {
2021-03-10 21:39:01 +00:00
name ,
device_type ,
2021-05-24 13:53:32 +00:00
initial_volume ,
has_volume_ctrl ,
autoplay ,
2021-02-21 10:08:34 +00:00
}
} ;
2021-05-31 20:32:39 +00:00
let enable_discovery = ! matches . opt_present ( DISABLE_DISCOVERY ) ;
let player_event_program = matches . opt_str ( ONEVENT ) ;
let emit_sink_events = matches . opt_present ( EMIT_SINK_EVENTS ) ;
2021-02-21 10:08:34 +00:00
Setup {
2021-04-10 08:27:24 +00:00
format ,
2021-03-10 21:39:01 +00:00
backend ,
2021-05-24 13:53:32 +00:00
device ,
mixer ,
2021-03-10 21:39:01 +00:00
cache ,
player_config ,
2021-05-24 13:53:32 +00:00
session_config ,
2021-03-10 21:39:01 +00:00
connect_config ,
2021-05-24 13:53:32 +00:00
mixer_config ,
2021-03-10 21:39:01 +00:00
credentials ,
enable_discovery ,
zeroconf_port ,
2021-05-24 13:53:32 +00:00
player_event_program ,
emit_sink_events ,
2021-02-21 10:08:34 +00:00
}
}
2021-03-01 03:09:46 +00:00
#[ tokio::main(flavor = " current_thread " ) ]
2021-02-21 10:08:34 +00:00
async fn main ( ) {
2021-05-31 20:32:39 +00:00
const RUST_BACKTRACE : & str = " RUST_BACKTRACE " ;
if env ::var ( RUST_BACKTRACE ) . is_err ( ) {
env ::set_var ( RUST_BACKTRACE , " full " )
2021-02-21 10:08:34 +00:00
}
let args : Vec < String > = std ::env ::args ( ) . collect ( ) ;
2021-04-10 13:08:39 +00:00
let setup = get_setup ( & args ) ;
2021-02-21 10:08:34 +00:00
let mut last_credentials = None ;
let mut spirc : Option < Spirc > = None ;
let mut spirc_task : Option < Pin < _ > > = None ;
let mut player_event_channel : Option < UnboundedReceiver < PlayerEvent > > = None ;
let mut auto_connect_times : Vec < Instant > = vec! [ ] ;
let mut discovery = None ;
2021-02-21 18:38:49 +00:00
let mut connecting : Pin < Box < dyn future ::FusedFuture < Output = _ > > > = Box ::pin ( future ::pending ( ) ) ;
2021-02-21 10:08:34 +00:00
2021-04-10 13:08:39 +00:00
if setup . enable_discovery {
let device_id = setup . session_config . device_id . clone ( ) ;
2021-02-21 10:08:34 +00:00
discovery = Some (
2021-02-28 23:10:13 +00:00
librespot ::discovery ::Discovery ::builder ( device_id )
. name ( setup . connect_config . name . clone ( ) )
. device_type ( setup . connect_config . device_type )
. port ( setup . zeroconf_port )
. launch ( )
2021-02-21 10:08:34 +00:00
. unwrap ( ) ,
) ;
}
2021-04-10 13:08:39 +00:00
if let Some ( credentials ) = setup . credentials {
2021-02-21 10:08:34 +00:00
last_credentials = Some ( credentials . clone ( ) ) ;
connecting = Box ::pin (
Session ::connect (
2021-04-10 13:08:39 +00:00
setup . session_config . clone ( ) ,
2021-02-21 10:08:34 +00:00
credentials ,
2021-04-10 13:08:39 +00:00
setup . cache . clone ( ) ,
2021-02-21 10:08:34 +00:00
)
. fuse ( ) ,
) ;
}
loop {
tokio ::select! {
credentials = async { discovery . as_mut ( ) . unwrap ( ) . next ( ) . await } , if discovery . is_some ( ) = > {
match credentials {
Some ( credentials ) = > {
last_credentials = Some ( credentials . clone ( ) ) ;
auto_connect_times . clear ( ) ;
if let Some ( spirc ) = spirc . take ( ) {
spirc . shutdown ( ) ;
}
if let Some ( spirc_task ) = spirc_task . take ( ) {
// Continue shutdown in its own task
tokio ::spawn ( spirc_task ) ;
}
connecting = Box ::pin ( Session ::connect (
2021-04-10 13:08:39 +00:00
setup . session_config . clone ( ) ,
2021-02-21 10:08:34 +00:00
credentials ,
2021-04-10 13:08:39 +00:00
setup . cache . clone ( ) ,
2021-02-21 10:08:34 +00:00
) . fuse ( ) ) ;
} ,
None = > {
warn! ( " Discovery stopped! " ) ;
discovery = None ;
}
}
} ,
session = & mut connecting , if ! connecting . is_terminated ( ) = > match session {
Ok ( session ) = > {
2021-04-10 13:08:39 +00:00
let mixer_config = setup . mixer_config . clone ( ) ;
2021-05-24 13:53:32 +00:00
let mixer = ( setup . mixer ) ( mixer_config ) ;
2021-04-10 13:08:39 +00:00
let player_config = setup . player_config . clone ( ) ;
let connect_config = setup . connect_config . clone ( ) ;
2021-02-21 10:08:34 +00:00
let audio_filter = mixer . get_audio_filter ( ) ;
2021-04-10 13:08:39 +00:00
let format = setup . format ;
let backend = setup . backend ;
let device = setup . device . clone ( ) ;
2021-02-21 10:08:34 +00:00
let ( player , event_channel ) =
Player ::new ( player_config , session . clone ( ) , audio_filter , move | | {
2021-03-12 22:05:38 +00:00
( backend ) ( device , format )
2021-02-21 10:08:34 +00:00
} ) ;
2021-04-10 13:08:39 +00:00
if setup . emit_sink_events {
if let Some ( player_event_program ) = setup . player_event_program . clone ( ) {
2021-02-21 10:08:34 +00:00
player . set_sink_event_callback ( Some ( Box ::new ( move | sink_status | {
match emit_sink_event ( sink_status , & player_event_program ) {
Ok ( e ) if e . success ( ) = > ( ) ,
Ok ( e ) = > {
if let Some ( code ) = e . code ( ) {
2021-05-26 20:30:32 +00:00
warn! ( " Sink event program returned exit code {} " , code ) ;
2021-02-21 10:08:34 +00:00
} else {
2021-05-26 20:30:32 +00:00
warn! ( " Sink event program returned failure " ) ;
2021-02-21 10:08:34 +00:00
}
2021-05-26 20:30:32 +00:00
} ,
2021-02-21 10:08:34 +00:00
Err ( e ) = > {
warn! ( " Emitting sink event failed: {} " , e ) ;
2021-05-26 20:30:32 +00:00
} ,
2021-02-21 10:08:34 +00:00
}
} ) ) ) ;
}
} ;
let ( spirc_ , spirc_task_ ) = Spirc ::new ( connect_config , session , player , mixer ) ;
spirc = Some ( spirc_ ) ;
spirc_task = Some ( Box ::pin ( spirc_task_ ) ) ;
player_event_channel = Some ( event_channel ) ;
} ,
Err ( e ) = > {
warn! ( " Connection failed: {} " , e ) ;
}
} ,
_ = async { spirc_task . as_mut ( ) . unwrap ( ) . await } , if spirc_task . is_some ( ) = > {
spirc_task = None ;
warn! ( " Spirc shut down unexpectedly " ) ;
while ! auto_connect_times . is_empty ( )
& & ( ( Instant ::now ( ) - auto_connect_times [ 0 ] ) . as_secs ( ) > 600 )
{
let _ = auto_connect_times . remove ( 0 ) ;
}
if let Some ( credentials ) = last_credentials . clone ( ) {
if auto_connect_times . len ( ) > = 5 {
warn! ( " Spirc shut down too often. Not reconnecting automatically. " ) ;
} else {
auto_connect_times . push ( Instant ::now ( ) ) ;
connecting = Box ::pin ( Session ::connect (
2021-04-10 13:08:39 +00:00
setup . session_config . clone ( ) ,
2021-02-21 10:08:34 +00:00
credentials ,
2021-04-10 13:08:39 +00:00
setup . cache . clone ( ) ,
2021-02-21 10:08:34 +00:00
) . fuse ( ) ) ;
}
}
} ,
2021-02-21 18:38:40 +00:00
event = async { player_event_channel . as_mut ( ) . unwrap ( ) . recv ( ) . await } , if player_event_channel . is_some ( ) = > match event {
2021-02-21 10:08:34 +00:00
Some ( event ) = > {
2021-04-10 13:08:39 +00:00
if let Some ( program ) = & setup . player_event_program {
2021-02-21 10:08:34 +00:00
if let Some ( child ) = run_program_on_events ( event , program ) {
2021-03-31 20:13:36 +00:00
if child . is_ok ( ) {
2021-02-21 10:08:34 +00:00
2021-04-21 19:07:56 +00:00
let mut child = child . unwrap ( ) ;
tokio ::spawn ( async move {
match child . wait ( ) . await {
2021-05-26 20:30:32 +00:00
Ok ( e ) if e . success ( ) = > ( ) ,
Ok ( e ) = > {
if let Some ( code ) = e . code ( ) {
warn! ( " On event program returned exit code {} " , code ) ;
} else {
warn! ( " On event program returned failure " ) ;
}
} ,
Err ( e ) = > {
warn! ( " On event program failed: {} " , e ) ;
} ,
2021-04-21 19:07:56 +00:00
}
} ) ;
2021-03-31 20:13:36 +00:00
} else {
2021-05-26 20:30:32 +00:00
warn! ( " On event program failed to start " ) ;
2021-03-31 20:13:36 +00:00
}
2021-02-21 10:08:34 +00:00
}
}
} ,
None = > {
player_event_channel = None ;
}
} ,
_ = tokio ::signal ::ctrl_c ( ) = > {
break ;
}
}
}
info! ( " Gracefully shutting down " ) ;
// Shutdown spirc if necessary
if let Some ( spirc ) = spirc {
spirc . shutdown ( ) ;
if let Some ( mut spirc_task ) = spirc_task {
tokio ::select! {
_ = tokio ::signal ::ctrl_c ( ) = > ( ) ,
_ = spirc_task . as_mut ( ) = > ( )
}
}
}
}