commit 8fec560bcfdf44d520ef2ac3e11b7010941783ba Author: G2-Games Date: Fri Jan 19 15:37:19 2024 -0600 Restructured repository as a workspace diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4a63b78 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "dango-core" +version = "0.1.1" +edition = "2021" +license = "AGPL-3.0-only" +description = "A music backend that manages storage, querying, and playback of remote and local songs." +homepage = "https://dangoware.com/dango-music-player" +documentation = "https://docs.rs/dango-core" +readme = "README.md" +repository = "https://github.com/DangoWare/dango-music-player" +keywords = ["audio", "music"] +categories = ["multimedia::audio"] + +[dependencies] +file-format = { version = "0.17.3", features = ["reader", "serde"] } +lofty = "0.14.0" +rusqlite = { version = "0.29.0", features = ["bundled"] } +serde = { version = "1.0.164", features = ["derive"] } +time = "0.3.22" +toml = "0.7.5" +walkdir = "2.3.3" +cpal = "0.15.2" +heapless = "0.7.16" +rb = "0.4.1" +symphonia = { version = "0.5.3", features = ["all-codecs"] } +serde_json = "1.0.104" +cue = "2.0.0" +async-std = "1.12.0" +async-trait = "0.1.73" +md-5 = "0.10.5" +surf = "2.3.2" +futures = "0.3.28" +rubato = "0.12.0" +arrayvec = "0.7.4" diff --git a/src/bus_control.rs b/src/bus_control.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3a25b3d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,24 @@ +pub mod music_tracker { + pub mod music_tracker; +} + +pub mod music_storage { + pub mod music_db; + pub mod playlist; +} + +pub mod music_processor { + pub mod music_processor; +} + +pub mod music_player { + pub mod music_player; + pub mod music_output; + pub mod music_resampler; +} + +pub mod music_controller { + pub mod music_controller; + pub mod config; + pub mod init; +} \ No newline at end of file diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs new file mode 100644 index 0000000..70ee273 --- /dev/null +++ b/src/music_controller/config.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; +use std::fs::read_to_string; +use std::fs; + +use serde::{Deserialize, Serialize}; + +use crate::music_tracker::music_tracker::LastFM; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub db_path: Box, + pub lastfm: Option, +} + +impl Config { + // Creates and saves a new config with default values + pub fn new(config_file: &PathBuf) -> std::io::Result { + let path = PathBuf::from("./music_database.db3"); + + let config = Config { + db_path: Box::new(path), + lastfm: None, + }; + config.save(config_file)?; + + Ok(config) + } + + // Loads config from given file path + pub fn from(config_file: &PathBuf) -> std::result::Result { + return toml::from_str(&read_to_string(config_file) + .expect("Failed to initalize music config: File not found!")); + } + + // Saves config to given path + // Saves -> temp file, if successful, removes old config, and renames temp to given path + pub fn save(&self, config_file: &PathBuf) -> std::io::Result<()> { + let toml = toml::to_string_pretty(self).unwrap(); + + let mut temp_file = config_file.clone(); + temp_file.set_extension("tomltemp"); + + fs::write(&temp_file, toml)?; + + // If configuration file already exists, delete it + match fs::metadata(config_file) { + Ok(_) => fs::remove_file(config_file)?, + Err(_) => {}, + } + + fs::rename(temp_file, config_file)?; + Ok(()) + } +} diff --git a/src/music_controller/init.rs b/src/music_controller/init.rs new file mode 100644 index 0000000..80b91bf --- /dev/null +++ b/src/music_controller/init.rs @@ -0,0 +1,19 @@ +use std::path::Path; +use std::fs::File; + +pub fn init() { + +} + +fn init_config() { + let config_path = "./config.toml"; + + if !Path::new(config_path).try_exists().unwrap() { + File::create("./config.toml").unwrap(); + } +} + +fn init_db() { + +} + diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs new file mode 100644 index 0000000..bb1ab66 --- /dev/null +++ b/src/music_controller/music_controller.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use rusqlite::Result; + +use crate::music_controller::config::Config; +use crate::music_player::music_player::{MusicPlayer, PlayerStatus, PlayerMessage, DSPMessage}; +use crate::music_processor::music_processor::MusicProcessor; +use crate::music_storage::music_db::URI; + +pub struct MusicController { + pub config: Config, + music_player: MusicPlayer, +} + +impl MusicController { + /// Creates new MusicController with config at given path + pub fn new(config_path: &PathBuf) -> Result{ + let config = Config::new(config_path)?; + let music_player = MusicPlayer::new(); + + let controller = MusicController { + config, + music_player, + }; + + return Ok(controller) + } + + /// Creates new music controller from a config at given path + pub fn from(config_path: &PathBuf) -> std::result::Result { + let config = Config::from(config_path)?; + let music_player = MusicPlayer::new(); + + let controller = MusicController { + config, + music_player, + }; + + return Ok(controller) + } + + /// Opens and plays song at given URI + pub fn open_song(&mut self, uri: &URI) { + self.music_player.open_song(uri); + } + + /// Sends given message to music player + pub fn song_control(&mut self, message: PlayerMessage) { + self.music_player.send_message(message); + } + + /// Gets status of the music player + pub fn player_status(&mut self) -> PlayerStatus { + return self.music_player.get_status(); + } + + /// Gets audio playback volume + pub fn get_vol(&mut self) -> f32 { + return self.music_player.music_processor.audio_volume; + } + + /// Sets audio playback volume on a scale of 0.0 to 1.0 + pub fn set_vol(&mut self, volume: f32) { + self.music_player.music_processor.audio_volume = volume; + self.song_control(PlayerMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone())))); + } + +} diff --git a/src/music_player/music_output.rs b/src/music_player/music_output.rs new file mode 100644 index 0000000..b774b0f --- /dev/null +++ b/src/music_player/music_output.rs @@ -0,0 +1,167 @@ +use std::{result, thread}; + +use symphonia::core::audio::{AudioBufferRef, SignalSpec, RawSample, SampleBuffer}; +use symphonia::core::conv::{ConvertibleSample, IntoSample, FromSample}; +use symphonia::core::units::Duration; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{self, SizedSample}; + +use rb::*; + +use crate::music_player::music_resampler::Resampler; + +pub trait AudioStream { + fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()>; + fn flush(&mut self); +} + +#[derive(Debug)] +pub enum AudioOutputError { + OpenStreamError, + PlayStreamError, + StreamClosedError, +} + +pub type Result = result::Result; + +pub trait OutputSample: SizedSample + FromSample + IntoSample +cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static {} + +pub struct AudioOutput +where T: OutputSample, +{ + ring_buf_producer: rb::Producer, + sample_buf: SampleBuffer, + stream: cpal::Stream, + resampler: Option>, +} +impl OutputSample for i8 {} +impl OutputSample for i16 {} +impl OutputSample for i32 {} +//impl OutputSample for i64 {} +impl OutputSample for u8 {} +impl OutputSample for u16 {} +impl OutputSample for u32 {} +//impl OutputSample for u64 {} +impl OutputSample for f32 {} +impl OutputSample for f64 {} +//create a new trait with functions, then impl that somehow + +pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result> { + let host = cpal::default_host(); + + // Uses default audio device + let device = match host.default_output_device() { + Some(device) => device, + _ => return Err(AudioOutputError::OpenStreamError), + }; + + let config = match device.default_output_config() { + Ok(config) => config, + Err(err) => return Err(AudioOutputError::OpenStreamError), + }; + + return match config.sample_format(){ + cpal::SampleFormat::I8 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::I16 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::I32 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + //cpal::SampleFormat::I64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U8 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U16 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U32 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + //cpal::SampleFormat::U64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::F32 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::F64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + _ => todo!(), + }; + } + + +impl AudioOutput { + // Creates the stream (TODO: Merge w/open_stream?) + fn create_stream(spec: SignalSpec, device: &cpal::Device, config: &cpal::StreamConfig, duration: Duration) -> Result> { + let num_channels = config.channels as usize; + + // Ring buffer is created with 200ms audio capacity + let ring_len = ((200 * config.sample_rate.0 as usize) / 1000) * num_channels; + let ring_buf= rb::SpscRb::new(ring_len); + + let ring_buf_producer = ring_buf.producer(); + let ring_buf_consumer = ring_buf.consumer(); + + let stream_result = device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + // Writes samples in the ring buffer to the audio output + let written = ring_buf_consumer.read(data).unwrap_or(0); + + // Mutes non-written samples + data[written..].iter_mut().for_each(|sample| *sample = T::MID); + }, + //TODO: Handle error here properly + move |err| println!("Yeah we erroring out here"), + None + ); + + if let Err(err) = stream_result { + return Err(AudioOutputError::OpenStreamError); + } + + let stream = stream_result.unwrap(); + + //Start output stream + if let Err(err) = stream.play() { + return Err(AudioOutputError::PlayStreamError); + } + + let sample_buf = SampleBuffer::::new(duration, spec); + + let mut resampler = None; + if spec.rate != config.sample_rate.0 { + println!("Resampling enabled"); + resampler = Some(Resampler::new(spec, config.sample_rate.0 as usize, duration)) + } + + Ok(Box::new(AudioOutput { ring_buf_producer, sample_buf, stream, resampler})) + } +} + +impl AudioStream for AudioOutput { + // Writes given samples to ring buffer + fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()> { + if decoded.frames() == 0 { + return Ok(()); + } + + let mut samples: &[T] = if let Some(resampler) = &mut self.resampler { + // Resamples if required + match resampler.resample(decoded) { + Some(resampled) => resampled, + None => return Ok(()), + } + } else { + self.sample_buf.copy_interleaved_ref(decoded); + self.sample_buf.samples() + }; + + // Write samples into ring buffer + while let Some(written) = self.ring_buf_producer.write_blocking(samples) { + samples = &samples[written..]; + } + + Ok(()) + } + + // Flushes resampler if needed + fn flush(&mut self) { + if let Some(resampler) = &mut self.resampler { + let mut stale_samples = resampler.flush().unwrap_or_default(); + + while let Some(written) = self.ring_buf_producer.write_blocking(stale_samples) { + stale_samples = &stale_samples[written..]; + } + } + + let _ = self.stream.pause(); + } +} \ No newline at end of file diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs new file mode 100644 index 0000000..1996240 --- /dev/null +++ b/src/music_player/music_player.rs @@ -0,0 +1,364 @@ +use std::sync::mpsc::{self, Sender, Receiver}; +use std::thread; +use std::io::SeekFrom; + +use async_std::io::ReadExt; +use async_std::task; + +use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions, Decoder}; +use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo}; +use symphonia::core::io::{MediaSourceStream, MediaSource}; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; +use symphonia::core::errors::Error; +use symphonia::core::units::Time; + +use futures::AsyncBufRead; + +use crate::music_player::music_output::AudioStream; +use crate::music_processor::music_processor::MusicProcessor; +use crate::music_storage::music_db::URI; + +// Struct that controls playback of music +pub struct MusicPlayer { + pub music_processor: MusicProcessor, + player_status: PlayerStatus, + message_sender: Option>, + status_receiver: Option>, +} + +#[derive(Clone, Copy)] +pub enum PlayerStatus { + Playing, + Paused, + Stopped, + Error, +} + +pub enum PlayerMessage { + Play, + Pause, + Stop, + SeekTo(u64), + DSP(DSPMessage) +} + +pub enum DSPMessage { + UpdateProcessor(Box) +} + +impl MusicPlayer { + pub fn new() -> Self { + MusicPlayer { + music_processor: MusicProcessor::new(), + player_status: PlayerStatus::Stopped, + message_sender: None, + status_receiver: None, + } + } + + // Opens and plays song with given path in separate thread + pub fn open_song(&mut self, uri: &URI) { + // Creates mpsc channels to communicate with thread + let (message_sender, message_receiver) = mpsc::channel(); + let (status_sender, status_receiver) = mpsc::channel(); + self.message_sender = Some(message_sender); + self.status_receiver = Some(status_receiver); + + let owned_uri = uri.clone(); + + // Creates thread that audio is decoded in + thread::spawn(move || { + let (mut reader, mut decoder) = MusicPlayer::get_reader_and_dec(&owned_uri); + + let mut seek_time: Option = None; + + let mut audio_output: Option> = None; + + let mut music_processor = MusicProcessor::new(); + + 'main_decode: loop { + // Handles message received from the MusicPlayer if there is one // TODO: Refactor + let received_message = message_receiver.try_recv(); + match received_message { + Ok(PlayerMessage::Pause) => { + status_sender.send(PlayerStatus::Paused).unwrap(); + // Loops on a blocking message receiver to wait for a play/stop message + 'inner_pause: loop { + let message = message_receiver.try_recv(); + match message { + Ok(PlayerMessage::Play) => { + status_sender.send(PlayerStatus::Playing).unwrap(); + break 'inner_pause + }, + Ok(PlayerMessage::Stop) => { + status_sender.send(PlayerStatus::Stopped).unwrap(); + break 'main_decode + }, + _ => {}, + } + } + }, + // Exits main decode loop and subsequently ends thread (?) + Ok(PlayerMessage::Stop) => { + status_sender.send(PlayerStatus::Stopped).unwrap(); + break 'main_decode + }, + Ok(PlayerMessage::SeekTo(time)) => seek_time = Some(time), + Ok(PlayerMessage::DSP(dsp_message)) => { + match dsp_message { + DSPMessage::UpdateProcessor(new_processor) => music_processor = *new_processor, + } + } + _ => {}, + } + + match seek_time { + Some(time) => { + let seek_to = SeekTo::Time { time: Time::from(time), track_id: Some(0) }; + reader.seek(SeekMode::Accurate, seek_to).unwrap(); + seek_time = None; + } + None => {} //Nothing to do! + } + + let packet = match reader.next_packet() { + Ok(packet) => packet, + Err(Error::ResetRequired) => panic!(), //TODO, + Err(err) => { + //Unrecoverable? + panic!("{}", err); + } + }; + + match decoder.decode(&packet) { + Ok(decoded) => { + // Opens audio stream if there is not one + if audio_output.is_none() { + let spec = *decoded.spec(); + let duration = decoded.capacity() as u64; + + audio_output.replace(crate::music_player::music_output::open_stream(spec, duration).unwrap()); + } + + // Handles audio normally provided there is an audio stream + if let Some(ref mut audio_output) = audio_output { + // Changes buffer of the MusicProcessor if the packet has a differing capacity or spec + if music_processor.audio_buffer.capacity() != decoded.capacity() ||music_processor.audio_buffer.spec() != decoded.spec() { + let spec = *decoded.spec(); + let duration = decoded.capacity() as u64; + + music_processor.set_buffer(duration, spec); + } + + let transformed_audio = music_processor.process(&decoded); + + // Writes transformed packet to audio out + audio_output.write(transformed_audio).unwrap() + } + }, + Err(Error::IoError(_)) => { + // rest in peace packet + continue; + }, + Err(Error::DecodeError(_)) => { + // may you one day be decoded + continue; + }, + Err(err) => { + // Unrecoverable, though shouldn't panic here + panic!("{}", err); + } + } + } + }); + } + + fn get_reader_and_dec(uri: &URI) -> (Box, Box) { + // Opens remote/local source and creates MediaSource for symphonia + let config = RemoteOptions { media_buffer_len: 10000, forward_buffer_len: 10000}; + let src: Box = match uri { + URI::Local(path) => Box::new(std::fs::File::open(path).expect("Failed to open file")), + URI::Remote(_, location) => Box::new(RemoteSource::new(location.as_ref(), &config).unwrap()), + }; + + let mss = MediaSourceStream::new(src, Default::default()); + + // Use default metadata and format options + let meta_opts: MetadataOptions = Default::default(); + let fmt_opts: FormatOptions = Default::default(); + + let mut hint = Hint::new(); + + let probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &meta_opts).expect("Unsupported format"); + + let mut reader = probed.format; + + let track = reader.tracks() + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no supported audio tracks"); + + let dec_opts: DecoderOptions = Default::default(); + + let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) + .expect("unsupported codec"); + + return (reader, decoder); + } + + // Updates status by checking on messages from spawned thread + fn update_status(&mut self) { + let status = self.status_receiver.as_mut().unwrap().try_recv(); + if status.is_ok() { + self.player_status = status.unwrap(); + match status.unwrap() { + // Removes receiver and sender since spawned thread no longer exists + PlayerStatus::Stopped => { + self.status_receiver = None; + self.message_sender = None; + } + _ => {} + } + } + } + + // Sends message to spawned thread + pub fn send_message(&mut self, message: PlayerMessage) { + self.update_status(); + // Checks that message sender exists before sending a message off + if self.message_sender.is_some() { + self.message_sender.as_mut().unwrap().send(message).unwrap(); + } + } + + pub fn get_status(&mut self) -> PlayerStatus { + self.update_status(); + return self.player_status; + } +} + +// TODO: Make the buffer length do anything +/// Options for remote sources +/// +/// media_buffer_len is how many bytes are to be buffered in totala +/// +/// forward_buffer is how many bytes can ahead of the seek position without the remote source being read from +pub struct RemoteOptions { + media_buffer_len: u64, + forward_buffer_len: u64, +} + +impl Default for RemoteOptions { + fn default() -> Self { + RemoteOptions { + media_buffer_len: 100000, + forward_buffer_len: 1024, + } + } +} + +/// A remote source of media +struct RemoteSource { + reader: Box, + media_buffer: Vec, + forward_buffer_len: u64, + offset: u64, +} + +impl RemoteSource { + /// Creates a new RemoteSource with given uri and configuration + pub fn new(uri: &str, config: &RemoteOptions) -> Result { + let mut response = task::block_on(async { + return surf::get(uri).await; + })?; + + let reader = response.take_body().into_reader(); + + Ok(RemoteSource { + reader, + media_buffer: Vec::new(), + forward_buffer_len: config.forward_buffer_len, + offset: 0, + }) + } +} +// TODO: refactor this + buffer into the buffer passed into the function, not a newly allocated one +impl std::io::Read for RemoteSource { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + // Reads bytes into the media buffer if the offset is within the specified distance from the end of the buffer + if self.media_buffer.len() as u64 - self.offset < self.forward_buffer_len { + let mut buffer = [0; 1024]; + let read_bytes = task::block_on(async { + match self.reader.read_exact(&mut buffer).await { + Ok(_) => { + self.media_buffer.extend_from_slice(&buffer); + return Ok(()); + }, + Err(err) => return Err(err), + } + }); + match read_bytes { + Err(err) => return Err(err), + _ => {}, + } + } + // Reads bytes from the media buffer into the buffer given by + let mut bytes_read = 0; + for location in 0..1024 { + if (location + self.offset as usize) < self.media_buffer.len() { + buf[location] = self.media_buffer[location + self.offset as usize]; + bytes_read += 1; + } + } + + self.offset += bytes_read; + return Ok(bytes_read as usize); + } +} + +impl std::io::Seek for RemoteSource { + // Seeks to a given position + // Seeking past the internal buffer's length results in the seeking to the end of content + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + match pos { + // Offset is set to given position + SeekFrom::Start(pos) => { + if pos > self.media_buffer.len() as u64{ + self.offset = self.media_buffer.len() as u64; + } else { + self.offset = pos; + } + return Ok(self.offset); + }, + // Offset is set to length of buffer + given position + SeekFrom::End(pos) => { + if self.media_buffer.len() as u64 + pos as u64 > self.media_buffer.len() as u64 { + self.offset = self.media_buffer.len() as u64; + } else { + self.offset = self.media_buffer.len() as u64 + pos as u64; + } + return Ok(self.offset); + }, + // Offset is set to current offset + given position + SeekFrom::Current(pos) => { + if self.offset + pos as u64 > self.media_buffer.len() as u64{ + self.offset = self.media_buffer.len() as u64; + } else { + self.offset += pos as u64 + } + return Ok(self.offset); + }, + } + } +} + +impl MediaSource for RemoteSource { + fn is_seekable(&self) -> bool { + return true; + } + + fn byte_len(&self) -> Option { + return None; + } +} \ No newline at end of file diff --git a/src/music_player/music_resampler.rs b/src/music_player/music_resampler.rs new file mode 100644 index 0000000..f654a17 --- /dev/null +++ b/src/music_player/music_resampler.rs @@ -0,0 +1,147 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, SignalSpec}; +use symphonia::core::conv::{FromSample, IntoSample}; +use symphonia::core::sample::Sample; + +pub struct Resampler { + resampler: rubato::FftFixedIn, + input: Vec>, + output: Vec>, + interleaved: Vec, + duration: usize, +} + +impl Resampler +where + T: Sample + FromSample + IntoSample, +{ + fn resample_inner(&mut self) -> &[T] { + { + //let mut input = heapless::Vec::::new(); + let mut input: arrayvec::ArrayVec<&[f32], 32> = Default::default(); + + for channel in self.input.iter() { + input.push(&channel[..self.duration]); + } + + // Resample. + rubato::Resampler::process_into_buffer( + &mut self.resampler, + &input, + &mut self.output, + None, + ) + .unwrap(); + } + + // Remove consumed samples from the input buffer. + for channel in self.input.iter_mut() { + channel.drain(0..self.duration); + } + + // Interleave the planar samples from Rubato. + let num_channels = self.output.len(); + + self.interleaved.resize(num_channels * self.output[0].len(), T::MID); + + for (i, frame) in self.interleaved.chunks_exact_mut(num_channels).enumerate() { + for (ch, s) in frame.iter_mut().enumerate() { + *s = self.output[ch][i].into_sample(); + } + } + + &self.interleaved + } +} + +impl Resampler +where + T: Sample + FromSample + IntoSample, +{ + pub fn new(spec: SignalSpec, to_sample_rate: usize, duration: u64) -> Self { + let duration = duration as usize; + let num_channels = spec.channels.count(); + + let resampler = rubato::FftFixedIn::::new( + spec.rate as usize, + to_sample_rate, + duration, + 2, + num_channels, + ) + .unwrap(); + + let output = rubato::Resampler::output_buffer_allocate(&resampler); + + let input = vec![Vec::with_capacity(duration); num_channels]; + + Self { resampler, input, output, duration, interleaved: Default::default() } + } + + /// Resamples a planar/non-interleaved input. + /// + /// Returns the resampled samples in an interleaved format. + pub fn resample(&mut self, input: AudioBufferRef<'_>) -> Option<&[T]> { + // Copy and convert samples into input buffer. + convert_samples_any(&input, &mut self.input); + + // Check if more samples are required. + if self.input[0].len() < self.duration { + return None; + } + + Some(self.resample_inner()) + } + + /// Resample any remaining samples in the resample buffer. + pub fn flush(&mut self) -> Option<&[T]> { + let len = self.input[0].len(); + + if len == 0 { + return None; + } + + let partial_len = len % self.duration; + + if partial_len != 0 { + // Fill each input channel buffer with silence to the next multiple of the resampler + // duration. + for channel in self.input.iter_mut() { + channel.resize(len + (self.duration - partial_len), f32::MID); + } + } + + Some(self.resample_inner()) + } +} + +fn convert_samples_any(input: &AudioBufferRef<'_>, output: &mut [Vec]) { + match input { + AudioBufferRef::U8(input) => convert_samples(input, output), + AudioBufferRef::U16(input) => convert_samples(input, output), + AudioBufferRef::U24(input) => convert_samples(input, output), + AudioBufferRef::U32(input) => convert_samples(input, output), + AudioBufferRef::S8(input) => convert_samples(input, output), + AudioBufferRef::S16(input) => convert_samples(input, output), + AudioBufferRef::S24(input) => convert_samples(input, output), + AudioBufferRef::S32(input) => convert_samples(input, output), + AudioBufferRef::F32(input) => convert_samples(input, output), + AudioBufferRef::F64(input) => convert_samples(input, output), + } +} + +fn convert_samples(input: &AudioBuffer, output: &mut [Vec]) +where + S: Sample + IntoSample, +{ + for (c, dst) in output.iter_mut().enumerate() { + let src = input.chan(c); + dst.extend(src.iter().map(|&s| s.into_sample())); + } +} \ No newline at end of file diff --git a/src/music_processor/music_processor.rs b/src/music_processor/music_processor.rs new file mode 100644 index 0000000..dd7effc --- /dev/null +++ b/src/music_processor/music_processor.rs @@ -0,0 +1,35 @@ +use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec}; + +#[derive(Clone)] +pub struct MusicProcessor { + pub audio_buffer: AudioBuffer, + pub audio_volume: f32, +} + +impl MusicProcessor { + /// Returns new MusicProcessor with blank buffer and 100% volume + pub fn new() -> Self { + MusicProcessor { + audio_buffer: AudioBuffer::unused(), + audio_volume: 1.0, + } + } + + /// Processes audio samples + /// + /// Currently only supports transformations of volume + pub fn process(&mut self, audio_buffer_ref: &AudioBufferRef) -> AudioBufferRef { + audio_buffer_ref.convert(&mut self.audio_buffer); + + let process = |sample| sample * self.audio_volume; + + self.audio_buffer.transform(process); + + return self.audio_buffer.as_audio_buffer_ref(); + } + + /// Sets buffer of the MusicProcessor + pub fn set_buffer(&mut self, duration: u64, spec: SignalSpec) { + self.audio_buffer = AudioBuffer::new(duration, spec); + } +} \ No newline at end of file diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs new file mode 100644 index 0000000..72d3053 --- /dev/null +++ b/src/music_storage/music_db.rs @@ -0,0 +1,456 @@ +use file_format::{FileFormat, Kind}; +use serde::Deserialize; +use lofty::{Accessor, AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; +use rusqlite::{params, Connection}; +use cue::{cd_text::PTI, cd::CD}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use time::Date; +use walkdir::WalkDir; + +use crate::music_controller::config::Config; + +#[derive(Debug)] +pub struct Song { + pub path: Box, + pub title: Option, + pub album: Option, + tracknum: Option, + pub artist: Option, + date: Option, + genre: Option, + plays: Option, + favorited: Option, + format: Option, // TODO: Make this a proper FileFormat eventually + duration: Option, + pub custom_tags: Option>, +} +#[derive(Clone)] +pub enum URI{ + Local(String), + Remote(Service, String), +} + +#[derive(Clone, Copy)] +pub enum Service { + InternetRadio, + Spotify, + Youtube, +} + +#[derive(Debug)] +pub struct Playlist { + title: String, + cover_art: Box, +} + +pub fn create_db() -> Result<(), rusqlite::Error> { + let path = "./music_database.db3"; + let db_connection = Connection::open(path)?; + + db_connection.pragma_update(None, "synchronous", "0")?; + db_connection.pragma_update(None, "journal_mode", "WAL")?; + + // Create the important tables + db_connection.execute( + "CREATE TABLE music_collection ( + song_path TEXT PRIMARY KEY, + title TEXT, + album TEXT, + tracknum INTEGER, + artist TEXT, + date INTEGER, + genre TEXT, + plays INTEGER, + favorited BLOB, + format TEXT, + duration INTEGER + )", + (), // empty list of parameters. + )?; + + db_connection.execute( + "CREATE TABLE playlists ( + playlist_name TEXT NOT NULL, + song_path TEXT NOT NULL, + FOREIGN KEY(song_path) REFERENCES music_collection(song_path) + )", + (), // empty list of parameters. + )?; + + db_connection.execute( + "CREATE TABLE custom_tags ( + song_path TEXT NOT NULL, + tag TEXT NOT NULL, + tag_value TEXT, + FOREIGN KEY(song_path) REFERENCES music_collection(song_path) + )", + (), // empty list of parameters. + )?; + + Ok(()) +} + +fn path_in_db(query_path: &Path, connection: &Connection) -> bool { + let query_string = format!("SELECT EXISTS(SELECT 1 FROM music_collection WHERE song_path='{}')", query_path.to_string_lossy()); + + let mut query_statement = connection.prepare(&query_string).unwrap(); + let mut rows = query_statement.query([]).unwrap(); + + match rows.next().unwrap() { + Some(value) => value.get::(0).unwrap(), + None => false + } +} + +/// Parse a cuesheet given a path and a directory it is located in, +/// returning a Vec of Song objects +fn parse_cuesheet( + cuesheet_path: &Path, + current_dir: &PathBuf +) -> Result, Box>{ + let cuesheet = CD::parse_file(cuesheet_path.to_path_buf())?; + + let album = cuesheet.get_cdtext().read(PTI::Title); + + let mut song_list:Vec = vec![]; + + for (index, track) in cuesheet.tracks().iter().enumerate() { + let track_string_path = format!("{}/{}", current_dir.to_string_lossy(), track.get_filename()); + let track_path = Path::new(&track_string_path); + + if !track_path.exists() {continue}; + + // Get the format as a string + let short_format = match FileFormat::from_file(track_path) { + Ok(fmt) => Some(fmt), + Err(_) => None + }; + + let duration = Duration::from_secs(track.get_length().unwrap_or(-1) as u64); + + let custom_index_start = Tag::Custom{ + tag: String::from("dango_cue_index_start"), + tag_value: track.get_index(0).unwrap_or(-1).to_string() + }; + let custom_index_end = Tag::Custom{ + tag: String::from("dango_cue_index_end"), + tag_value: track.get_index(0).unwrap_or(-1).to_string() + }; + + let custom_tags: Vec = vec![custom_index_start, custom_index_end]; + + let tags = track.get_cdtext(); + let cue_song = Song { + path: track_path.into(), + title: tags.read(PTI::Title), + album: album.clone(), + tracknum: Some(index + 1), + artist: tags.read(PTI::Performer), + date: None, + genre: tags.read(PTI::Genre), + plays: Some(0), + favorited: Some(false), + format: short_format, + duration: Some(duration), + custom_tags: Some(custom_tags) + }; + + song_list.push(cue_song); + } + + Ok(song_list) +} + +pub fn find_all_music( + config: &Config, + target_path: &str, +) -> Result<(), Box> { + let db_connection = Connection::open(&*config.db_path)?; + + db_connection.pragma_update(None, "synchronous", "0")?; + db_connection.pragma_update(None, "journal_mode", "WAL")?; + + let mut current_dir = PathBuf::new(); + for entry in WalkDir::new(target_path).follow_links(true).into_iter().filter_map(|e| e.ok()) { + let target_file = entry; + let is_file = fs::metadata(target_file.path())?.is_file(); + + // Ensure the target is a file and not a directory, if it isn't, skip this loop + if !is_file { + current_dir = target_file.into_path(); + continue; + } + + let format = FileFormat::from_file(target_file.path())?; + let extension = target_file + .path() + .extension() + .expect("Could not find file extension"); + + // If it's a normal file, add it to the database + // if it's a cuesheet, do a bunch of fancy stuff + if format.kind() == Kind::Audio { + add_file_to_db(target_file.path(), &db_connection) + } else if extension.to_ascii_lowercase() == "cue" { + // TODO: implement cuesheet support + parse_cuesheet(target_file.path(), ¤t_dir); + } + } + + // create the indexes after all the data is inserted + db_connection.execute( + "CREATE INDEX path_index ON music_collection (song_path)", () + )?; + + db_connection.execute( + "CREATE INDEX custom_tags_index ON custom_tags (song_path)", () + )?; + + Ok(()) +} + +pub fn add_file_to_db(target_file: &Path, connection: &Connection) { + // TODO: Fix error handling here + let tagged_file = match lofty::read_from_path(target_file) { + Ok(tagged_file) => tagged_file, + + Err(_) => match Probe::open(target_file) + .expect("ERROR: Bad path provided!") + .read() { + Ok(tagged_file) => tagged_file, + + Err(_) => return + } + }; + + // Ensure the tags exist, if not, insert blank data + let blank_tag = &lofty::Tag::new(TagType::Id3v2); + let tag = match tagged_file.primary_tag() { + Some(primary_tag) => primary_tag, + + None => match tagged_file.first_tag() { + Some(first_tag) => first_tag, + None => blank_tag + }, + }; + + let mut custom_insert = String::new(); + let mut loops = 0; + for item in tag.items() { + let mut custom_key = String::new(); + match item.key() { + ItemKey::TrackArtist | + ItemKey::TrackTitle | + ItemKey::AlbumTitle | + ItemKey::Genre | + ItemKey::TrackNumber | + ItemKey::Year | + ItemKey::RecordingDate => continue, + ItemKey::Unknown(unknown) => custom_key.push_str(&unknown), + custom => custom_key.push_str(&format!("{:?}", custom)) + // TODO: This is kind of cursed, maybe fix? + }; + + let custom_value = match item.value() { + ItemValue::Text(value) => value, + ItemValue::Locator(value) => value, + ItemValue::Binary(_) => "" + }; + + if loops > 0 { + custom_insert.push_str(", "); + } + + custom_insert.push_str(&format!(" (?1, '{}', '{}')", custom_key.replace("\'", "''"), custom_value.replace("\'", "''"))); + + loops += 1; + } + + // Get the format as a string + let short_format: Option = match FileFormat::from_file(target_file) { + Ok(fmt) => Some(fmt.to_string()), + Err(_) => None + }; + + println!("{}", short_format.as_ref().unwrap()); + + let duration = tagged_file.properties().duration().as_secs().to_string(); + + // TODO: Fix error handling + let binding = fs::canonicalize(target_file).unwrap(); + let abs_path = binding.to_str().unwrap(); + + // Add all the info into the music_collection table + connection.execute( + "INSERT INTO music_collection ( + song_path, + title, + album, + tracknum, + artist, + date, + genre, + plays, + favorited, + format, + duration + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![abs_path, tag.title(), tag.album(), tag.track(), tag.artist(), tag.year(), tag.genre(), 0, false, short_format, duration], + ).unwrap(); + + //TODO: Fix this, it's horrible + if custom_insert != "" { + connection.execute( + &format!("INSERT INTO custom_tags ('song_path', 'tag', 'tag_value') VALUES {}", &custom_insert), + params![ + abs_path, + ] + ).unwrap(); + } +} + +#[derive(Debug, Deserialize)] +pub enum Tag { + SongPath, + Title, + Album, + TrackNum, + Artist, + Date, + Genre, + Plays, + Favorited, + Format, + Duration, + Custom{tag: String, tag_value: String}, +} + +impl Tag { + fn as_str(&self) -> &str { + match self { + Tag::SongPath => "song_path", + Tag::Title => "title", + Tag::Album => "album", + Tag::TrackNum => "tracknum", + Tag::Artist => "artist", + Tag::Date => "date", + Tag::Genre => "genre", + Tag::Plays => "plays", + Tag::Favorited => "favorited", + Tag::Format => "format", + Tag::Duration => "duration", + Tag::Custom{tag, ..} => tag, + } + } +} + +#[derive(Debug)] +pub enum MusicObject { + Song(Song), + Album(Playlist), + Playlist(Playlist), +} + +impl MusicObject { + pub fn as_song(&self) -> Option<&Song> { + match self { + MusicObject::Song(data) => Some(data), + _ => None + } + } +} + +/// Query the database, returning a list of items +pub fn query( + config: &Config, + text_input: &String, + queried_tags: &Vec<&Tag>, + order_by_tags: &Vec<&Tag>, +) -> Option> { + let db_connection = Connection::open(&*config.db_path).unwrap(); + + // Set up some database settings + db_connection.pragma_update(None, "synchronous", "0").unwrap(); + db_connection.pragma_update(None, "journal_mode", "WAL").unwrap(); + + // Build the "WHERE" part of the SQLite query + let mut where_string = String::new(); + let mut loops = 0; + for tag in queried_tags { + if loops > 0 { + where_string.push_str("OR "); + } + + match tag { + Tag::Custom{tag, ..} => where_string.push_str(&format!("custom_tags.tag = '{tag}' AND custom_tags.tag_value LIKE '{text_input}' ")), + Tag::SongPath => where_string.push_str(&format!("music_collection.{} LIKE '{text_input}' ", tag.as_str())), + _ => where_string.push_str(&format!("{} LIKE '{text_input}' ", tag.as_str())) + } + + loops += 1; + } + + // Build the "ORDER BY" part of the SQLite query + let mut order_by_string = String::new(); + let mut loops = 0; + for tag in order_by_tags { + match tag { + Tag::Custom{..} => continue, + _ => () + } + + if loops > 0 { + order_by_string.push_str(", "); + } + + order_by_string.push_str(tag.as_str()); + + loops += 1; + } + + // Build the final query string + let query_string = format!(" + SELECT music_collection.*, JSON_GROUP_ARRAY(JSON_OBJECT('Custom',JSON_OBJECT('tag', custom_tags.tag, 'tag_value', custom_tags.tag_value))) AS custom_tags + FROM music_collection + LEFT JOIN custom_tags ON music_collection.song_path = custom_tags.song_path + WHERE {where_string} + GROUP BY music_collection.song_path + ORDER BY {order_by_string} + "); + + let mut query_statement = db_connection.prepare(&query_string).unwrap(); + let mut rows = query_statement.query([]).unwrap(); + + let mut final_result:Vec = vec![]; + + while let Some(row) = rows.next().unwrap() { + let custom_tags: Vec = match row.get::(11) { + Ok(result) => serde_json::from_str(&result).unwrap_or(vec![]), + Err(_) => vec![] + }; + + let file_format: FileFormat = FileFormat::from(row.get::(9).unwrap().as_bytes()); + + let new_song = Song { + // TODO: Implement proper errors here + path: Path::new(&row.get::(0).unwrap_or("".to_owned())).into(), + title: row.get::(1).ok(), + album: row.get::(2).ok(), + tracknum: row.get::(3).ok(), + artist: row.get::(4).ok(), + date: Date::from_calendar_date(row.get::(5).unwrap_or(0), time::Month::January, 1).ok(), // TODO: Fix this to get the actual date + genre: row.get::(6).ok(), + plays: row.get::(7).ok(), + favorited: row.get::(8).ok(), + format: Some(file_format), + duration: Some(Duration::from_secs(row.get::(10).unwrap_or(0))), + custom_tags: Some(custom_tags), + }; + + final_result.push(MusicObject::Song(new_song)); + }; + + Some(final_result) +} diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs new file mode 100644 index 0000000..f6fc9b7 --- /dev/null +++ b/src/music_storage/playlist.rs @@ -0,0 +1,27 @@ +use std::path::Path; +use crate::music_controller::config::Config; +use rusqlite::{params, Connection}; + +pub fn playlist_add( + config: &Config, + playlist_name: &str, + song_paths: &Vec<&Path> +) { + let db_connection = Connection::open(&*config.db_path).unwrap(); + + for song_path in song_paths { + db_connection.execute( + "INSERT INTO playlists ( + playlist_name, + song_path + ) VALUES ( + ?1, + ?2 + )", + params![ + playlist_name, + song_path.to_str().unwrap() + ], + ).unwrap(); + } +} diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs new file mode 100644 index 0000000..93fb5fb --- /dev/null +++ b/src/music_tracker/music_tracker.rs @@ -0,0 +1,186 @@ +use std::time::{SystemTime, UNIX_EPOCH}; +use std::collections::BTreeMap; + +use async_trait::async_trait; +use serde::{Serialize, Deserialize}; +use md5::{Md5, Digest}; + +#[async_trait] +pub trait MusicTracker { + /// Adds one listen to a song halfway through playback + async fn track_song(&self, song: &String) -> Result<(), surf::Error>; + + /// Adds a 'listening' status to the music tracker service of choice + async fn track_now(&self, song: &String) -> Result<(), surf::Error>; + + /// Reads config files, and attempts authentication with service + async fn test_tracker(&self) -> Result<(), surf::Error>; + + /// Returns plays for a given song according to tracker service + async fn get_times_tracked(&self, song: &String) -> Result; +} + +#[derive(Serialize, Deserialize)] +pub struct LastFM { + dango_api_key: String, + auth_token: Option, + shared_secret: Option, + session_key: Option, +} + +#[async_trait] +impl MusicTracker for LastFM { + async fn track_song(&self, song: &String) -> Result<(), surf::Error> { + let mut params: BTreeMap<&str, &str> = BTreeMap::new(); + + // Sets timestamp of song beginning play time + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; + let string_timestamp = timestamp.to_string(); + params.insert("method", "track.scrobble"); + params.insert("artist", "Kikuo"); + params.insert("track", "A Happy Death - Again"); + params.insert("timestamp", &string_timestamp); + + self.api_request(params).await?; + Ok(()) + } + + async fn track_now(&self, song: &String) -> Result<(), surf::Error> { + let mut params: BTreeMap<&str, &str> = BTreeMap::new(); + params.insert("method", "track.updateNowPlaying"); + params.insert("artist", "Kikuo"); + params.insert("track", "A Happy Death - Again"); + self.api_request(params).await?; + Ok(()) + } + + async fn test_tracker(&self) -> Result<(), surf::Error> { + let mut params: BTreeMap<&str, &str> = BTreeMap::new(); + params.insert("method", "chart.getTopArtists"); + self.api_request(params).await?; + Ok(()) + } + + async fn get_times_tracked(&self, song: &String) -> Result { + todo!(); + } +} + +#[derive(Deserialize, Serialize)] +struct AuthToken { + token: String +} + +#[derive(Deserialize, Serialize, Debug)] +struct SessionResponse { + name: String, + key: String, + subscriber: i32, +} + +#[derive(Deserialize, Serialize, Debug)] +struct Session { + session: SessionResponse +} + +impl LastFM { + // Returns a url to be accessed by the user + pub async fn get_auth_url(&mut self) -> Result { + let method = String::from("auth.gettoken"); + let api_key = self.dango_api_key.clone(); + let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&format=json"); + + let auth_token: AuthToken = surf::get(api_request_url).await?.body_json().await?; + self.auth_token = Some(auth_token.token.clone()); + + let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token); + + return Ok(auth_url); + } + + pub async fn set_session(&mut self) { + let method = String::from("auth.getSession"); + let api_key = self.dango_api_key.clone(); + let auth_token = self.auth_token.clone().unwrap(); + let shared_secret = self.shared_secret.clone().unwrap(); + + // Creates api_sig as defined in last.fm documentation + let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}"); + + // Creates insecure MD5 hash for last.fm api sig + let mut hasher = Md5::new(); + hasher.update(api_sig); + let hash_result = hasher.finalize(); + let hex_string_hash = format!("{:#02x}", hash_result); + + let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&token={auth_token}&api_sig={hex_string_hash}&format=json"); + + let response = surf::get(api_request_url).recv_string().await.unwrap(); + + // Sets session key from received response + let session_response: Session = serde_json::from_str(&response).unwrap(); + self.session_key = Some(session_response.session.key.clone()); + } + + // Creates a new LastFM struct + pub fn new() -> LastFM { + let last_fm = LastFM { + // Grab this from config in future + dango_api_key: String::from("29a071e3113ab8ed36f069a2d3e20593"), + auth_token: None, + // Also grab from config in future + shared_secret: Some(String::from("5400c554430de5c5002d5e4bcc295b3d")), + session_key: None, + }; + return last_fm; + } + + // Creates an api request with the given parameters + pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result { + params.insert("api_key", &self.dango_api_key); + params.insert("sk", &self.session_key.as_ref().unwrap()); + + // Creates and sets api call signature + let api_sig = LastFM::request_sig(¶ms, &self.shared_secret.as_ref().unwrap()); + params.insert("api_sig", &api_sig); + let mut string_params = String::from(""); + + // Creates method call string + for key in params.keys() { + let param_value = params.get(key).unwrap(); + string_params.push_str(&format!("{key}={param_value}&")); + } + + string_params.pop(); + + let url = "http://ws.audioscrobbler.com/2.0/"; + + let response = surf::post(url).body_string(string_params).await; + + return response; + } + + // Returns an api signature as defined in the last.fm api documentation + fn request_sig(params: &BTreeMap<&str, &str>, shared_secret: &str) -> String { + let mut sig_string = String::new(); + // Appends keys and values of parameters to the unhashed sig + for key in params.keys() { + let param_value = params.get(*key); + sig_string.push_str(&format!("{key}{}", param_value.unwrap())); + } + sig_string.push_str(shared_secret); + + // Hashes signature using **INSECURE** MD5 (Required by last.fm api) + let mut md5_hasher = Md5::new(); + md5_hasher.update(sig_string); + let hash_result = md5_hasher.finalize(); + let hashed_sig = format!("{:#02x}", hash_result); + + return hashed_sig; + } + + // Removes last.fm account from dango-music-player + pub fn reset_account() { + todo!(); + } +} \ No newline at end of file