pub mod string;
use crate::pdb::string::DeviceSQLString;
use crate::util::ColorIndex;
use binrw::{
binread, binrw,
io::{Read, Seek, SeekFrom, Write},
BinRead, BinResult, BinWrite, Endian, FilePtr16, FilePtr8, ReadOptions, WriteOptions,
};
fn current_offset<R: Read + Seek>(reader: &mut R, _: &ReadOptions, _: ()) -> BinResult<u64> {
reader.stream_position().map_err(binrw::Error::Io)
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[brw(little)]
pub enum PageType {
#[brw(magic = 0u32)]
Tracks,
#[brw(magic = 1u32)]
Genres,
#[brw(magic = 2u32)]
Artists,
#[brw(magic = 3u32)]
Albums,
#[brw(magic = 4u32)]
Labels,
#[brw(magic = 5u32)]
Keys,
#[brw(magic = 6u32)]
Colors,
#[brw(magic = 7u32)]
PlaylistTree,
#[brw(magic = 8u32)]
PlaylistEntries,
#[brw(magic = 11u32)]
HistoryPlaylists,
#[brw(magic = 12u32)]
HistoryEntries,
#[brw(magic = 13u32)]
Artwork,
#[brw(magic = 19u32)]
History,
Unknown(u32),
}
#[binrw]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
#[brw(little)]
pub struct PageIndex(u32);
impl PageIndex {
#[must_use]
pub fn offset(&self, page_size: u32) -> u64 {
u64::from(self.0) * u64::from(page_size)
}
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Table {
pub page_type: PageType,
#[allow(dead_code)]
empty_candidate: u32,
pub first_page: PageIndex,
pub last_page: PageIndex,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Header {
#[br(temp, assert(unknown1 == 0))]
#[bw(calc = 0u32)]
unknown1: u32,
pub page_size: u32,
#[br(temp)]
#[bw(calc = tables.len().try_into().expect("too many tables"))]
num_tables: u32,
#[allow(dead_code)]
next_unused_page: PageIndex,
#[allow(dead_code)]
unknown: u32,
pub sequence: u32,
#[br(temp, assert(gap == 0))]
#[bw(calc = 0u32)]
gap: u32,
#[br(count = num_tables)]
pub tables: Vec<Table>,
}
impl Header {
pub fn read_pages<R: Read + Seek>(
&self,
reader: &mut R,
ro: &ReadOptions,
args: (&PageIndex, &PageIndex),
) -> BinResult<Vec<Page>> {
let (first_page, last_page) = args;
let mut pages = vec![];
let mut page_index = first_page.clone();
loop {
let page_offset = SeekFrom::Start(page_index.offset(self.page_size));
reader.seek(page_offset).map_err(binrw::Error::Io)?;
let page = Page::read_options(reader, ro, (self.page_size,))?;
let is_last_page = &page.page_index == last_page;
page_index = page.next_page.clone();
pages.push(page);
if is_last_page {
break;
}
}
Ok(pages)
}
}
#[binread]
#[derive(Debug, PartialEq)]
#[br(little, magic = 0u32)]
#[br(import(page_size: u32))]
pub struct Page {
pub page_index: PageIndex,
pub page_type: PageType,
pub next_page: PageIndex,
#[allow(dead_code)]
unknown1: u32,
#[allow(dead_code)]
unknown2: u32,
pub num_rows_small: u8,
#[allow(dead_code)]
unknown3: u8,
#[allow(dead_code)]
unknown4: u8,
pub page_flags: u8,
pub free_size: u16,
pub used_size: u16,
#[allow(dead_code)]
unknown5: u16,
pub num_rows_large: u16,
#[allow(dead_code)]
unknown6: u16,
#[allow(dead_code)]
unknown7: u16,
#[br(temp)]
#[br(calc = if num_rows_large > num_rows_small.into() && num_rows_large != 0x1fff { num_rows_large } else { num_rows_small.into() })]
num_rows: u16,
#[br(temp)]
#[br(calc = if num_rows > 0 { (num_rows - 1) / RowGroup::MAX_ROW_COUNT + 1 } else { 0 })]
num_row_groups: u16,
#[br(temp)]
#[br(calc = SeekFrom::Current(i64::from(page_size) - i64::from(Self::HEADER_SIZE) - i64::from(num_rows) * 2 - i64::from(num_row_groups) * 4))]
row_groups_offset: SeekFrom,
#[br(temp)]
#[br(calc = page_index.offset(page_size) + u64::from(Self::HEADER_SIZE))]
page_heap_offset: u64,
#[br(seek_before(row_groups_offset), restore_position)]
#[br(parse_with = Self::parse_row_groups, args(page_type, page_heap_offset, num_rows, num_row_groups))]
pub row_groups: Vec<RowGroup>,
}
impl Page {
pub const HEADER_SIZE: u32 = 0x28;
fn parse_row_groups<R: Read + Seek>(
reader: &mut R,
ro: &ReadOptions,
args: (PageType, u64, u16, u16),
) -> BinResult<Vec<RowGroup>> {
let (page_type, page_heap_offset, num_rows, num_row_groups) = args;
if num_row_groups == 0 {
return Ok(vec![]);
}
let mut row_groups = Vec::with_capacity(num_row_groups.into());
let mut num_rows_in_last_row_group = num_rows % RowGroup::MAX_ROW_COUNT;
if num_rows_in_last_row_group == 0 {
num_rows_in_last_row_group = RowGroup::MAX_ROW_COUNT;
}
let row_group = RowGroup::read_options(
reader,
ro,
(page_type, page_heap_offset, num_rows_in_last_row_group),
)?;
row_groups.push(row_group);
for _ in 1..num_row_groups {
let row_group = RowGroup::read_options(
reader,
ro,
(page_type, page_heap_offset, RowGroup::MAX_ROW_COUNT),
)?;
row_groups.insert(0, row_group);
}
Ok(row_groups)
}
#[must_use]
pub fn has_data(&self) -> bool {
(self.page_flags & 0x40) == 0
}
#[must_use]
pub fn num_rows(&self) -> u16 {
if self.num_rows_large > self.num_rows_small.into() && self.num_rows_large != 0x1fff {
self.num_rows_large
} else {
self.num_rows_small.into()
}
}
#[must_use]
pub fn num_row_groups(&self) -> u16 {
let num_rows = self.num_rows();
if num_rows > 0 {
(num_rows - 1) / RowGroup::MAX_ROW_COUNT + 1
} else {
0
}
}
}
#[binread]
#[derive(Debug, PartialEq)]
#[brw(little)]
#[br(import(page_type: PageType, page_heap_offset: u64, num_rows: u16))]
pub struct RowGroup {
#[br(offset = page_heap_offset, args { count: num_rows.into(), inner: (page_type,) })]
rows: Vec<FilePtr16<Row>>,
row_presence_flags: u16,
unknown: u16,
}
impl RowGroup {
const MAX_ROW_COUNT: u16 = 16;
pub fn present_rows(&self) -> impl Iterator<Item = &Row> {
self.rows
.iter()
.rev()
.enumerate()
.filter_map(|(i, row_offset)| {
if (self.row_presence_flags & (1 << i)) != 0 {
row_offset.value.as_ref()
} else {
None
}
})
}
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct TrackId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct ArtworkId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct AlbumId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct ArtistId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct GenreId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct KeyId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct LabelId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct PlaylistTreeNodeId(pub u32);
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[br(little)]
pub struct HistoryPlaylistId(pub u32);
#[binread]
#[derive(Debug, PartialEq, Eq, Clone)]
#[br(little)]
pub struct Album {
#[br(temp, parse_with = current_offset)]
#[bw(ignore)]
base_offset: u64,
unknown1: u16,
index_shift: u16,
unknown2: u32,
artist_id: ArtistId,
id: AlbumId,
unknown3: u32,
unknown4: u8,
#[br(offset = base_offset, parse_with = FilePtr8::parse)]
name: DeviceSQLString,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Artist {
subtype: u16,
index_shift: u16,
id: ArtistId,
unknown1: u8,
ofs_name_near: u8,
#[br(if(subtype == 0x64))]
ofs_name_far: Option<u16>,
#[br(seek_before = Artist::calculate_name_seek(ofs_name_near, &ofs_name_far))]
#[bw(seek_before = Artist::calculate_name_seek(*ofs_name_near, ofs_name_far))]
#[brw(restore_position)]
name: DeviceSQLString,
}
impl Artist {
fn calculate_name_seek(ofs_near: u8, ofs_far: &Option<u16>) -> SeekFrom {
let offset: u16 = ofs_far.map_or_else(|| ofs_near.into(), |v| v - 2) - 10;
SeekFrom::Current(offset.into())
}
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Artwork {
id: ArtworkId,
path: DeviceSQLString,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Color {
unknown1: u32,
unknown2: u8,
color: ColorIndex,
unknown3: u16,
name: DeviceSQLString,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Genre {
id: GenreId,
name: DeviceSQLString,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct HistoryPlaylist {
id: HistoryPlaylistId,
name: DeviceSQLString,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct HistoryEntry {
track_id: TrackId,
playlist_id: HistoryPlaylistId,
entry_index: u32,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Key {
id: KeyId,
id2: u32,
name: DeviceSQLString,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct Label {
id: LabelId,
name: DeviceSQLString,
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct PlaylistTreeNode {
pub parent_id: PlaylistTreeNodeId,
unknown: u32,
sort_order: u32,
pub id: PlaylistTreeNodeId,
node_is_folder: u32,
pub name: DeviceSQLString,
}
impl PlaylistTreeNode {
#[must_use]
pub fn is_folder(&self) -> bool {
self.node_is_folder > 0
}
}
#[binrw]
#[derive(Debug, PartialEq, Eq, Clone)]
#[brw(little)]
pub struct PlaylistEntry {
entry_index: u32,
track_id: TrackId,
playlist_id: PlaylistTreeNodeId,
}
#[binread]
#[derive(Debug, PartialEq, Eq, Clone)]
#[br(little)]
pub struct Track {
#[br(temp, parse_with = current_offset)]
base_offset: u64,
unknown1: u16,
index_shift: u16,
bitmask: u32,
sample_rate: u32,
composer_id: ArtistId,
file_size: u32,
unknown2: u32,
unknown3: u16,
unknown4: u16,
artwork_id: ArtworkId,
key_id: KeyId,
orig_artist_id: ArtistId,
label_id: LabelId,
remixer_id: ArtistId,
bitrate: u32,
track_number: u32,
tempo: u32,
genre_id: GenreId,
album_id: AlbumId,
artist_id: ArtistId,
id: TrackId,
disc_number: u16,
play_count: u16,
year: u16,
sample_depth: u16,
duration: u16,
unknown5: u16,
color: ColorIndex,
rating: u8,
unknown6: u16,
unknown7: u16,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
isrc: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string1: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string2: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string3: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string4: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
message: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
kuvo_public: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
autoload_hotcues: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string5: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string6: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
date_added: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
release_date: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
mix_name: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string7: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
analyze_path: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
analyze_date: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
comment: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
title: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
unknown_string8: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
filename: DeviceSQLString,
#[br(offset = base_offset, parse_with = FilePtr16::parse)]
file_path: DeviceSQLString,
}
impl binrw::meta::WriteEndian for Track {
const ENDIAN: binrw::meta::EndianKind = binrw::meta::EndianKind::Endian(Endian::Little);
}
impl BinWrite for Track {
type Args = ();
fn write_options<W: Write + Seek>(
&self,
writer: &mut W,
options: &WriteOptions,
_args: Self::Args,
) -> BinResult<()> {
debug_assert!(options.endian() == Endian::Little);
let base_position = writer.stream_position()?;
self.unknown1.write_options(writer, options, ())?;
self.index_shift.write_options(writer, options, ())?;
self.bitmask.write_options(writer, options, ())?;
self.sample_rate.write_options(writer, options, ())?;
self.composer_id.write_options(writer, options, ())?;
self.file_size.write_options(writer, options, ())?;
self.unknown2.write_options(writer, options, ())?;
self.unknown3.write_options(writer, options, ())?;
self.unknown4.write_options(writer, options, ())?;
self.artwork_id.write_options(writer, options, ())?;
self.key_id.write_options(writer, options, ())?;
self.orig_artist_id.write_options(writer, options, ())?;
self.label_id.write_options(writer, options, ())?;
self.remixer_id.write_options(writer, options, ())?;
self.bitrate.write_options(writer, options, ())?;
self.track_number.write_options(writer, options, ())?;
self.tempo.write_options(writer, options, ())?;
self.genre_id.write_options(writer, options, ())?;
self.album_id.write_options(writer, options, ())?;
self.artist_id.write_options(writer, options, ())?;
self.id.write_options(writer, options, ())?;
self.disc_number.write_options(writer, options, ())?;
self.play_count.write_options(writer, options, ())?;
self.year.write_options(writer, options, ())?;
self.sample_depth.write_options(writer, options, ())?;
self.duration.write_options(writer, options, ())?;
self.unknown5.write_options(writer, options, ())?;
self.color.write_options(writer, options, ())?;
self.rating.write_options(writer, options, ())?;
self.unknown6.write_options(writer, options, ())?;
self.unknown7.write_options(writer, options, ())?;
let start_of_string_section = writer.stream_position()?;
debug_assert_eq!(start_of_string_section - base_position, 0x5e);
let mut string_offsets = [0u16; 21];
writer.seek(SeekFrom::Current(0x2a))?;
for (i, string) in [
&self.isrc,
&self.unknown_string1,
&self.unknown_string2,
&self.unknown_string3,
&self.unknown_string4,
&self.message,
&self.kuvo_public,
&self.autoload_hotcues,
&self.unknown_string5,
&self.unknown_string6,
&self.date_added,
&self.release_date,
&self.mix_name,
&self.unknown_string7,
&self.analyze_path,
&self.analyze_date,
&self.comment,
&self.title,
&self.unknown_string8,
&self.filename,
&self.file_path,
]
.into_iter()
.enumerate()
{
let current_position = writer.stream_position()?;
let offset: u16 = current_position
.checked_sub(base_position)
.and_then(|v| u16::try_from(v).ok())
.ok_or_else(|| binrw::Error::AssertFail {
pos: current_position,
message: "Wraparound while calculating row offset".to_string(),
})?;
string_offsets[i] = offset;
string.write_options(writer, options, ())?;
}
let end_of_row = writer.stream_position()?;
writer.seek(SeekFrom::Start(start_of_string_section))?;
string_offsets.write_options(writer, options, ())?;
writer.seek(SeekFrom::Start(end_of_row))?;
Ok(())
}
}
#[binread]
#[derive(Debug, PartialEq, Eq, Clone)]
#[br(little)]
#[br(import(page_type: PageType))]
#[allow(clippy::large_enum_variant)]
pub enum Row {
#[br(pre_assert(page_type == PageType::Albums))]
Album(Album),
#[br(pre_assert(page_type == PageType::Artists))]
Artist(Artist),
#[br(pre_assert(page_type == PageType::Artwork))]
Artwork(Artwork),
#[br(pre_assert(page_type == PageType::Colors))]
Color(Color),
#[br(pre_assert(page_type == PageType::Genres))]
Genre(Genre),
#[br(pre_assert(page_type == PageType::HistoryPlaylists))]
HistoryPlaylist(HistoryPlaylist),
#[br(pre_assert(page_type == PageType::HistoryEntries))]
HistoryEntry(HistoryEntry),
#[br(pre_assert(page_type == PageType::Keys))]
Key(Key),
#[br(pre_assert(page_type == PageType::Labels))]
Label(Label),
#[br(pre_assert(page_type == PageType::PlaylistTree))]
PlaylistTreeNode(PlaylistTreeNode),
#[br(pre_assert(page_type == PageType::PlaylistEntries))]
PlaylistEntry(PlaylistEntry),
#[br(pre_assert(page_type == PageType::Tracks))]
Track(Track),
#[br(pre_assert(matches!(page_type, PageType::History | PageType::Unknown(_))))]
Unknown,
}
#[cfg(test)]
mod test {
use super::*;
use crate::util::testing::test_roundtrip;
#[test]
fn empty_header() {
let header = Header {
page_size: 4096,
next_unused_page: PageIndex(1),
unknown: 0,
sequence: 1,
tables: vec![],
};
test_roundtrip(
&[
0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
],
header,
);
}
#[test]
fn demo_tracks_header() {
let header = Header {
page_size: 4096,
next_unused_page: PageIndex(51),
unknown: 5,
sequence: 34,
tables: [
Table {
page_type: PageType::Tracks,
empty_candidate: 47,
first_page: PageIndex(1),
last_page: PageIndex(2),
},
Table {
page_type: PageType::Genres,
empty_candidate: 4,
first_page: PageIndex(3),
last_page: PageIndex(3),
},
Table {
page_type: PageType::Artists,
empty_candidate: 49,
first_page: PageIndex(5),
last_page: PageIndex(6),
},
Table {
page_type: PageType::Albums,
empty_candidate: 8,
first_page: PageIndex(7),
last_page: PageIndex(7),
},
Table {
page_type: PageType::Labels,
empty_candidate: 50,
first_page: PageIndex(9),
last_page: PageIndex(10),
},
Table {
page_type: PageType::Keys,
empty_candidate: 46,
first_page: PageIndex(11),
last_page: PageIndex(12),
},
Table {
page_type: PageType::Colors,
empty_candidate: 42,
first_page: PageIndex(13),
last_page: PageIndex(14),
},
Table {
page_type: PageType::PlaylistTree,
empty_candidate: 16,
first_page: PageIndex(15),
last_page: PageIndex(15),
},
Table {
page_type: PageType::PlaylistEntries,
empty_candidate: 18,
first_page: PageIndex(17),
last_page: PageIndex(17),
},
Table {
page_type: PageType::Unknown(9),
empty_candidate: 20,
first_page: PageIndex(19),
last_page: PageIndex(19),
},
Table {
page_type: PageType::Unknown(10),
empty_candidate: 22,
first_page: PageIndex(21),
last_page: PageIndex(21),
},
Table {
page_type: PageType::HistoryPlaylists,
empty_candidate: 24,
first_page: PageIndex(23),
last_page: PageIndex(23),
},
Table {
page_type: PageType::HistoryEntries,
empty_candidate: 26,
first_page: PageIndex(25),
last_page: PageIndex(25),
},
Table {
page_type: PageType::Artwork,
empty_candidate: 28,
first_page: PageIndex(27),
last_page: PageIndex(27),
},
Table {
page_type: PageType::Unknown(14),
empty_candidate: 30,
first_page: PageIndex(29),
last_page: PageIndex(29),
},
Table {
page_type: PageType::Unknown(15),
empty_candidate: 32,
first_page: PageIndex(31),
last_page: PageIndex(31),
},
Table {
page_type: PageType::Unknown(16),
empty_candidate: 43,
first_page: PageIndex(33),
last_page: PageIndex(34),
},
Table {
page_type: PageType::Unknown(17),
empty_candidate: 44,
first_page: PageIndex(35),
last_page: PageIndex(36),
},
Table {
page_type: PageType::Unknown(18),
empty_candidate: 45,
first_page: PageIndex(37),
last_page: PageIndex(38),
},
Table {
page_type: PageType::History,
empty_candidate: 48,
first_page: PageIndex(39),
last_page: PageIndex(41),
},
]
.to_vec(),
};
test_roundtrip(
&[
0, 0, 0, 0, 0, 16, 0, 0, 20, 0, 0, 0, 51, 0, 0, 0, 5, 0, 0, 0, 34, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 3,
0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 49, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0,
8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 50, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0,
0, 5, 0, 0, 0, 46, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 42, 0, 0, 0, 13,
0, 0, 0, 14, 0, 0, 0, 7, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 8, 0, 0,
0, 18, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 9, 0, 0, 0, 20, 0, 0, 0, 19, 0, 0, 0, 19,
0, 0, 0, 10, 0, 0, 0, 22, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 24, 0, 0,
0, 23, 0, 0, 0, 23, 0, 0, 0, 12, 0, 0, 0, 26, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0,
13, 0, 0, 0, 28, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 14, 0, 0, 0, 30, 0, 0, 0, 29,
0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 32, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 16, 0, 0,
0, 43, 0, 0, 0, 33, 0, 0, 0, 34, 0, 0, 0, 17, 0, 0, 0, 44, 0, 0, 0, 35, 0, 0, 0,
36, 0, 0, 0, 18, 0, 0, 0, 45, 0, 0, 0, 37, 0, 0, 0, 38, 0, 0, 0, 19, 0, 0, 0, 48,
0, 0, 0, 39, 0, 0, 0, 41, 0, 0, 0,
],
header,
);
}
#[test]
fn track_row() {
let row = Track {
unknown1: 36,
index_shift: 160,
bitmask: 788224,
sample_rate: 44100,
composer_id: ArtistId(0),
file_size: 6899624,
unknown2: 214020570,
unknown3: 64128,
unknown4: 1511,
artwork_id: ArtworkId(0),
key_id: KeyId(5),
orig_artist_id: ArtistId(0),
label_id: LabelId(1),
remixer_id: ArtistId(0),
bitrate: 320,
track_number: 0,
tempo: 12800,
genre_id: GenreId(0),
album_id: AlbumId(0),
artist_id: ArtistId(1),
id: TrackId(1),
disc_number: 0,
play_count: 0,
year: 0,
sample_depth: 16,
duration: 172,
unknown5: 41,
color: ColorIndex::None,
rating: 0,
unknown6: 1,
unknown7: 3,
isrc: DeviceSQLString::new_isrc("".to_string()).unwrap(),
unknown_string1: DeviceSQLString::empty(),
unknown_string2: DeviceSQLString::new("3".to_string()).unwrap(),
unknown_string3: DeviceSQLString::new("3".to_string()).unwrap(),
unknown_string4: DeviceSQLString::empty(),
message: DeviceSQLString::empty(),
kuvo_public: DeviceSQLString::empty(),
autoload_hotcues: DeviceSQLString::new("ON".to_string()).unwrap(),
unknown_string5: DeviceSQLString::empty(),
unknown_string6: DeviceSQLString::empty(),
date_added: DeviceSQLString::new("2018-05-25".to_string()).unwrap(),
release_date: DeviceSQLString::empty(),
mix_name: DeviceSQLString::empty(),
unknown_string7: DeviceSQLString::empty(),
analyze_path: DeviceSQLString::new(
"/PIONEER/USBANLZ/P016/0000875E/ANLZ0000.DAT".to_string(),
)
.unwrap(),
analyze_date: DeviceSQLString::new("2022-02-02".to_string()).unwrap(),
comment: DeviceSQLString::new("Tracks by www.loopmasters.com".to_string()).unwrap(),
title: DeviceSQLString::new("Demo Track 1".to_string()).unwrap(),
unknown_string8: DeviceSQLString::empty(),
filename: DeviceSQLString::new("Demo Track 1.mp3".to_string()).unwrap(),
file_path: DeviceSQLString::new(
"/Contents/Loopmasters/UnknownAlbum/Demo Track 1.mp3".to_string(),
)
.unwrap(),
};
test_roundtrip(
&[
36, 0, 160, 0, 0, 7, 12, 0, 68, 172, 0, 0, 0, 0, 0, 0, 168, 71, 105, 0, 218, 177,
193, 12, 128, 250, 231, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 64, 1, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 172, 0, 41, 0, 0, 0, 1, 0, 3, 0, 136, 0, 137, 0,
138, 0, 140, 0, 142, 0, 143, 0, 144, 0, 145, 0, 148, 0, 149, 0, 150, 0, 161, 0,
162, 0, 163, 0, 164, 0, 208, 0, 219, 0, 249, 0, 6, 1, 7, 1, 24, 1, 3, 3, 5, 51, 5,
51, 3, 3, 3, 7, 79, 78, 3, 3, 23, 50, 48, 49, 56, 45, 48, 53, 45, 50, 53, 3, 3, 3,
89, 47, 80, 73, 79, 78, 69, 69, 82, 47, 85, 83, 66, 65, 78, 76, 90, 47, 80, 48, 49,
54, 47, 48, 48, 48, 48, 56, 55, 53, 69, 47, 65, 78, 76, 90, 48, 48, 48, 48, 46, 68,
65, 84, 23, 50, 48, 50, 50, 45, 48, 50, 45, 48, 50, 61, 84, 114, 97, 99, 107, 115,
32, 98, 121, 32, 119, 119, 119, 46, 108, 111, 111, 112, 109, 97, 115, 116, 101,
114, 115, 46, 99, 111, 109, 27, 68, 101, 109, 111, 32, 84, 114, 97, 99, 107, 32,
49, 3, 35, 68, 101, 109, 111, 32, 84, 114, 97, 99, 107, 32, 49, 46, 109, 112, 51,
105, 47, 67, 111, 110, 116, 101, 110, 116, 115, 47, 76, 111, 111, 112, 109, 97,
115, 116, 101, 114, 115, 47, 85, 110, 107, 110, 111, 119, 110, 65, 108, 98, 117,
109, 47, 68, 101, 109, 111, 32, 84, 114, 97, 99, 107, 32, 49, 46, 109, 112, 51,
],
row,
);
}
#[test]
fn artist_row() {
let row = Artist {
subtype: 96,
index_shift: 32,
id: ArtistId(1),
unknown1: 3,
ofs_name_near: 10,
ofs_name_far: None,
name: DeviceSQLString::new("Loopmasters".to_string()).unwrap(),
};
test_roundtrip(
&[
96, 0, 32, 0, 1, 0, 0, 0, 3, 10, 25, 76, 111, 111, 112, 109, 97, 115, 116, 101,
114, 115,
],
row,
);
}
#[test]
fn label_row() {
let row = Label {
id: LabelId(1),
name: DeviceSQLString::new("Loopmasters".to_string()).unwrap(),
};
test_roundtrip(
&[
1, 0, 0, 0, 25, 76, 111, 111, 112, 109, 97, 115, 116, 101, 114, 115,
],
row,
);
}
#[test]
fn key_row() {
let row = Key {
id: KeyId(1),
id2: 1,
name: DeviceSQLString::new("Dm".to_string()).unwrap(),
};
test_roundtrip(&[1, 0, 0, 0, 1, 0, 0, 0, 7, 68, 109], row);
}
#[test]
fn color_row() {
let row = Color {
unknown1: 0,
unknown2: 1,
color: ColorIndex::Pink,
unknown3: 0,
name: DeviceSQLString::new("Pink".to_string()).unwrap(),
};
test_roundtrip(&[0, 0, 0, 0, 1, 1, 0, 0, 11, 80, 105, 110, 107], row);
}
}