A bunch of improvements

This commit is contained in:
G2-Games 2023-11-06 02:39:09 -06:00
parent 7ddc829dac
commit 2e0ce48506
7 changed files with 101 additions and 150 deletions

View file

@ -38,7 +38,6 @@ rayon = "1.8.0"
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
cue = "2.0.0" cue = "2.0.0"
jwalk = "0.8.1"
base64 = "0.21.5" base64 = "0.21.5"
zip = "0.6.6" zip = "0.6.6"
flate2 = "1.0.28" flate2 = "1.0.28"

View file

@ -5,11 +5,7 @@ pub mod music_tracker {
pub mod music_storage { pub mod music_storage {
pub mod music_db; pub mod music_db;
pub mod playlist; pub mod playlist;
pub mod utils; mod utils;
}
pub mod music_processor {
pub mod music_processor;
} }
pub mod music_player { pub mod music_player {

View file

@ -2,7 +2,7 @@ use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use crate::music_player::music_player::{DSPMessage, DecoderMessage, MusicPlayer, PlayerStatus}; use crate::music_player::music_player::{DecoderMessage, MusicPlayer, PlayerStatus};
use crate::music_storage::music_db::{MusicLibrary, Song, Tag}; use crate::music_storage::music_db::{MusicLibrary, Song, Tag};
pub struct MusicController { pub struct MusicController {
@ -63,19 +63,6 @@ impl MusicController {
return self.music_player.get_current_song(); return self.music_player.get_current_song();
} }
/// Gets audio playback volume
pub fn get_vol(&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(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(
self.music_player.music_processor.clone(),
))));
}
/// Queries the [MusicLibrary], returning a `Vec<Song>` /// Queries the [MusicLibrary], returning a `Vec<Song>`
pub fn query_library( pub fn query_library(
&self, &self,

View file

@ -19,7 +19,6 @@ use futures::AsyncBufRead;
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use crate::music_player::music_output::AudioStream; use crate::music_player::music_output::AudioStream;
use crate::music_processor::music_processor::MusicProcessor;
use crate::music_storage::music_db::{Song, URI}; use crate::music_storage::music_db::{Song, URI};
use crate::music_tracker::music_tracker::{ use crate::music_tracker::music_tracker::{
DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError, DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError,
@ -27,7 +26,6 @@ use crate::music_tracker::music_tracker::{
// Struct that controls playback of music // Struct that controls playback of music
pub struct MusicPlayer { pub struct MusicPlayer {
pub music_processor: MusicProcessor,
player_status: PlayerStatus, player_status: PlayerStatus,
music_trackers: Vec<Box<dyn MusicTracker + Send>>, music_trackers: Vec<Box<dyn MusicTracker + Send>>,
current_song: Arc<RwLock<Option<Song>>>, current_song: Arc<RwLock<Option<Song>>>,
@ -51,7 +49,6 @@ pub enum DecoderMessage {
Pause, Pause,
Stop, Stop,
SeekTo(u64), SeekTo(u64),
DSP(DSPMessage),
} }
#[derive(Clone)] #[derive(Clone)]
@ -60,11 +57,6 @@ pub enum TrackerMessage {
TrackNow(Song), TrackNow(Song),
} }
#[derive(Debug, Clone)]
pub enum DSPMessage {
UpdateProcessor(Box<MusicProcessor>),
}
// Holds a song decoder reader, etc // Holds a song decoder reader, etc
struct SongHandler { struct SongHandler {
pub reader: Box<dyn FormatReader>, pub reader: Box<dyn FormatReader>,
@ -148,7 +140,6 @@ impl MusicPlayer {
); );
MusicPlayer { MusicPlayer {
music_processor: MusicProcessor::new(),
music_trackers: Vec::new(), music_trackers: Vec::new(),
player_status: PlayerStatus::Stopped, player_status: PlayerStatus::Stopped,
current_song, current_song,
@ -235,8 +226,6 @@ impl MusicPlayer {
let mut audio_output: Option<Box<dyn AudioStream>> = None; let mut audio_output: Option<Box<dyn AudioStream>> = None;
let mut music_processor = MusicProcessor::new();
let (tracker_sender, tracker_receiver): ( let (tracker_sender, tracker_receiver): (
Sender<TrackerMessage>, Sender<TrackerMessage>,
Receiver<TrackerMessage>, Receiver<TrackerMessage>,
@ -290,11 +279,6 @@ impl MusicPlayer {
status_sender.send(PlayerStatus::Paused).unwrap(); status_sender.send(PlayerStatus::Paused).unwrap();
} }
Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time), Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time),
Some(DecoderMessage::DSP(dsp_message)) => match dsp_message {
DSPMessage::UpdateProcessor(new_processor) => {
music_processor = *new_processor
}
},
// Exits main decode loop and subsequently ends thread // Exits main decode loop and subsequently ends thread
Some(DecoderMessage::Stop) => { Some(DecoderMessage::Stop) => {
status_sender.send(PlayerStatus::Stopped).unwrap(); status_sender.send(PlayerStatus::Stopped).unwrap();
@ -373,22 +357,6 @@ impl MusicPlayer {
.unwrap(), .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(_)) => { Err(Error::IoError(_)) => {
// rest in peace packet // rest in peace packet

View file

@ -1,45 +0,0 @@
use std::fmt::Debug;
use symphonia::core::audio::{AsAudioBufferRef, AudioBuffer, AudioBufferRef, Signal, SignalSpec};
#[derive(Clone)]
pub struct MusicProcessor {
pub audio_buffer: AudioBuffer<f32>,
pub audio_volume: f32,
}
impl Debug for MusicProcessor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MusicProcessor")
.field("audio_volume", &self.audio_volume)
.finish()
}
}
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

@ -1,17 +1,17 @@
// Crate things // Crate things
use super::utils::{normalize, read_library, write_library}; use super::utils::{normalize, read_library, write_library, find_images};
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
// Various std things // Various std things
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::error::Error; use std::error::Error;
use std::ops::ControlFlow::{Break, Continue};
// Files // Files
use cue::cd::CD; use cue::cd::CD;
use file_format::{FileFormat, Kind}; use file_format::{FileFormat, Kind};
use jwalk::WalkDir; use walkdir::WalkDir;
use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt};
use std::ffi::OsStr;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -28,9 +28,9 @@ use rayon::prelude::*;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AlbumArt { pub enum AlbumArt {
pub index: u16, Embedded(usize),
pub path: Option<URI>, External(URI),
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
@ -112,7 +112,7 @@ impl Song {
}, },
None => None, None => None,
}, },
_ => None, // Other field types are not yet supported _ => todo!(), // Other field types are not yet supported
} }
} }
} }
@ -182,6 +182,7 @@ pub enum Service {
InternetRadio, InternetRadio,
Spotify, Spotify,
Youtube, Youtube,
None,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -236,6 +237,8 @@ impl Album<'_> {
} }
} }
const BLOCKED_EXTENSIONS: [&str; 3] = ["vob", "log", "txt"];
#[derive(Debug)] #[derive(Debug)]
pub struct MusicLibrary { pub struct MusicLibrary {
pub library: Vec<Song>, pub library: Vec<Song>,
@ -272,8 +275,7 @@ impl MusicLibrary {
Ok(Self { library }) Ok(Self { library })
} }
/// Serializes the database out to the file /// Serializes the database out to the file specified in the config
/// specified in the config
pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> { pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> {
match config.db_path.try_exists() { match config.db_path.try_exists() {
Ok(exists) => { Ok(exists) => {
@ -293,19 +295,16 @@ impl MusicLibrary {
/// Queries for a [Song] by its [URI], returning a single `Song` /// Queries for a [Song] by its [URI], returning a single `Song`
/// with the `URI` that matches /// with the `URI` that matches
fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> {
let result = Arc::new(Mutex::new(None)); let result = self.library.par_iter().enumerate().try_for_each(|(i, track)| {
let index = Arc::new(Mutex::new(0));
let _ = &self.library.par_iter().enumerate().for_each(|(i, track)| {
if path == &track.location { if path == &track.location {
*result.clone().lock().unwrap() = Some(track); return std::ops::ControlFlow::Break((track, i));
*index.clone().lock().unwrap() = i;
return;
} }
Continue(())
}); });
let song = Arc::try_unwrap(result).unwrap().into_inner().unwrap();
match song { match result {
Some(song) => Some((song, Arc::try_unwrap(index).unwrap().into_inner().unwrap())), Break(song) => Some(song),
None => None, Continue(_) => None,
} }
} }
@ -313,7 +312,7 @@ impl MusicLibrary {
/// with matching `PathBuf`s /// with matching `PathBuf`s
fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> { fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> {
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
let _ = &self.library.par_iter().for_each(|track| { let _ = self.library.par_iter().for_each(|track| {
if path == track.location.path() { if path == track.location.path() {
result.clone().lock().unwrap().push(&track); result.clone().lock().unwrap().push(&track);
return; return;
@ -326,20 +325,18 @@ impl MusicLibrary {
} }
} }
/// Finds all the music files within a specified folder /// Finds all the audio files within a specified folder
pub fn scan_folder( pub fn scan_folder(
&mut self, &mut self,
target_path: &str, target_path: &str,
config: &Config, config: &Config,
) -> Result<usize, Box<dyn std::error::Error>> { ) -> Result<usize, Box<dyn std::error::Error>> {
let mut total = 0; let mut total = 0;
let mut i = 0; for target_file in WalkDir::new(target_path)
for entry in WalkDir::new(target_path)
.follow_links(true) .follow_links(true)
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ {
let target_file = entry;
let path = target_file.path(); let path = target_file.path();
// Ensure the target is a file and not a directory, // Ensure the target is a file and not a directory,
@ -348,6 +345,7 @@ impl MusicLibrary {
continue; continue;
} }
/* TODO: figure out how to increase the speed of this maybe
// Check if the file path is already in the db // Check if the file path is already in the db
if self.query_uri(&URI::Local(path.to_path_buf())).is_some() { if self.query_uri(&URI::Local(path.to_path_buf())).is_some() {
continue; continue;
@ -358,30 +356,27 @@ impl MusicLibrary {
if i % 500 == 0 { if i % 500 == 0 {
self.save(config).unwrap(); self.save(config).unwrap();
} }
*/
let format = FileFormat::from_file(&path)?; let format = FileFormat::from_file(&path)?;
let extension: &OsStr = match path.extension() { let extension = match path.extension() {
Some(ext) => ext, Some(ext) => ext.to_string_lossy().to_ascii_lowercase(),
None => OsStr::new(""), None => String::new(),
}; };
// If it's a normal file, add it to the database // If it's a normal file, add it to the database
// if it's a cuesheet, do a bunch of fancy stuff // if it's a cuesheet, do a bunch of fancy stuff
if (format.kind() == Kind::Audio || format.kind() == Kind::Video) if (format.kind() == Kind::Audio || format.kind() == Kind::Video)
&& extension.to_ascii_lowercase() != "log" && !BLOCKED_EXTENSIONS.contains(&extension.as_str())
&& extension.to_ascii_lowercase() != "vob"
{ {
match self.add_file(&target_file.path()) { match self.add_file(&target_file.path()) {
Ok(_) => { Ok(_) => total += 1,
//println!("{:?}", target_file.path());
total += 1
}
Err(_error) => { Err(_error) => {
//println!("{}, {:?}: {}", format, target_file.file_name(), _error) println!("{}, {:?}: {}", format, target_file.file_name(), _error)
} // TODO: Handle more of these errors } // TODO: Handle more of these errors
}; };
} else if extension.to_ascii_lowercase() == "cue" { } else if extension == "cue" {
total += match self.add_cuesheet(&target_file.path()) { total += match self.add_cuesheet(&target_file.path().to_path_buf()) {
Ok(added) => added, Ok(added) => added,
Err(error) => { Err(error) => {
println!("{}", error); println!("{}", error);
@ -437,25 +432,26 @@ impl MusicLibrary {
}; };
let value = match item.value() { let value = match item.value() {
ItemValue::Text(value) => String::from(value), ItemValue::Text(value) => value.clone(),
ItemValue::Locator(value) => String::from(value), ItemValue::Locator(value) => value.clone(),
ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)),
}; };
tags.insert(key, value); tags.insert(key, value);
} }
// Get all the album artwork information // Get all the album artwork information from the file
let mut album_art: Vec<AlbumArt> = Vec::new(); let mut album_art: Vec<AlbumArt> = Vec::new();
for (i, _art) in tag.pictures().iter().enumerate() { for (i, _art) in tag.pictures().iter().enumerate() {
let new_art = AlbumArt { let new_art = AlbumArt::Embedded(i as usize);
index: i as u16,
path: None,
};
album_art.push(new_art) album_art.push(new_art)
} }
// Find images around the music file that can be used
let mut found_images = find_images(&target_file.to_path_buf()).unwrap();
album_art.append(&mut found_images);
// Get the format as a string // Get the format as a string
let format: Option<FileFormat> = match FileFormat::from_file(target_file) { let format: Option<FileFormat> = match FileFormat::from_file(target_file) {
Ok(fmt) => Some(fmt), Ok(fmt) => Some(fmt),
@ -515,7 +511,10 @@ impl MusicLibrary {
} }
// Try to remove the original audio file from the db if it exists // Try to remove the original audio file from the db if it exists
let _ = self.remove_uri(&URI::Local(audio_location.clone())); match self.remove_uri(&URI::Local(audio_location.clone())) {
Ok(_) => tracks_added -= 1,
Err(_) => ()
};
// Get the track timing information // Get the track timing information
let pregap = match track.get_zero_pre() { let pregap = match track.get_zero_pre() {
@ -687,6 +686,7 @@ impl MusicLibrary {
sort_by: &Vec<Tag>, // Tags to sort the resulting data by sort_by: &Vec<Tag>, // Tags to sort the resulting data by
) -> Option<Vec<&Song>> { ) -> Option<Vec<&Song>> {
let songs = Arc::new(Mutex::new(Vec::new())); let songs = Arc::new(Mutex::new(Vec::new()));
//let matcher = SkimMatcherV2::default();
self.library.par_iter().for_each(|track| { self.library.par_iter().for_each(|track| {
for tag in target_tags { for tag in target_tags {
@ -701,9 +701,19 @@ impl MusicLibrary {
}, },
}; };
if normalize(&track_result.to_string()) /*
.contains(&normalize(&query_string.to_owned())) let match_level = match matcher.fuzzy_match(&normalize(&track_result), &normalize(query_string)) {
{ Some(conf) => conf,
None => continue
};
if match_level > 100 {
songs.lock().unwrap().push(track);
return;
}
*/
if normalize(&track_result.to_string()).contains(&normalize(&query_string.to_owned())) {
songs.lock().unwrap().push(track); songs.lock().unwrap().push(track);
return; return;
} }
@ -776,6 +786,7 @@ impl MusicLibrary {
.unwrap_or(&"".to_string()) .unwrap_or(&"".to_string())
.parse::<usize>() .parse::<usize>()
.unwrap_or(1); .unwrap_or(1);
match albums.get_mut(&norm_title) { match albums.get_mut(&norm_title) {
// If the album is in the list, add the track to the appropriate disc in it // If the album is in the list, add the track to the appropriate disc in it
Some(album) => match album.discs.get_mut(&disc_num) { Some(album) => match album.discs.get_mut(&disc_num) {
@ -786,11 +797,13 @@ impl MusicLibrary {
}, },
// If the album is not in the list, make a new one and add it // If the album is not in the list, make a new one and add it
None => { None => {
let album_art = result.album_art.get(0);
let new_album = Album { let new_album = Album {
title, title,
artist: result.get_tag(&Tag::AlbumArtist), artist: result.get_tag(&Tag::AlbumArtist),
discs: BTreeMap::from([(disc_num, vec![result])]), discs: BTreeMap::from([(disc_num, vec![result])]),
cover: None, cover: album_art,
}; };
albums.insert(norm_title, new_album); albums.insert(norm_title, new_album);
} }
@ -824,6 +837,7 @@ impl MusicLibrary {
albums albums
} }
/// Queries a list of albums by title
pub fn query_albums( pub fn query_albums(
&self, &self,
query_string: &String, // The query itself query_string: &String, // The query itself

View file

@ -1,13 +1,14 @@
use std::io::{BufReader, BufWriter}; use std::io::{BufReader, BufWriter};
use std::{error::Error, fs, path::PathBuf}; use std::{error::Error, fs, path::PathBuf};
use walkdir::WalkDir;
use file_format::{FileFormat, Kind};
use snap; use snap;
use crate::music_storage::music_db::Song; use super::music_db::{Song, AlbumArt, URI};
use unidecode::unidecode; use unidecode::unidecode;
pub fn normalize(input_string: &String) -> String { pub(super) fn normalize(input_string: &String) -> String {
// Normalize the unicode and convert everything to lowercase
let mut normalized = unidecode(input_string); let mut normalized = unidecode(input_string);
// Remove non alphanumeric characters // Remove non alphanumeric characters
@ -16,7 +17,7 @@ pub fn normalize(input_string: &String) -> String {
normalized normalized
} }
pub fn read_library(path: PathBuf) -> Result<Vec<Song>, Box<dyn Error>> { pub(super) fn read_library(path: PathBuf) -> Result<Vec<Song>, Box<dyn Error>> {
// Create a new snap reader over the database file // Create a new snap reader over the database file
let database = fs::File::open(path)?; let database = fs::File::open(path)?;
let reader = BufReader::new(database); let reader = BufReader::new(database);
@ -32,7 +33,7 @@ pub fn read_library(path: PathBuf) -> Result<Vec<Song>, Box<dyn Error>> {
Ok(library) Ok(library)
} }
pub fn write_library( pub(super) fn write_library(
library: &Vec<Song>, library: &Vec<Song>,
path: PathBuf, path: PathBuf,
take_backup: bool, take_backup: bool,
@ -63,3 +64,34 @@ pub fn write_library(
Ok(()) Ok(())
} }
pub fn find_images(song_path: &PathBuf) -> Result<Vec<AlbumArt>, Box<dyn Error>> {
let mut images: Vec<AlbumArt> = Vec::new();
let song_dir = song_path.parent().ok_or("")?;
for target_file in WalkDir::new(song_dir)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
if target_file.depth() >= 3 { // Don't recurse very deep
break
}
let path = target_file.path();
if !path.is_file() {
continue;
}
let format = FileFormat::from_file(&path)?.kind();
if format != Kind::Image {
break
}
let image_uri = URI::Local(path.to_path_buf().canonicalize().unwrap());
images.push(AlbumArt::External(image_uri));
}
Ok(images)
}