use crate::audio::DEFAULT_SAMPLE_RATE;
use std::fs;
use std::path::Path;
use ym2149::Ym2149Backend;
use ym2149_arkos_replayer::{ArkosPlayer, load_aks};
use ym2149_ay_replayer::{AyPlayer, CPC_UNSUPPORTED_MSG};
use ym2149_sndh_replayer::is_sndh_data;
use ym2149_ym_replayer::{Player, load_song};
use crate::args::ChipChoice;
use crate::{ArkosPlayerWrapper, AyPlayerWrapper, RealtimeChip, SndhPlayerWrapper};
pub struct PlayerInfo {
pub player: Box<dyn RealtimeChip>,
pub total_samples: usize,
pub song_info: String,
pub color_filter: bool,
pub title: String,
pub author: String,
pub format: String,
}
fn load_arkos_file(
file_data: &[u8],
file_path: &str,
_chip_choice: ChipChoice,
color_filter_override: Option<bool>,
) -> ym2149_ym_replayer::Result<PlayerInfo> {
let song = load_aks(file_data).map_err(|e| format!("Failed to load AKS file: {e}"))?;
if song.subsongs.is_empty() {
return Err("AKS file does not contain any subsongs".into());
}
let subsong = &song.subsongs[0];
if subsong.psgs.is_empty() {
return Err("AKS subsong defines no PSG chips".into());
}
let estimated_duration = subsong.end_position as f32 / subsong.replay_frequency_hz;
let total_samples = (estimated_duration * DEFAULT_SAMPLE_RATE as f32) as usize;
let info_str = format!(
"File: {}\nFormat: Arkos Tracker 3 (AKS)\n\
Title: {}\nAuthor: {}\nComposer: {}\n\
Subsongs: {}\nInstruments: {}\n\
PSG Count: {} ({} channels)\n\
Tracks: {}\nReplay Freq: {} Hz",
file_path,
song.metadata.title,
song.metadata.author,
if song.metadata.composer.is_empty() {
"-"
} else {
&song.metadata.composer
},
song.subsongs.len(),
song.instruments.len(),
subsong.psgs.len(),
subsong.psgs.len() * 3,
subsong.tracks.len(),
subsong.replay_frequency_hz,
);
let title = song.metadata.title.clone();
let author = song.metadata.author.clone();
let player =
ArkosPlayer::new(song, 0).map_err(|e| format!("Failed to create Arkos player: {e}"))?;
let color_filter = color_filter_override.unwrap_or(true);
Ok(PlayerInfo {
player: Box::new(ArkosPlayerWrapper::new(player)) as Box<dyn RealtimeChip>,
total_samples,
song_info: info_str,
color_filter,
title,
author,
format: "Arkos Tracker 3 (AKS)".to_string(),
})
}
fn load_sndh_file(
file_data: &[u8],
file_path: &str,
color_filter_override: Option<bool>,
) -> ym2149_ym_replayer::Result<PlayerInfo> {
let player = SndhPlayerWrapper::new(file_data, DEFAULT_SAMPLE_RATE)
.map_err(|e| format!("SNDH player init failed: {e}"))?;
let metadata = player.metadata();
let title = if metadata.title.is_empty() {
"(unknown)".to_string()
} else {
metadata.title.to_string()
};
let author = if metadata.author.is_empty() {
"(unknown)".to_string()
} else {
metadata.author.to_string()
};
let player_rate = metadata.frame_rate;
use ym2149_common::ChiptunePlayerBase;
let duration_secs = player.duration_seconds();
let total_samples = if duration_secs > 0.0 {
(duration_secs * DEFAULT_SAMPLE_RATE as f32) as usize
} else {
DEFAULT_SAMPLE_RATE as usize * 180
};
let duration_str = if duration_secs > 0.0 {
let mins = (duration_secs / 60.0) as u32;
let secs = (duration_secs % 60.0) as u32;
format!("{mins}:{secs:02}")
} else {
"unknown".to_string()
};
let info_str = format!(
"File: {file_path}\nFormat: SNDH (Atari ST)\nTitle: {title}\nAuthor: {author}\nPlayer rate: {player_rate} Hz\nDuration: {duration_str}"
);
let color_filter = color_filter_override.unwrap_or(false);
Ok(PlayerInfo {
player: Box::new(player) as Box<dyn RealtimeChip>,
total_samples,
song_info: info_str,
color_filter,
title,
author,
format: "SNDH (Atari ST)".to_string(),
})
}
fn load_ay_file(
file_data: &[u8],
file_path: &str,
color_filter_override: Option<bool>,
) -> ym2149_ym_replayer::Result<PlayerInfo> {
let (mut player, metadata) =
AyPlayer::load_from_bytes(file_data, 0).map_err(|e| format!("AY load failed: {e}"))?;
if let Some(cf) = color_filter_override {
player.set_color_filter(cf);
}
if player.requires_cpc_firmware() {
return Err(CPC_UNSUPPORTED_MSG.into());
}
let samples_per_frame = (DEFAULT_SAMPLE_RATE as f32 / 50.0).round() as usize;
let total_samples = metadata
.frame_count
.map(|frames| frames * samples_per_frame)
.unwrap_or(DEFAULT_SAMPLE_RATE as usize * 180);
let info_str = format!(
"File: {}\nFormat: AY/EMUL\nTitle: {}\nAuthor: {}\nSongs: {}/{}\nFrame length: {}\n",
file_path,
metadata.song_name,
if metadata.author.is_empty() {
"(unknown)"
} else {
&metadata.author
},
metadata.song_index + 1,
metadata.song_count,
metadata
.frame_count
.map(|f| f.to_string())
.unwrap_or_else(|| "unknown".into()),
);
let color_filter = color_filter_override.unwrap_or(true);
Ok(PlayerInfo {
player: Box::new(AyPlayerWrapper::new(player)) as Box<dyn RealtimeChip>,
total_samples,
song_info: info_str,
color_filter,
title: metadata.song_name.clone(),
author: metadata.author.clone(),
format: "AY/EMUL".to_string(),
})
}
pub fn create_player(
file_path: &str,
chip_choice: ChipChoice,
color_filter_override: Option<bool>,
) -> ym2149_ym_replayer::Result<PlayerInfo> {
let file_data =
fs::read(file_path).map_err(|e| format!("Failed to read file '{file_path}': {e}"))?;
let path = Path::new(file_path);
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
if extension == "aks" {
return load_arkos_file(&file_data, file_path, chip_choice, color_filter_override);
} else if extension == "ay" {
return load_ay_file(&file_data, file_path, color_filter_override);
} else if extension == "sndh" {
return load_sndh_file(&file_data, file_path, color_filter_override);
}
if is_sndh_data(&file_data) {
return load_sndh_file(&file_data, file_path, color_filter_override);
}
let (mut ym_player, summary) = load_song(&file_data)?;
match chip_choice {
ChipChoice::Ym2149 => {
let color_filter = color_filter_override.unwrap_or(true);
if let Some(cf) = color_filter_override {
ym_player.get_chip_mut().set_color_filter(cf);
}
let (title, author) = if let Some(info) = ym_player.info() {
(info.song_name.clone(), info.author.clone())
} else {
(String::new(), String::new())
};
let info_str = format!(
"File: {} ({})\n{}",
file_path,
summary.format,
ym_player.format_info()
);
let total_samples = summary.total_samples();
Ok(PlayerInfo {
player: Box::new(ym_player) as Box<dyn RealtimeChip>,
total_samples,
song_info: info_str,
color_filter,
title,
author,
format: summary.format.to_string(),
})
}
}
}
pub fn create_demo_player(chip_choice: ChipChoice) -> ym2149_ym_replayer::Result<PlayerInfo> {
match chip_choice {
ChipChoice::Ym2149 => {
let mut demo_player = Player::new();
let frames = vec![[0u8; 16]; 250];
demo_player.load_frames(frames);
let duration_secs = demo_player.get_duration_seconds();
let total_samples = (duration_secs * DEFAULT_SAMPLE_RATE as f32) as usize;
let info_str = format!("Demo Mode: {duration_secs:.2} seconds of silence");
Ok(PlayerInfo {
player: Box::new(demo_player) as Box<dyn RealtimeChip>,
total_samples,
song_info: info_str,
color_filter: true,
title: "Demo Mode".to_string(),
author: String::new(),
format: "Demo".to_string(),
})
}
}
}