Restructured repository as a workspace

This commit is contained in:
G2-Games 2024-01-19 15:37:19 -06:00
commit 8fec560bcf
13 changed files with 1581 additions and 0 deletions

34
Cargo.toml Normal file
View file

@ -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"

0
src/bus_control.rs Normal file
View file

24
src/lib.rs Normal file
View file

@ -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;
}

View file

@ -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<PathBuf>,
pub lastfm: Option<LastFM>,
}
impl Config {
// Creates and saves a new config with default values
pub fn new(config_file: &PathBuf) -> std::io::Result<Config> {
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<Config, toml::de::Error> {
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(())
}
}

View file

@ -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() {
}

View file

@ -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<MusicController, std::io::Error>{
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<MusicController, toml::de::Error> {
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()))));
}
}

View file

@ -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<T> = result::Result<T, AudioOutputError>;
pub trait OutputSample: SizedSample + FromSample<f32> + IntoSample<f32> +cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static {}
pub struct AudioOutput<T>
where T: OutputSample,
{
ring_buf_producer: rb::Producer<T>,
sample_buf: SampleBuffer<T>,
stream: cpal::Stream,
resampler: Option<Resampler<T>>,
}
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<Box<dyn AudioStream>> {
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::<i8>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::I16 => AudioOutput::<i16>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::I32 => AudioOutput::<i32>::create_stream(spec, &device, &config.into(), duration),
//cpal::SampleFormat::I64 => AudioOutput::<i64>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::U8 => AudioOutput::<u8>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::U16 => AudioOutput::<u16>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::U32 => AudioOutput::<u32>::create_stream(spec, &device, &config.into(), duration),
//cpal::SampleFormat::U64 => AudioOutput::<u64>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::F32 => AudioOutput::<f32>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::F64 => AudioOutput::<f64>::create_stream(spec, &device, &config.into(), duration),
_ => todo!(),
};
}
impl<T: OutputSample> AudioOutput<T> {
// Creates the stream (TODO: Merge w/open_stream?)
fn create_stream(spec: SignalSpec, device: &cpal::Device, config: &cpal::StreamConfig, duration: Duration) -> Result<Box<dyn AudioStream>> {
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::<T>::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<T: OutputSample> AudioStream for AudioOutput<T> {
// 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();
}
}

View file

@ -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<Sender<PlayerMessage>>,
status_receiver: Option<Receiver<PlayerStatus>>,
}
#[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<MusicProcessor>)
}
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<u64> = None;
let mut audio_output: Option<Box<dyn AudioStream>> = 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<dyn FormatReader>, Box<dyn Decoder>) {
// Opens remote/local source and creates MediaSource for symphonia
let config = RemoteOptions { media_buffer_len: 10000, forward_buffer_len: 10000};
let src: Box<dyn MediaSource> = 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<dyn AsyncBufRead + Send + Sync + Unpin>,
media_buffer: Vec<u8>,
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<Self, surf::Error> {
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<usize> {
// 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<u64> {
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<u64> {
return None;
}
}

View file

@ -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<T> {
resampler: rubato::FftFixedIn<f32>,
input: Vec<Vec<f32>>,
output: Vec<Vec<f32>>,
interleaved: Vec<T>,
duration: usize,
}
impl<T> Resampler<T>
where
T: Sample + FromSample<f32> + IntoSample<f32>,
{
fn resample_inner(&mut self) -> &[T] {
{
//let mut input = heapless::Vec::<f32, 32>::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<T> Resampler<T>
where
T: Sample + FromSample<f32> + IntoSample<f32>,
{
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::<f32>::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<f32>]) {
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<S>(input: &AudioBuffer<S>, output: &mut [Vec<f32>])
where
S: Sample + IntoSample<f32>,
{
for (c, dst) in output.iter_mut().enumerate() {
let src = input.chan(c);
dst.extend(src.iter().map(|&s| s.into_sample()));
}
}

View file

@ -0,0 +1,35 @@
use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec};
#[derive(Clone)]
pub struct MusicProcessor {
pub audio_buffer: AudioBuffer<f32>,
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);
}
}

View file

@ -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<Path>,
pub title: Option<String>,
pub album: Option<String>,
tracknum: Option<usize>,
pub artist: Option<String>,
date: Option<Date>,
genre: Option<String>,
plays: Option<usize>,
favorited: Option<bool>,
format: Option<FileFormat>, // TODO: Make this a proper FileFormat eventually
duration: Option<Duration>,
pub custom_tags: Option<Vec<Tag>>,
}
#[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<Path>,
}
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::<usize, bool>(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<Vec<Song>, Box<dyn std::error::Error>>{
let cuesheet = CD::parse_file(cuesheet_path.to_path_buf())?;
let album = cuesheet.get_cdtext().read(PTI::Title);
let mut song_list:Vec<Song> = 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<Tag> = 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<dyn std::error::Error>> {
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(), &current_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<String> = 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<Vec<MusicObject>> {
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<MusicObject> = vec![];
while let Some(row) = rows.next().unwrap() {
let custom_tags: Vec<Tag> = match row.get::<usize, String>(11) {
Ok(result) => serde_json::from_str(&result).unwrap_or(vec![]),
Err(_) => vec![]
};
let file_format: FileFormat = FileFormat::from(row.get::<usize, String>(9).unwrap().as_bytes());
let new_song = Song {
// TODO: Implement proper errors here
path: Path::new(&row.get::<usize, String>(0).unwrap_or("".to_owned())).into(),
title: row.get::<usize, String>(1).ok(),
album: row.get::<usize, String>(2).ok(),
tracknum: row.get::<usize, usize>(3).ok(),
artist: row.get::<usize, String>(4).ok(),
date: Date::from_calendar_date(row.get::<usize, i32>(5).unwrap_or(0), time::Month::January, 1).ok(), // TODO: Fix this to get the actual date
genre: row.get::<usize, String>(6).ok(),
plays: row.get::<usize, usize>(7).ok(),
favorited: row.get::<usize, bool>(8).ok(),
format: Some(file_format),
duration: Some(Duration::from_secs(row.get::<usize, u64>(10).unwrap_or(0))),
custom_tags: Some(custom_tags),
};
final_result.push(MusicObject::Song(new_song));
};
Some(final_result)
}

View file

@ -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();
}
}

View file

@ -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<u32, surf::Error>;
}
#[derive(Serialize, Deserialize)]
pub struct LastFM {
dango_api_key: String,
auth_token: Option<String>,
shared_secret: Option<String>,
session_key: Option<String>,
}
#[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<u32, surf::Error> {
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<String, surf::Error> {
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<surf::Response, surf::Error> {
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(&params, &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!();
}
}