1use memmap2::Mmap;
2use rayon::iter::{IntoParallelIterator, ParallelIterator};
3use std::{
4 env,
5 error::Error,
6 fmt::Debug,
7 fs::{self, File, OpenOptions},
8 io::{self, BufWriter, Write},
9 mem::size_of,
10 ops::Range,
11 path::{Path, PathBuf},
12 str::{from_utf8, from_utf8_unchecked},
13 thread::{self, JoinHandle},
14 time::Instant,
15};
16use symphonia::{
17 core::{
18 formats::FormatOptions,
19 io::{MediaSourceStream, MediaSourceStreamOptions},
20 meta::{MetadataOptions, MetadataRevision, StandardTagKey},
21 probe::Hint,
22 },
23 default::get_probe,
24};
25use walkdir::{DirEntry, WalkDir};
26
27pub const SONG_LEN: usize = TEXT_LEN + 2 + 4;
28pub const TEXT_LEN: usize = 522;
29
30pub const NUMBER_POS: usize = SONG_LEN - 1 - 4 - 2;
31pub const DISC_POS: usize = SONG_LEN - 1 - 4 - 1;
32pub const GAIN_POS: Range<usize> = SONG_LEN - 1 - 4..SONG_LEN - 1;
33
34mod index;
35mod playlist;
36mod query;
37
38pub use index::*;
39pub use playlist::*;
40pub use query::*;
41
42pub static mut MMAP: Option<Mmap> = None;
43pub static mut SETTINGS: Settings = Settings::default();
44
45pub fn database_path() -> PathBuf {
46 let gonk = if cfg!(windows) {
47 PathBuf::from(&env::var("APPDATA").unwrap())
48 } else {
49 PathBuf::from(&env::var("HOME").unwrap()).join(".config")
50 }
51 .join("gonk");
52
53 if !gonk.exists() {
54 fs::create_dir_all(&gonk).unwrap();
55 }
56
57 gonk.join("gonk_new.db")
58}
59
60pub fn settings_path() -> PathBuf {
61 let mut path = database_path();
62 path.pop();
63 path.push("settings.db");
64 path
65}
66
67pub fn init() -> Result<(), Box<dyn Error>> {
69 match fs::read(&settings_path()) {
71 Ok(bytes) if !bytes.is_empty() => unsafe { SETTINGS = Settings::from(bytes) },
72 _ => save_settings(),
74 }
75
76 let file = OpenOptions::new()
78 .read(true)
79 .write(true)
80 .create(true)
81 .open(&database_path())
82 .unwrap();
83
84 unsafe { MMAP = Some(Mmap::map(&file).unwrap()) };
85
86 if validate().is_err() {
88 reset()?;
90 }
91
92 Ok(())
93}
94
95fn validate() -> Result<(), Box<dyn Error>> {
96 let mmap = mmap().unwrap();
97
98 if mmap.is_empty() {
99 return Ok(());
100 } else if mmap.len() < SONG_LEN {
101 return Err("Invalid song")?;
102 }
103 let text = &mmap[..TEXT_LEN];
104 let artist_len = u16::from_le_bytes(text[0..2].try_into()?) as usize;
105 if artist_len > TEXT_LEN {
106 Err("Invalid u16")?;
107 }
108 let _artist = from_utf8(&text[2..artist_len + 2])?;
109
110 let album_len =
111 u16::from_le_bytes(text[2 + artist_len..2 + artist_len + 2].try_into()?) as usize;
112 if album_len > TEXT_LEN {
113 Err("Invalid u16")?;
114 }
115 let album = 2 + artist_len + 2..artist_len + 2 + album_len + 2;
116 let _album = from_utf8(&text[album])?;
117
118 let title_len = u16::from_le_bytes(
119 text[2 + artist_len + 2 + album_len..2 + artist_len + 2 + album_len + 2].try_into()?,
120 ) as usize;
121 if title_len > TEXT_LEN {
122 Err("Invalid u16")?;
123 }
124 let title = 2 + artist_len + 2 + album_len + 2..artist_len + 2 + album_len + 2 + title_len + 2;
125 let _title = from_utf8(&text[title])?;
126
127 let path_len = u16::from_le_bytes(
128 text[2 + artist_len + 2 + album_len + 2 + title_len
129 ..2 + artist_len + 2 + album_len + 2 + title_len + 2]
130 .try_into()?,
131 ) as usize;
132 if path_len > TEXT_LEN {
133 Err("Invalid u16")?;
134 }
135 let path = 2 + artist_len + 2 + album_len + 2 + title_len + 2
136 ..artist_len + 2 + album_len + 2 + title_len + 2 + path_len + 2;
137 let _path = from_utf8(&text[path])?;
138
139 let _number = mmap[NUMBER_POS];
140 let _disc = mmap[DISC_POS];
141 let _gain = f32::from_le_bytes(mmap[GAIN_POS].try_into()?);
142
143 Ok(())
144}
145
146pub fn reset() -> io::Result<()> {
147 if let Some(mmap) = unsafe { MMAP.take() } {
148 drop(mmap);
149 }
150 fs::remove_file(settings_path())?;
151 fs::remove_file(database_path())
152}
153
154#[derive(Debug)]
156pub struct Settings {
157 pub volume: u8,
158 pub index: u16,
159 pub elapsed: f32,
160 pub output_device: String,
161 pub music_folder: String,
162 pub queue: Vec<RawSong>,
163}
164
165impl Settings {
166 pub const fn default() -> Self {
167 Self {
168 volume: 15,
169 index: 0,
170 elapsed: 0.0,
171 output_device: String::new(),
172 music_folder: String::new(),
173 queue: Vec::new(),
174 }
175 }
176 pub fn into_bytes(&self) -> Vec<u8> {
177 let mut bytes = Vec::new();
178 bytes.push(self.volume);
179 bytes.extend(self.index.to_le_bytes());
180 bytes.extend(self.elapsed.to_le_bytes());
181 bytes.extend(self.output_device.replace('\0', "").as_bytes());
182 bytes.push(b'\0');
183 bytes.extend(self.music_folder.replace('\0', "").as_bytes());
184 bytes.push(b'\0');
185 for song in &self.queue {
186 bytes.extend(song.into_bytes());
187 }
188 bytes
189 }
190 pub fn from(bytes: Vec<u8>) -> Self {
191 unsafe {
192 let volume = bytes[0];
193 let index = u16::from_le_bytes(bytes[1..3].try_into().unwrap_unchecked());
194 let elapsed = f32::from_le_bytes(bytes[3..7].try_into().unwrap_unchecked());
195 let end = bytes[7..].iter().position(|&c| c == b'\0').unwrap() + 7;
196 let output_device = from_utf8_unchecked(&bytes[7..end]).to_string();
197 let old_end = end + 1;
198 let end = bytes[old_end..].iter().position(|&c| c == b'\0').unwrap() + old_end;
199 let music_folder = from_utf8_unchecked(&bytes[old_end..end]).to_string();
200
201 let mut queue = Vec::new();
202 let mut i = end + 1;
204 while let Some(bytes) = bytes.get(i..i + SONG_LEN) {
205 queue.push(RawSong::from(bytes));
206 i += SONG_LEN;
207 }
208
209 Self {
210 index,
211 volume,
212 output_device,
213 music_folder,
214 elapsed,
215 queue,
216 }
217 }
218 }
219}
220
221pub fn save_settings() {
222 let file = File::create(settings_path()).unwrap();
224 let mut writer = BufWriter::new(file);
225 let bytes = unsafe { SETTINGS.into_bytes() };
226 writer.write_all(&bytes).unwrap();
227 writer.flush().unwrap();
228}
229
230pub fn update_volume(new_volume: u8) {
231 unsafe {
232 SETTINGS.volume = new_volume;
233 save_settings();
234 }
235}
236
237pub fn update_queue(queue: &[Song], index: u16, elapsed: f32) {
238 unsafe {
239 SETTINGS.queue = queue.iter().map(RawSong::from).collect();
240 SETTINGS.index = index;
241 SETTINGS.elapsed = elapsed;
242 save_settings();
243 }
244}
245
246pub fn update_output_device(device: &str) {
247 unsafe {
248 SETTINGS.output_device = device.to_string();
249 save_settings();
250 }
251}
252
253pub fn update_music_folder(folder: &str) {
254 unsafe {
255 SETTINGS.music_folder = folder.replace('\\', "/");
256 save_settings();
257 }
258}
259
260pub fn get_queue() -> (Vec<Song>, Option<usize>, f32) {
261 unsafe {
262 let index = if SETTINGS.queue.is_empty() {
263 None
264 } else {
265 Some(SETTINGS.index as usize)
266 };
267 (
268 SETTINGS
269 .queue
270 .iter()
271 .map(|song| Song::from(&song.into_bytes(), 0))
272 .collect(),
273 index,
274 SETTINGS.elapsed,
275 )
276 }
277}
278
279pub fn get_output_device() -> &'static str {
280 unsafe { &SETTINGS.output_device }
281}
282
283pub fn get_music_folder() -> &'static str {
284 unsafe { &SETTINGS.music_folder }
285}
286
287pub fn volume() -> u8 {
288 unsafe { SETTINGS.volume }
289}
290
291pub fn mmap() -> Option<&'static Mmap> {
292 unsafe { MMAP.as_ref() }
293}
294
295pub fn scan(path: String) -> JoinHandle<()> {
296 unsafe {
297 let mmap = MMAP.take().unwrap();
298 drop(mmap);
299 debug_assert!(MMAP.is_none());
300 }
301
302 thread::spawn(|| {
303 let file = OpenOptions::new()
304 .write(true)
305 .read(true)
306 .truncate(true)
307 .open(&database_path())
308 .unwrap();
309 let mut writer = BufWriter::new(&file);
310
311 let paths: Vec<DirEntry> = WalkDir::new(path)
312 .into_iter()
313 .flatten()
314 .filter(|path| match path.path().extension() {
315 Some(ex) => {
316 matches!(ex.to_str(), Some("flac" | "mp3" | "ogg" | "wav" | "m4a"))
317 }
318 None => false,
319 })
320 .collect();
321
322 let songs: Vec<RawSong> = paths
323 .into_par_iter()
324 .map(|path| RawSong::from(path.path()))
325 .collect();
326
327 for song in songs {
328 writer.write_all(&song.into_bytes()).unwrap();
329 }
330
331 writer.flush().unwrap();
332 unsafe { MMAP = Some(Mmap::map(&file).unwrap()) };
333 })
334}
335
336#[derive(Clone, Debug)]
337pub struct Song {
338 pub artist: String,
339 pub album: String,
340 pub title: String,
341 pub path: String,
342 pub number: u8,
343 pub disc: u8,
344 pub gain: f32,
345 pub id: usize,
346}
347
348impl PartialEq for Song {
349 fn eq(&self, other: &Self) -> bool {
350 self.artist == other.artist
351 && self.album == other.album
352 && self.title == other.title
353 && self.path == other.path
354 && self.number == other.number
355 && self.disc == other.disc
356 && self.gain == other.gain
357 && self.id == other.id
358 }
359}
360
361impl PartialOrd for Song {
362 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
363 if self.artist == other.artist {
364 if self.album == other.album {
365 if self.disc == other.disc {
366 self.number.partial_cmp(&other.number)
367 } else {
368 self.disc.partial_cmp(&other.disc)
369 }
370 } else {
371 self.album.partial_cmp(&other.album)
372 }
373 } else {
374 self.artist.partial_cmp(&other.artist)
375 }
376 }
377}
378
379impl Eq for Song {}
380
381impl Ord for Song {
382 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
383 self.partial_cmp(other).unwrap()
384 }
385}
386
387impl Song {
388 pub fn from(bytes: &[u8], id: usize) -> Self {
389 unsafe {
390 let text = &bytes[..TEXT_LEN];
391 let artist_len = u16::from_le_bytes(text[0..2].try_into().unwrap_unchecked()) as usize;
392 let artist = from_utf8_unchecked(&text[2..artist_len + 2]);
393
394 let album_len = u16::from_le_bytes(
395 text[2 + artist_len..2 + artist_len + 2]
396 .try_into()
397 .unwrap_unchecked(),
398 ) as usize;
399 let album = 2 + artist_len + 2..artist_len + 2 + album_len + 2;
400 let album = from_utf8_unchecked(&text[album]);
401
402 let title_len = u16::from_le_bytes(
403 text[2 + artist_len + 2 + album_len..2 + artist_len + 2 + album_len + 2]
404 .try_into()
405 .unwrap_unchecked(),
406 ) as usize;
407 let title =
408 2 + artist_len + 2 + album_len + 2..artist_len + 2 + album_len + 2 + title_len + 2;
409 let title = from_utf8_unchecked(&text[title]);
410
411 let path_len = u16::from_le_bytes(
412 text[2 + artist_len + 2 + album_len + 2 + title_len
413 ..2 + artist_len + 2 + album_len + 2 + title_len + 2]
414 .try_into()
415 .unwrap_unchecked(),
416 ) as usize;
417 let path = 2 + artist_len + 2 + album_len + 2 + title_len + 2
418 ..artist_len + 2 + album_len + 2 + title_len + 2 + path_len + 2;
419 let path = from_utf8_unchecked(&text[path]);
420
421 let number = bytes[NUMBER_POS];
422 let disc = bytes[DISC_POS];
423 let gain = f32::from_le_bytes(bytes[GAIN_POS].try_into().unwrap_unchecked());
424
425 Self {
426 artist: artist.to_string(),
427 album: album.to_string(),
428 title: title.to_string(),
429 path: path.to_string(),
430 number,
431 disc,
432 gain,
433 id,
434 }
435 }
436 }
437}
438
439pub struct RawSong {
440 pub text: [u8; TEXT_LEN],
441 pub number: u8,
442 pub disc: u8,
443 pub gain: f32,
444}
445
446impl RawSong {
447 pub fn new(
448 artist: &str,
449 album: &str,
450 title: &str,
451 path: &str,
452 number: u8,
453 disc: u8,
454 gain: f32,
455 ) -> Self {
456 if path.len() > TEXT_LEN {
457 panic!("PATH IS TOO LONG! {}", path)
458 }
459
460 let mut artist = artist.to_string();
461 let mut album = album.to_string();
462 let mut title = title.to_string();
463
464 let mut i = 0;
467 while artist.len() + album.len() + title.len() + path.len()
468 > TEXT_LEN - (4 * size_of::<u16>())
469 {
470 if i % 3 == 0 {
471 artist.pop();
472 } else if i % 3 == 1 {
473 album.pop();
474 } else {
475 title.pop();
476 }
477 i += 1;
478 }
479
480 let artist = [&(artist.len() as u16).to_le_bytes(), artist.as_bytes()].concat();
481 let album = [&(album.len() as u16).to_le_bytes(), album.as_bytes()].concat();
482 let title = [&(title.len() as u16).to_le_bytes(), title.as_bytes()].concat();
483 let path = [&(path.len() as u16).to_le_bytes(), path.as_bytes()].concat();
484
485 let mut text = [0u8; TEXT_LEN];
486
487 let artist_pos = artist.len();
488 let album_pos = artist_pos + album.len();
489 let title_pos = album_pos + title.len();
490 let path_pos = title_pos + path.len();
491
492 text[..artist_pos].copy_from_slice(&artist);
493 text[artist_pos..album_pos].copy_from_slice(&album);
494 text[album_pos..title_pos].copy_from_slice(&title);
495 text[title_pos..path_pos].copy_from_slice(&path);
496
497 Self {
498 text,
499 number,
500 disc,
501 gain,
502 }
503 }
504 pub fn into_bytes(&self) -> [u8; SONG_LEN] {
505 let mut song = [0u8; SONG_LEN];
506 song[..TEXT_LEN].copy_from_slice(&self.text);
507 song[NUMBER_POS] = self.number;
508 song[DISC_POS] = self.disc;
509 song[GAIN_POS].copy_from_slice(&self.gain.to_le_bytes());
510 song
511 }
512 pub fn artist(&self) -> &str {
513 artist(&self.text)
514 }
515 pub fn album(&self) -> &str {
516 album(&self.text)
517 }
518 pub fn title(&self) -> &str {
519 title(&self.text)
520 }
521 pub fn path(&self) -> &str {
522 path(&self.text)
523 }
524}
525
526impl Debug for RawSong {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 let title = title(&self.text);
529 let album = album(&self.text);
530 let artist = artist(&self.text);
531 let path = path(&self.text);
532 f.debug_struct("Song")
533 .field("artist", &artist)
534 .field("album", &album)
535 .field("title", &title)
536 .field("path", &path)
537 .field("number", &self.number)
538 .field("disc", &self.disc)
539 .field("gain", &self.gain)
540 .finish()
541 }
542}
543
544impl From<&'_ [u8]> for RawSong {
545 fn from(bytes: &[u8]) -> Self {
546 Self {
547 text: bytes[..TEXT_LEN].try_into().unwrap(),
548 number: bytes[NUMBER_POS],
549 disc: bytes[DISC_POS],
550 gain: f32::from_le_bytes(bytes[GAIN_POS].try_into().unwrap()),
551 }
552 }
553}
554
555impl From<&Song> for RawSong {
556 fn from(song: &Song) -> Self {
557 RawSong::new(
558 &song.artist,
559 &song.album,
560 &song.title,
561 &song.path,
562 song.number,
563 song.disc,
564 song.gain,
565 )
566 }
567}
568
569impl From<&'_ Path> for RawSong {
570 fn from(path: &'_ Path) -> Self {
571 let file = Box::new(File::open(path).expect("Could not open file."));
572 let mss = MediaSourceStream::new(file, MediaSourceStreamOptions::default());
573
574 let mut probe = match get_probe().format(
575 &Hint::new(),
576 mss,
577 &FormatOptions::default(),
578 &MetadataOptions::default(),
579 ) {
580 Ok(probe) => probe,
581 Err(_) => panic!("{:?}", path),
582 };
583
584 let mut title = String::from("Unknown Title");
585 let mut album = String::from("Unknown Album");
586 let mut artist = String::from("Unknown Artist");
587 let mut number = 1;
588 let mut disc = 1;
589 let mut gain = 0.0;
590
591 let mut update_metadata = |metadata: &MetadataRevision| {
592 for tag in metadata.tags() {
593 if let Some(std_key) = tag.std_key {
594 match std_key {
595 StandardTagKey::AlbumArtist => artist = tag.value.to_string(),
596 StandardTagKey::Artist if artist == "Unknown Artist" => {
597 artist = tag.value.to_string()
598 }
599 StandardTagKey::Album => album = tag.value.to_string(),
600 StandardTagKey::TrackTitle => title = tag.value.to_string(),
601 StandardTagKey::TrackNumber => {
602 let num = tag.value.to_string();
603 if let Some((num, _)) = num.split_once('/') {
604 number = num.parse().unwrap_or(1);
605 } else {
606 number = num.parse().unwrap_or(1);
607 }
608 }
609 StandardTagKey::DiscNumber => {
610 let num = tag.value.to_string();
611 if let Some((num, _)) = num.split_once('/') {
612 disc = num.parse().unwrap_or(1);
613 } else {
614 disc = num.parse().unwrap_or(1);
615 }
616 }
617 StandardTagKey::ReplayGainTrackGain => {
618 let db = tag
619 .value
620 .to_string()
621 .split(' ')
622 .next()
623 .unwrap()
624 .parse()
625 .unwrap_or(0.0);
626
627 gain = 10.0f32.powf(db / 20.0);
628 }
629 _ => (),
630 }
631 }
632 }
633 };
634
635 if let Some(metadata) = probe.format.metadata().skip_to_latest() {
636 update_metadata(metadata);
637 } else if let Some(mut metadata) = probe.metadata.get() {
638 let metadata = metadata.skip_to_latest().unwrap();
639 update_metadata(metadata);
640 } else {
641 }
643
644 RawSong::new(
645 &artist,
646 &album,
647 &title,
648 &path.to_string_lossy(),
649 number,
650 disc,
651 gain,
652 )
653 }
654}
655
656pub fn bench<F>(func: F)
657where
658 F: Fn(),
659{
660 let now = Instant::now();
661 for _ in 0..100_000 {
662 func();
663 }
664 println!("{:?}", now.elapsed());
665}
666
667pub fn bench_slow<F>(func: F)
668where
669 F: Fn(),
670{
671 let now = Instant::now();
672 for _ in 0..4000 {
673 func();
674 }
675 println!("{:?}", now.elapsed());
676}
677
678pub fn bench_super_slow<F>(func: F)
679where
680 F: Fn(),
681{
682 let now = Instant::now();
683 for _ in 0..500 {
684 func();
685 }
686 println!("{:?}", now.elapsed());
687}
688
689#[cfg(test)]
690mod tests {
691 use crate::*;
692
693 #[test]
694 fn clamp_song() {
695 let song = RawSong::new(
696 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
697 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
698 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
699 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
700 1,
701 1,
702 0.25,
703 );
704 assert_eq!(song.artist().len(), 126);
705 assert_eq!(song.album().len(), 127);
706 assert_eq!(song.title().len(), 127);
707 assert_eq!(song.path().len(), 134);
708 assert_eq!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".len(), 134);
709 }
710
711 #[test]
712 fn settings() {
713 let mut queue = Vec::new();
714 for i in 0..100 {
715 let song = RawSong::new(
716 &format!("{} artist", i),
717 &format!("{} album", i),
718 &format!("{} title", i),
719 &format!("{} path", i),
720 1,
721 1,
722 0.25,
723 );
724 queue.push(song)
725 }
726 let settings = Settings {
727 volume: 15,
728 index: 1,
729 elapsed: 0.25,
730 output_device: String::from("output device"),
731 music_folder: String::from("music folder"),
732 queue,
733 };
734 let bytes = settings.into_bytes();
735 let new_settings = Settings::from(bytes);
736
737 assert_eq!(settings.volume, new_settings.volume);
738 assert_eq!(settings.index, new_settings.index);
739 assert_eq!(settings.elapsed, new_settings.elapsed);
740 assert_eq!(settings.output_device, new_settings.output_device);
741 assert_eq!(settings.music_folder, new_settings.music_folder);
742
743 assert_ne!(settings.queue[0].text, new_settings.queue[0].text);
745 }
746
747 #[test]
748 fn database() {
749 let mut db = Vec::new();
750 for i in 0..10_000 {
751 let song = RawSong::new(
752 &format!("{} artist", i),
753 &format!("{} album", i),
754 &format!("{} title", i),
755 &format!("{} path", i),
756 1,
757 1,
758 0.25,
759 );
760 db.extend(song.into_bytes());
761 }
762
763 assert_eq!(db.len(), 5280000);
764 assert_eq!(db.len() / SONG_LEN, 10_000);
765 assert_eq!(artist(&db[..TEXT_LEN]), "0 artist");
766 assert_eq!(album(&db[..TEXT_LEN]), "0 album");
767 assert_eq!(title(&db[..TEXT_LEN]), "0 title");
768 assert_eq!(path(&db[..TEXT_LEN]), "0 path");
769 assert_eq!(artist_and_album(&db[..TEXT_LEN]), ("0 artist", "0 album"));
770
771 assert_eq!(
772 artist(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
773 "1000 artist"
774 );
775 assert_eq!(
776 album(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
777 "1000 album"
778 );
779 assert_eq!(
780 title(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
781 "1000 title"
782 );
783 assert_eq!(
784 path(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
785 "1000 path"
786 );
787 assert_eq!(
788 artist_and_album(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
789 ("1000 artist", "1000 album")
790 );
791
792 let song = Song::from(&db[..SONG_LEN], 0);
793 assert_eq!(song.artist, "0 artist");
794 assert_eq!(song.album, "0 album");
795 assert_eq!(song.title, "0 title");
796 assert_eq!(song.path, "0 path");
797 assert_eq!(song.number, 1);
798 assert_eq!(song.disc, 1);
799 assert_eq!(song.gain, 0.25);
800 }
801}