use std::{
error::Error,
ffi::{c_void, CStr, CString, NulError},
fmt::Display,
os::raw::c_char,
path::{Path, PathBuf},
};
mod flac_player;
use flac_player::FlacPlayer;
extern "C" {
fn free(__ptr: *mut ::std::os::raw::c_void);
}
#[repr(C)]
struct IdResult {
pub title: *const c_char,
pub game: *const c_char,
pub composer: *const c_char,
pub format: *const c_char,
pub length: i32,
}
extern "C" {
fn musix_create(data_dir: *const c_char) -> i32;
fn musix_find_plugin(music_file: *const c_char, after_plugin: *const c_void) -> *mut c_void;
fn musix_plugin_create_player(plugin: *mut c_void, music_file: *const c_char) -> *mut c_void;
fn musix_player_get_meta(player: *const c_void, what: *const c_char) -> *const c_char;
fn musix_player_get_samples(player: *const c_void, target: *mut i16, size: i32) -> i32;
fn musix_player_seek(player: *const c_void, song: i32, seconds: i32);
fn musix_player_destroy(player: *const c_void);
fn musix_player_get_hz(player: *const c_void) -> i32;
fn musix_get_changed_meta(player: *const c_void) -> *const c_char;
fn musix_get_error() -> *const c_char;
fn musix_identify_file(music_file: *const c_char, ext: *const c_char) -> *const IdResult;
fn musix_plugin_get_secondary_file(
player: *const c_void,
song_file: *const c_char,
) -> *const c_char;
}
#[derive(Debug, Clone, PartialEq)]
pub struct MusicError {
pub msg: String,
}
impl Display for MusicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.msg)
}
}
impl From<MusicError> for String {
fn from(val: MusicError) -> Self {
val.msg
}
}
impl Error for MusicError {}
impl From<NulError> for MusicError {
fn from(_value: NulError) -> Self {
MusicError {
msg: "NULL".to_string(),
}
}
}
pub trait MusixPlayer {
fn get_song_files(&self) -> &Vec<PathBuf>;
fn get_meta_string(&mut self, _what: &str) -> Option<String> {
None
}
fn get_frequency(&self) -> u32;
fn get_changed_meta(&mut self) -> Option<String> {
None
}
fn get_samples(&mut self, target: &mut [i16]) -> usize;
fn seek(&self, _song: i32, _seconds: i32) {}
}
#[derive(Debug)]
pub struct ChipPlayer {
player: *mut c_void,
files: Vec<PathBuf>,
}
unsafe impl Send for ChipPlayer {}
unsafe impl Sync for ChipPlayer {}
impl MusixPlayer for ChipPlayer {
fn get_song_files(&self) -> &Vec<PathBuf> {
&self.files
}
fn get_meta_string(&mut self, what: &str) -> Option<String> {
if self.player.is_null() {
return None;
}
let cwhat = CString::new(what).unwrap();
unsafe {
let cptr = musix_player_get_meta(self.player, cwhat.as_ptr());
if cptr.is_null() {
return None;
}
let meta = CStr::from_ptr(cptr).to_string_lossy().into_owned();
free(cptr as *mut c_void);
Some(meta)
}
}
fn get_frequency(&self) -> u32 {
unsafe { musix_player_get_hz(self.player) as u32 }
}
fn get_changed_meta(&mut self) -> Option<String> {
if self.player.is_null() {
return None;
}
unsafe {
let cptr = musix_get_changed_meta(self.player);
if cptr.is_null() {
return None;
}
let meta = CStr::from_ptr(cptr).to_string_lossy().into_owned();
free(cptr as *mut c_void);
Some(meta)
}
}
fn get_samples(&mut self, target: &mut [i16]) -> usize {
if self.player.is_null() {
0
} else {
let size = target.len() as i32;
let rc = unsafe { musix_player_get_samples(self.player, target.as_mut_ptr(), size) };
if rc < 0 {
0
} else {
rc as usize
}
}
}
fn seek(&self, song: i32, seconds: i32) {
if self.player.is_null() {
return;
}
unsafe {
musix_player_seek(self.player, song, seconds);
}
}
}
impl Drop for ChipPlayer {
fn drop(&mut self) {
if !self.player.is_null() {
unsafe { musix_player_destroy(self.player) }
}
}
}
#[derive(Default, Debug, Clone)]
pub struct SongInfo {
pub title: String,
pub game: String,
pub composer: String,
pub format: String,
}
pub fn identify_song(song_file: &Path) -> Option<SongInfo> {
let s = song_file.to_string_lossy();
let music_file = CString::new(s.as_ref()).unwrap();
unsafe {
let ptr = musix_identify_file(music_file.as_ptr(), std::ptr::null());
if ptr.is_null() {
return None;
}
let info = SongInfo {
title: CStr::from_ptr((*ptr).title).to_string_lossy().into(),
game: CStr::from_ptr((*ptr).game).to_string_lossy().into(),
composer: CStr::from_ptr((*ptr).composer).to_string_lossy().into(),
format: CStr::from_ptr((*ptr).format).to_string_lossy().into(),
};
free(ptr as *mut c_void);
Some(info)
}
}
pub fn can_handle(song_file: &Path) -> Result<bool, MusicError> {
if song_file.extension().and_then(|e| e.to_str()) == Some("flac") {
return Ok(true);
}
let s = song_file.to_string_lossy();
let music_file = CString::new(s.as_ref())?;
let plugin = unsafe { musix_find_plugin(music_file.as_ptr(), std::ptr::null()) };
Ok(!plugin.is_null())
}
pub fn load_song(song_file: &Path) -> Result<Box<dyn MusixPlayer>, MusicError> {
if song_file.extension().and_then(|e| e.to_str()) == Some("flac") {
return Ok(Box::new(FlacPlayer::new(song_file)?));
}
let s = song_file.to_string_lossy();
let music_file = CString::new(s.as_ref())?;
unsafe {
let mut plugin: *mut c_void = std::ptr::null_mut();
loop {
plugin = musix_find_plugin(music_file.as_ptr(), plugin);
if plugin.is_null() {
return Err(MusicError {
msg: "Could not find a plugin for file.".to_string(),
});
}
let player = musix_plugin_create_player(plugin, music_file.as_ptr());
if player.is_null() {
continue;
}
let mut files = vec![song_file.to_owned()];
let cptr = musix_plugin_get_secondary_file(plugin, music_file.as_ptr());
if !cptr.is_null() {
let name = CStr::from_ptr(cptr).to_string_lossy().into_owned();
free(cptr as *mut c_void);
if let Some(parent) = song_file.parent() {
files.push(parent.join(PathBuf::from(name)));
} else {
files.push(PathBuf::from(name).to_owned());
}
}
return Ok(Box::new(ChipPlayer { player, files }));
}
}
}
pub fn init(path: &Path) -> Result<(), MusicError> {
let s = path.to_string_lossy();
let data_dir = CString::new(s.as_ref())?;
unsafe {
if musix_create(data_dir.as_ptr()) != 0 {
let err = musix_get_error();
let cs = CStr::from_ptr(err).to_str().unwrap();
return Err(MusicError {
msg: cs.to_string(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::Path;
use crate::*;
#[test]
fn load_song_works() {
init(Path::new("init")).unwrap();
let mut song = load_song(Path::new(
"music/Martin Galway - Ultima Runes of Virtue II.gbs",
))
.unwrap();
let mut target = [0; 1024];
let count = song.get_samples(&mut target);
assert!(count == 1024);
}
#[test]
fn load_flac_works() {
let mut player = load_song(Path::new("music/test_sine.flac")).unwrap();
assert_eq!(player.get_frequency(), 44100);
let mut target = [0i16; 1024];
let count = player.get_samples(&mut target);
assert_eq!(count, 1024);
assert!(target.iter().any(|&s| s != 0));
}
#[test]
fn identify_file_works() {
let info = identify_song(Path::new("music/Castlevania.nsfe")).unwrap();
println!("INFO {:?} {:?}", info.game, info.composer);
println!("TITLE '{}'", info.game);
assert!(info.game == "Castlevania");
}
}