use crate::ay8910::AY8910;
use crate::config::Config;
use crate::gameboy::GameBoy;
use crate::gd3::{get_gd3_header, Gd3Header};
use crate::midi_shim::{MIDIShim, TEXT_LOOP_END, TEXT_LOOP_START};
use crate::nesapu::NESAPU;
use crate::pcm::SegaPCM;
use crate::sn76489::SN76489;
use crate::vgm2mid::{
OPL_TYPE_Y8950, OPL_TYPE_YM3526, OPL_TYPE_YM3812, OPL_TYPE_YMF262, OPN_TYPE_YM2203,
OPN_TYPE_YM2608, OPN_TYPE_YM2610, OPN_TYPE_YM2612,
};
use crate::ym2151::YM2151;
use crate::ym2413::YM2413;
use crate::ym2612::{YM2612, YM2612_DAC};
use crate::ym3812::YM3812;
use crate::ymf278::YMF278;
use crate::{strict, verbose};
use anyhow::{bail, Result};
use midly::MetaMessage::Marker;
use midly::TrackEventKind::Meta;
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct VgmHeader {
str_vgm: [u8; 4], eof_offset: u32,
version: u32,
hz_sn76489: u32,
hz_ym2413: u32, gd3_offset: u32,
total_samples: u32,
loop_offset: u32,
loop_samples: u32, rate: u32,
sn_feedback: u16,
sn_stwidth: u8,
sn_flags: u8,
hz_ym2612: u32,
hz_ym2151: u32, data_offset: u32,
hz_spcm: u32,
spcm_intf: u32,
hz_rf5c68: u32, hz_ym2203: u32,
hz_ym2608: u32,
hz_ym2610b: u32,
hz_ym3812: u32, hz_ym3526: u32,
hz_y8950: u32,
hz_ymf262: u32,
hz_ymf278b: u32, hz_ymf271: u32,
hz_ymz280b: u32,
hz_rf5c164: u32,
hz_pwm: u32, hz_ay8910: u32,
ay_type: u8,
ay_flags: u8,
ay_flags2203: u8,
ay_flags2608: u8,
volume_modifier: u8,
reserved_1: u8,
loop_base: u8,
loop_modifier: u8,
hz_gb_dmg: u32, hz_nes_apu: u32,
hz_multipcm: u32,
hz_upd7759: u32,
hz_okim6258: u32, okim_flags: u8,
k0_flags: u8,
c140_type: u8,
reserved_2: u8,
hz_okim5295: u32,
hz_k051649: u32,
hz_k054539: u32, hz_huc6280: u32,
hz_c140: u32,
k053260: u32,
hz_pokey: u32, hz_qsound: u32,
hz_scsp: u32,
extra_header_offset: u32,
}
struct Vgm {
file_header: VgmHeader,
data_bank: Vec<u8>,
data_block_count: u32,
data_block_pos: [usize; 0xFF],
gt_gd3_tag: Gd3Header,
conversion_status_current: u32,
conversion_status_total: u32,
clock_sn76489: u32,
clock_ym2151: u32,
clock_ym2413: u32,
clock_ym2612: u32,
clock_ay8910: u32,
}
const GG_STEREO: u8 = 0x4F;
const SN76489: u8 = 0x50;
const YM2413: u8 = 0x51;
const YM2612_P0: u8 = 0x52;
const YM2612_P1: u8 = 0x53;
const YM2151: u8 = 0x54;
const YM2203: u8 = 0x55;
const YM2608_P0: u8 = 0x56;
const YM2608_P1: u8 = 0x57;
const YM2610_P0: u8 = 0x58;
const YM2610_P1: u8 = 0x59;
const YM3812: u8 = 0x5A;
const YM3526: u8 = 0x5B;
const Y8950: u8 = 0x5C;
const YMF262_P0: u8 = 0x5E;
const YMF262_P1: u8 = 0x5F;
const RF5C68_REG: u8 = 0xB0;
const SPCM_MEM: u8 = 0xC0;
const RF5C68_MEM: u8 = 0xC1;
const DATABNK_SEEK: u8 = 0xE0;
const AY8910: u8 = 0xA0;
const WAIT_N_SAMPLES: u8 = 0x61;
const WAIT_735_SAMPLES: u8 = 0x62;
const WAIT_882_SAMPLES: u8 = 0x63;
const END_OF_SOUND_DATA: u8 = 0x66;
const DATA_BLOCK: u8 = 0x67;
fn correct_header_for_version(mut header: VgmHeader) -> VgmHeader {
header.eof_offset += 0x4;
if header.gd3_offset > 0x0 {
header.gd3_offset += 0x14;
}
if header.loop_offset > 0x0 {
header.loop_offset += 0x1C;
}
if header.version < 0x101 {
header.rate = 0x0;
}
if header.version < 0x110 {
header.sn_feedback = 0x9;
header.sn_stwidth = 16;
header.hz_ym2612 = header.hz_ym2413;
header.hz_ym2612 = header.hz_ym2413;
}
if header.version < 0x150 || header.data_offset == 0x0 {
header.data_offset = 0xC;
}
if header.version < 0x151 || header.data_offset <= 0xC {
header.hz_spcm = 0x00;
header.spcm_intf = 0x00;
header.hz_rf5c68 = 0x00;
header.hz_ym2203 = 0x00;
header.hz_ym2608 = 0x00;
header.hz_ym2610b = 0x00;
header.hz_ym3812 = 0x00;
header.hz_ym3526 = 0x00;
header.hz_y8950 = 0x00;
header.hz_ymf262 = 0x00;
header.hz_ymf278b = 0x00;
header.hz_ymf271 = 0x00;
header.hz_ymz280b = 0x00;
header.hz_rf5c164 = 0x00;
header.hz_pwm = 0x00;
header.hz_ay8910 = 0x00;
header.ay_type = 0x00;
header.ay_flags = 0x00;
header.ay_flags2203 = 0x00;
header.ay_flags2608 = 0x00;
}
header.data_offset += 0x34;
header
}
fn parse_vgm_header(data: &[u8]) -> Result<VgmHeader> {
let mut header = VgmHeader {
str_vgm: [0; 4],
eof_offset: 0,
version: 0,
hz_sn76489: 0,
hz_ym2413: 0,
gd3_offset: 0,
total_samples: 0,
loop_offset: 0,
loop_samples: 0,
rate: 0,
sn_feedback: 0,
sn_stwidth: 0,
sn_flags: 0,
hz_ym2612: 0,
hz_ym2151: 0,
data_offset: 0,
hz_spcm: 0,
spcm_intf: 0,
hz_rf5c68: 0,
hz_ym2203: 0,
hz_ym2608: 0,
hz_ym2610b: 0,
hz_ym3812: 0,
hz_ym3526: 0,
hz_y8950: 0,
hz_ymf262: 0,
hz_ymf278b: 0,
hz_ymf271: 0,
hz_ymz280b: 0,
hz_rf5c164: 0,
hz_pwm: 0,
hz_ay8910: 0,
ay_type: 0,
ay_flags: 0,
ay_flags2203: 0,
ay_flags2608: 0,
volume_modifier: 0,
reserved_1: 0,
loop_base: 0,
loop_modifier: 0,
hz_gb_dmg: 0,
hz_nes_apu: 0,
hz_multipcm: 0,
hz_upd7759: 0,
hz_okim6258: 0,
okim_flags: 0,
k0_flags: 0,
c140_type: 0,
reserved_2: 0,
hz_okim5295: 0,
hz_k051649: 0,
hz_k054539: 0,
hz_huc6280: 0,
hz_c140: 0,
k053260: 0,
hz_pokey: 0,
hz_qsound: 0,
hz_scsp: 0,
extra_header_offset: 0,
};
let mut pos = 0;
header.str_vgm.clone_from_slice(&data[0..=3]);
if header.str_vgm != "Vgm ".as_bytes() {
verbose!("VGM Header is incorrect.");
strict!();
}
pos += 4;
header.eof_offset = extract_u32(data, &mut pos);
if data.len() < header.eof_offset as usize {
verbose!("VGM EOF is incorrect.");
strict!();
}
header.version = extract_u32(data, &mut pos);
header.hz_sn76489 = extract_u32(data, &mut pos);
header.hz_ym2413 = extract_u32(data, &mut pos);
header.gd3_offset = extract_u32(data, &mut pos);
header.total_samples = extract_u32(data, &mut pos);
header.loop_offset = extract_u32(data, &mut pos);
header.loop_samples = extract_u32(data, &mut pos);
header.rate = extract_u32(data, &mut pos);
pos += 4; header.hz_ym2612 = extract_u32(data, &mut pos);
header.hz_ym2151 = extract_u32(data, &mut pos);
header.data_offset = extract_u32(data, &mut pos);
header.hz_spcm = extract_u32(data, &mut pos);
header.spcm_intf = extract_u32(data, &mut pos);
header.hz_rf5c68 = extract_u32(data, &mut pos);
header.hz_ym2203 = extract_u32(data, &mut pos);
header.hz_ym2608 = extract_u32(data, &mut pos);
header.hz_ym2610b = extract_u32(data, &mut pos);
header.hz_ym3812 = extract_u32(data, &mut pos);
header.hz_ym3526 = extract_u32(data, &mut pos);
header.hz_y8950 = extract_u32(data, &mut pos);
header.hz_ymf262 = extract_u32(data, &mut pos);
header.hz_ymf278b = extract_u32(data, &mut pos);
header = correct_header_for_version(header);
Ok(header)
}
fn extract_u32(data: &[u8], pos: &mut usize) -> u32 {
let mut dst = [0u8; 4];
dst.clone_from_slice(&data[*pos..=*pos + 3]);
*pos += 4;
u32::from_le_bytes(dst)
}
fn parse_vgm_data(data: &[u8]) -> Result<Vgm> {
Ok(Vgm {
file_header: parse_vgm_header(data)?,
data_bank: Vec::new(),
data_block_count: 0,
data_block_pos: [0; 0xFF],
gt_gd3_tag: get_gd3_header(None)?,
conversion_status_current: 0,
conversion_status_total: 0,
clock_sn76489: 0,
clock_ym2151: 0,
clock_ym2413: 0,
clock_ym2612: 0,
clock_ay8910: 0,
})
}
pub(crate) fn convert_vgm_to_mid(file_data: &[u8], config: &Config, midi: &mut MIDIShim) -> Result<()> {
let mut vgm = parse_vgm_data(file_data)?;
dbg!(&vgm.file_header);
let mut file_pos: usize;
let (mut command, mut port, mut register, mut data): (u8, u8, u8, u8);
let _detimer: f32;
let mut last_data_note: u8;
let mut data_bank_pos: usize = 0;
let temp_long: u32 = 0;
let mut last_sn76489_cmd: [u8; 2] = [0; 2];
let mut stop_vgm: bool = false;
let mut loop_pos: usize = 0;
let mut dac_wrt: bool;
let mut cur_loop: u16 = 0;
vgm.conversion_status_current = 0x0;
vgm.conversion_status_total = vgm.file_header.eof_offset - vgm.file_header.data_offset;
let clock_sn76489 = vgm.file_header.hz_sn76489 & 0x3FFFFFFF;
vgm.clock_ym2413 = vgm.file_header.hz_ym2413 & 0x3FFFFFFF;
vgm.clock_ym2612 = vgm.file_header.hz_ym2612 & 0x3FFFFFFF;
vgm.clock_ym2151 = vgm.file_header.hz_ym2151 & 0x3FFFFFFF;
let mut fsam_2612 = f64::from(vgm.clock_ym2612) / 72.0;
let mut fsam_3812 = 0.0;
vgm.clock_ay8910 = vgm.file_header.hz_ay8910;
let t6w28_sn76489 = (vgm.file_header.hz_sn76489 & 0xC0000000) == 0xC0000000;
let mut ay8910 = AY8910::new(config, None);
let mut sn76489 = SN76489::new(t6w28_sn76489, clock_sn76489, config, None);
let mut segapcm = SegaPCM::new(None);
let mut gameboy = GameBoy::new(config, None);
let mut nes_apu = NESAPU::new(config, None);
let mut ym2151 = YM2151::new(config, None);
let mut ym2413 = YM2413::new(config, None);
let mut ym2612 = YM2612::new(config, None);
let mut ym3812 = YM3812::new(config, None);
let mut ymf278 = YMF278::new(config, None);
gameboy.init(midi);
if vgm.file_header.hz_ymf278b != 0 {
ymf278.load_opl4_instrument_set()?;
}
let loop_available = vgm.file_header.loop_offset > 0;
if loop_available { loop_pos = vgm.file_header.loop_offset as usize }
last_sn76489_cmd[0x0] = 0x0;
last_sn76489_cmd[0x1] = 0x0;
dac_wrt = false;
let mut _dac_data_byte = 0x80;
last_data_note = 0xFF;
midi.data_init(
vgm.clock_sn76489 != 0 || vgm.clock_ay8910 != 0,
t6w28_sn76489,
);
file_pos = vgm.file_header.data_offset as usize;
loop {
let mut wait = 0;
let mut cur_chip = 0x00u8;
command = file_data[file_pos];
if config.dualchips {
match command {
0x30 => {
cur_chip = 0x1;
command += 0x20;
},
0x3F => {
cur_chip = 0x1;
command += 0x10;
},
0xA1..=0xAC => {
cur_chip = 0x1;
command -= 0x50;
},
_ => verbose!("Invalid dual-chip command: {}", command),
}
}
match command {
GG_STEREO => {
data = file_data[file_pos + 1];
sn76489.gg_stereo_handle(data, cur_chip, midi)?;
file_pos += 2;
},
SN76489 => {
data = file_data[file_pos + 1];
sn76489.command_handle(data, cur_chip, midi)?;
file_pos += 2
},
YM2413 => {
register = file_data[file_pos + 1];
data = file_data[file_pos + 2];
ym2413.command_handle(
register,
data,
vgm.clock_ym2413,
midi,
)?;
file_pos += 3;
},
YM2612_P0 | YM2612_P1 | YM2203..=YM2610_P1 => {
(port, fsam_2612) = match command {
YM2612_P0 | YM2612_P1 => {
ym2612.state.opn_type = OPN_TYPE_YM2612;
(command & 0x1,
f64::from(
vgm.file_header.hz_ym2612 & 0x3FFFFFFF,
) / 72.0)
},
YM2203 => {
ym2612.state.opn_type = OPN_TYPE_YM2203;
(0x0,
f64::from(
vgm.file_header.hz_ym2203 & 0x3FFFFFFF,
) / 72.0)
},
YM2608_P0 | YM2608_P1 => {
ym2612.state.opn_type = OPN_TYPE_YM2608;
(command & 0x1,
f64::from(
vgm.file_header.hz_ym2608 & 0x3FFFFFFF,
) / 72.0)
},
YM2610_P0 | YM2610_P1 => {
ym2612.state.opn_type = OPN_TYPE_YM2610;
(command & 0x1,
f64::from(
vgm.file_header.hz_ym2610b & 0x3FFFFFFF,
) / 72.0)
},
_ => bail!("Invalid register"),
};
register = file_data[file_pos + 1];
data = file_data[file_pos + 2];
ym2612.command_handle(
port * 0x3,
register,
data,
fsam_2612,
midi,
)?;
if register == YM2612_DAC {
_dac_data_byte = data;
dac_wrt = true;
}
file_pos += 3;
},
YM2151 => {
register = file_data[file_pos + 1];
data = file_data[file_pos + 2];
ym2151.command_handle(
register,
data,
midi,
)?;
file_pos += 3;
},
YM3812..=Y8950 => {
match command {
YM3812 => {
ym3812.state.opl_type = OPL_TYPE_YM3812;
fsam_3812 = f64::from(
vgm.file_header.hz_ym3812 & 0x3FFFFFFF,
) / 72.0;
},
YM3526 => {
ym3812.state.opl_type = OPL_TYPE_YM3526;
fsam_3812 = f64::from(
vgm.file_header.hz_ym3526 & 0x3FFFFFFF,
) / 72.0;
},
Y8950 => {
ym3812.state.opl_type = OPL_TYPE_Y8950;
fsam_3812 = f64::from(
vgm.file_header.hz_y8950 & 0x3FFFFFFF,
) / 72.0;
},
_ => (),
}
register = file_data[file_pos + 1];
data = file_data[file_pos + 2];
ym3812.command_handle(register as u16, data, fsam_3812, midi)?;
file_pos += 3;
},
YMF262_P0..=YMF262_P1 => {
ym3812.state.opl_type = OPL_TYPE_YMF262;
fsam_3812 =
f64::from(vgm.file_header.hz_ymf262 & 0x3FFFFFFF) / 288.0;
register = file_data[file_pos + 1];
data = file_data[file_pos + 2];
ym3812.command_handle(
(command as u16 & 0x1) << 8 | register as u16,
data,
fsam_3812,
midi,
)?;
file_pos += 3;
},
0xD0 => {
port = file_data[file_pos + 1];
register = file_data[file_pos + 2];
data = file_data[file_pos + 3];
ymf278.command_handle(
port,
register,
data,
&mut ym3812,
fsam_3812,
midi,
)?;
file_pos += 4;
},
0xB3 => {
register = file_data[file_pos + 1];
data = file_data[file_pos + 2];
gameboy.command_handle(
register,
data,
midi,
)?;
file_pos += 3;
},
0xB4 => {
if vgm.clock_sn76489 == 0 {
vgm.clock_sn76489 = 1
}
register = file_data[file_pos + 1];
data = file_data[file_pos + 2];
nes_apu.command_handle(
register,
data,
midi,
)?;
file_pos += 3;
},
WAIT_N_SAMPLES => {
wait = (file_data[file_pos + 2] as u32) << 8;
wait |= file_data[file_pos + 1] as u32;
file_pos += 3
},
WAIT_735_SAMPLES => {
wait = 735;
file_pos += 1
},
WAIT_882_SAMPLES => {
wait = 882;
file_pos += 1;
},
END_OF_SOUND_DATA => stop_vgm = true,
DATA_BLOCK => {
data = file_data[file_pos + 2];
wait = file_data[file_pos + 3] as u32
| (file_data[file_pos + 4] as u32) << 0x08
| (file_data[file_pos + 5] as u32) << 0x10
| (file_data[file_pos + 6] as u32) << 0x18;
file_pos += 7;
if data == 0x00 {
let previous_length = vgm.data_bank.len();
vgm.data_bank
.resize(vgm.data_bank.len() + wait as usize, 0);
for offset in 0x0..wait {
vgm.data_bank[previous_length + offset as usize] =
file_data[file_pos + offset as usize]
}
}
file_pos += TryInto::<usize>::try_into(wait).unwrap();
wait = 0;
},
0x70..=0x7F => {
wait = ((command & 0x0F) + 1).into();
file_pos += 1;
},
0x80..=0x8F => {
wait = (command & 0x0F).into();
port = 0x00;
register = YM2612_DAC;
data = vgm.data_bank[data_bank_pos];
ym2612.command_handle(port, register, data, fsam_2612, midi)?;
_dac_data_byte = data;
dac_wrt = true;
data_bank_pos += 1;
file_pos += 1;
},
AY8910 => {
port = file_data[file_pos + 1] / 0x80;
register = file_data[file_pos + 1] & 0xF;
data = file_data[file_pos + 2];
ay8910.command_handle(
port,
register,
data,
vgm.clock_ay8910,
midi,
)?;
file_pos += 3;
},
RF5C68_REG =>
{
file_pos += 3
},
SPCM_MEM => {
wait = (file_data[file_pos + 2] as u32) << 8
| file_data[file_pos + 1] as u32;
data = file_data[file_pos + 3];
segapcm.mem_write(
wait.try_into().unwrap(),
data,
midi,
)?;
wait = 0;
file_pos += 4;
},
RF5C68_MEM =>
{
file_pos += 4
},
DATABNK_SEEK => {
data_bank_pos = file_data[file_pos + 1] as usize
| (file_data[file_pos + 2] as usize) << 0x08
| (file_data[file_pos + 3] as usize) << 0x10
| (file_data[file_pos + 4] as usize) << 0x18;
for block in 0x0..vgm.data_block_count {
if vgm.data_block_pos[block as usize] == data_bank_pos {
break;
}
}
if temp_long >= vgm.data_block_count {
vgm.data_block_pos[vgm.data_block_count as usize] =
data_bank_pos;
vgm.data_block_count += 0x01;
}
file_pos += 5;
},
0x30..=0x4E => file_pos += 2,
0x55..=0x5F => file_pos += 3,
0xA0..=0xBF => file_pos += 3,
0xC0..=0xDF => file_pos += 4,
0xE1..=0xFF => file_pos += 5,
0x90 =>
{
file_pos += 5
},
0x91 =>
{
file_pos += 5
},
0x92 =>
{
file_pos += 6
},
0x93 => {
data_bank_pos = file_data[file_pos + 2] as usize
| (file_data[file_pos + 3] as usize) << 8
| (file_data[file_pos + 4] as usize) << 16
| (file_data[file_pos + 5] as usize) << 24;
for data_block_pointer in 0x0..(vgm.data_block_count as usize)
{
if vgm.data_block_pos[data_block_pointer] == data_bank_pos {
break;
}
}
if temp_long >= vgm.data_block_count {
vgm.data_block_pos[vgm.data_block_count as usize] =
data_bank_pos;
vgm.data_block_count += 0x1;
}
dac_wrt = true;
file_pos += 11;
},
0x94 => {
if last_data_note < 0x80 {
midi.note_off_write(0x0F.into(), last_data_note.into(), 0x00.into());
}
last_data_note = 0xFF;
file_pos += 2
},
0x95 => {
let temp = file_data[file_pos + 2] & 0x7F;
if last_data_note < 0x80 {
midi.note_off_write(0x0F.into(), last_data_note.into(), 0x00.into());
}
midi.note_on_write(0x0F.into(), temp.into(), 0x7F.into());
last_data_note = temp;
file_pos += 5;
},
_ => {
file_pos += 1;
verbose!("Unrecognized command {} at {}", command, file_pos);
},
}
midi.delta_time += wait;
if loop_available {
if stop_vgm {
cur_loop += 0x01;
if cur_loop < config.vgm_loops {
midi.event_write(Meta(Marker(TEXT_LOOP_END)));
stop_vgm = false;
file_pos = loop_pos;
}
}
if file_pos == loop_pos {
midi.event_write(Meta(Marker(TEXT_LOOP_START)));
}
}
if vgm.clock_sn76489 != 0 {
for counter in 0..sn76489.state.note_delay.len() {
sn76489.state.note_delay[counter] += wait;
}
}
if dac_wrt {
for counter in 0x0..vgm.data_block_count {
if vgm.data_block_pos[counter as usize] == data_bank_pos - 1 {
if last_data_note < 0x80 {
midi.note_off_write(0x0F.into(), last_data_note.into(), 0x00.into());
}
midi.note_on_write(0x0F.into(), (counter as u8).into(), 0x7F.into());
last_data_note = counter.try_into().unwrap(); }
}
dac_wrt = false
}
if file_pos >= vgm.file_header.eof_offset as usize || stop_vgm {
break;
}
}
if loop_available {
midi.event_write(Meta(Marker(TEXT_LOOP_END)));
}
if vgm.clock_sn76489 != 0 {
port = if vgm.file_header.hz_sn76489 & 0x40000000 != 0 {
0x1
} else {
0x0
};
for chip_number in 0x0..=port {
for command in 0x0..=0x3 {
data = (1 << 7) | (command << 5);
sn76489.command_handle(
data,
chip_number,
midi,
)?;
sn76489.command_handle(
0x00,
chip_number,
midi,
)?;
}
}
}
if vgm.clock_ym2151 != 0 {
for command in 0x0..=0x7 {
register = 0x8;
data = command;
ym2151.command_handle(register, data, midi)?;
}
}
Ok(())
}