use std::f64::consts::TAU;
use super::code::{BANDS, TONE_AMPLITUDE, ToneCode};
use crate::FrameRate;
pub const SIGNAL_PEAK: f64 = 0.9;
pub const MUSIC_PEAK: f64 = SIGNAL_PEAK - BANDS as f64 * TONE_AMPLITUDE;
pub const GUARD: u32 = 1;
const BEATS_PER_SECOND: u32 = 2;
const LOOP_BEATS: u32 = 16;
const DO_HZ: f64 = 240.0;
#[derive(Clone, Copy, Debug, PartialEq)]
enum Voice {
Tune,
Chord,
Bass,
}
impl Voice {
fn sound(self) -> (f64, &'static [f64], f64, u64) {
match self {
Voice::Tune => (1.0, &[1.0, 0.2], 0.8, 1),
Voice::Chord => (0.4, &[1.0], 0.15, 0),
Voice::Bass => (0.8, &[1.0, 0.55, 0.3, 0.18, 0.1], 0.4, 1),
}
}
}
use Voice::{Bass, Chord, Tune};
#[rustfmt::skip]
const SCORE: &[(f64, f64, Voice, &[u32])] = &[
(0.0, 0.5, Tune, &[10]), (0.5, 0.5, Tune, &[12]), (1.0, 1.0, Tune, &[16]),
(2.0, 0.5, Tune, &[10]), (2.5, 0.5, Tune, &[12]), (3.0, 1.0, Tune, &[16]),
(4.0, 0.5, Tune, &[15]), (4.5, 0.5, Tune, &[16]), (5.0, 0.5, Tune, &[15]), (5.5, 0.5, Tune, &[12]),
(6.0, 0.5, Tune, &[9]), (6.5, 0.5, Tune, &[12]), (7.0, 1.0, Tune, &[15]),
(8.0, 0.5, Tune, &[16]), (8.5, 0.5, Tune, &[12]),
(9.0, 0.25, Tune, &[10]), (9.25, 0.25, Tune, &[12]), (9.5, 0.25, Tune, &[14]), (9.75, 0.25, Tune, &[15]),
(10.0, 0.5, Tune, &[16]), (10.5, 0.5, Tune, &[14]), (11.0, 0.5, Tune, &[12]), (11.5, 0.5, Tune, &[10]),
(12.0, 0.25, Tune, &[12]), (12.5, 0.25, Tune, &[12]), (13.0, 0.25, Tune, &[15]), (13.5, 0.25, Tune, &[15]),
(14.0, 0.25, Tune, &[9]), (14.25, 0.25, Tune, &[12]), (14.5, 0.5, Tune, &[15]), (15.0, 1.0, Tune, &[16]),
(1.0, 0.5, Chord, &[8, 10, 12]), (3.0, 0.5, Chord, &[8, 10, 12]),
(5.0, 0.5, Chord, &[9, 12, 15]), (7.0, 0.5, Chord, &[9, 12, 15]),
(9.0, 0.5, Chord, &[8, 10, 14]), (11.0, 0.5, Chord, &[8, 10, 14]),
(13.0, 0.5, Chord, &[9, 12, 15]), (15.0, 1.0, Chord, &[8, 10, 12]),
(0.0, 1.0, Bass, &[4]), (2.0, 1.0, Bass, &[3]),
(4.0, 1.0, Bass, &[3]), (6.0, 1.0, Bass, &[6]),
(8.0, 1.0, Bass, &[4]), (10.0, 1.0, Bass, &[3]),
(12.0, 1.0, Bass, &[3]), (14.0, 0.5, Bass, &[3]), (15.0, 1.0, Bass, &[4]),
];
#[derive(Clone, Copy, Debug, PartialEq)]
enum Key {
Times(u32),
Bugle,
}
impl Key {
fn map(self, h: u32) -> u32 {
match self {
Key::Times(n) => h * n,
Key::Bugle => match h {
3 => 3,
9 => 6,
15 => 7,
h => h.div_ceil(2),
},
}
}
fn do_harmonic(self) -> f64 {
f64::from(self.map(8))
}
}
#[derive(Clone, Debug)]
pub struct Music {
fps: FrameRate,
frames: Vec<Vec<(f64, f64)>>,
}
impl Music {
pub fn new(code: &ToneCode) -> Option<Music> {
let fps = code.fps();
let highest = code.base_harmonic().checked_sub(GUARD + 1)?;
let top = SCORE.iter().flat_map(|n| n.3).copied().max().unwrap_or(0);
let key = [Key::Times(4), Key::Times(2), Key::Times(1), Key::Bugle]
.into_iter()
.filter(|k| k.map(top) <= highest)
.min_by(|a, b| {
let off = |k: &Key| (k.do_harmonic() * fps.as_f64() / DO_HZ).log2().abs();
off(a).total_cmp(&off(b))
})?;
let nominal = u64::from(fps.nominal());
let frames_per_beat = nominal as f64 / f64::from(BEATS_PER_SECOND);
let loop_frames = nominal * u64::from(LOOP_BEATS / BEATS_PER_SECOND);
let at_beat = |beat: f64| (beat * frames_per_beat).round() as u64;
let mut table: Vec<Vec<(u32, f64)>> = vec![Vec::new(); loop_frames as usize];
let release = 2;
for &(beat, beats, voice, harmonics) in SCORE {
let (level, overtones, decay, attack) = voice.sound();
let (start, end) = (at_beat(beat), at_beat(beat + beats));
let mut notes: Vec<u32> = harmonics.iter().map(|&h| key.map(h)).collect();
notes.dedup();
for f in start..end + release {
let i = f - start;
let mut env = level * (-(i as f64) / nominal as f64 / decay).exp();
if i < attack {
env *= (i + 1) as f64 / (attack + 1) as f64;
}
if f >= end {
env *= 1.0 - (f - end + 1) as f64 / (release + 1) as f64;
}
let frame = &mut table[(f % loop_frames) as usize];
for &h in ¬es {
for (m, &a) in (1..).zip(overtones) {
if h * m <= highest {
frame.push((h * m, env * a));
}
}
}
}
}
let frames: Vec<Vec<(u32, f64)>> = table
.into_iter()
.map(|mut partials| {
partials.sort_by_key(|p| p.0);
let mut merged: Vec<(u32, f64)> = Vec::new();
for (h, a) in partials {
match merged.last_mut() {
Some(last) if last.0 == h => last.1 += a,
_ => merged.push((h, a)),
}
}
merged
})
.collect();
let loudest = frames
.iter()
.map(|f| f.iter().map(|p| p.1.abs()).sum::<f64>())
.fold(0.0, f64::max);
let scale = if loudest > 0.0 {
MUSIC_PEAK / loudest
} else {
0.0
};
Some(Music {
fps,
frames: frames
.into_iter()
.map(|f| {
f.into_iter()
.map(|(h, a)| (f64::from(h), a * scale))
.collect()
})
.collect(),
})
}
pub fn highest_harmonic(&self) -> u32 {
self.frames
.iter()
.flatten()
.map(|p| p.0 as u32)
.max()
.unwrap_or(0)
}
pub fn signal_at(&self, t: f64) -> f64 {
if t < 0.0 {
return 0.0;
}
let position = t * self.fps.as_f64();
let frame = position.floor();
let frac = position - frame;
let n = self.frames.len() as u64;
self.frames[(frame as u64 % n) as usize]
.iter()
.map(|&(k, a)| a * (TAU * k * frac).sin())
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::detect::CellDetector;
use crate::audio::synth::ToneSynth;
fn code(fps: &str) -> ToneCode {
ToneCode::new(fps.parse().unwrap(), 48000).unwrap()
}
#[test]
fn stays_below_the_code_and_within_full_scale() {
for fps in [
"15", "23.976", "24", "25", "29.97", "30", "48", "50", "59.94", "60",
] {
let code = code(fps);
let music = Music::new(&code).unwrap_or_else(|| panic!("{fps}: no music"));
assert!(
music.highest_harmonic() + GUARD < code.base_harmonic(),
"{fps}"
);
let peak = (0..96_000)
.map(|i| music.signal_at(i as f64 / 48_000.0).abs())
.fold(0.0, f64::max);
assert!(
peak <= MUSIC_PEAK + 1e-9 && peak > 0.5 * MUSIC_PEAK,
"{fps}: peak {peak}"
);
}
for fps in ["100", "120"] {
assert!(Music::new(&code(fps)).is_none(), "{fps}");
}
}
#[test]
fn is_silent_at_frame_boundaries_and_loops() {
let code = code("29.97");
let music = Music::new(&code).unwrap();
for frame in [1i64, 15, 239, 240, 241, 1000] {
let t = code.fps().frame_time(frame);
assert!(music.signal_at(t + 1e-9).abs() < 1e-4, "frame {frame}");
}
let loop_secs = 240.0 / code.fps().as_f64();
for t in [0.01, 1.234, 5.5] {
assert!((music.signal_at(t) - music.signal_at(t + loop_secs)).abs() < 1e-6);
}
assert_eq!(music.signal_at(-1.0), 0.0);
}
#[test]
fn the_decoder_does_not_hear_it() {
for fps in ["24", "29.97", "30", "50", "60"] {
let code = code(fps);
let count = 3 * 48_000;
let render = |music: bool| {
let mut synth = ToneSynth::new(code.clone(), 0.0123);
if music {
synth = synth.with_music();
}
let mut out = vec![0.0; count];
synth.fill(&mut out);
out
};
let detector = CellDetector::new(code.clone());
let plain = detector.detect(&render(false), 0.0);
let mixed = detector.detect(&render(true), 0.0);
assert!(plain.len() > 60, "{fps}: {} cells", plain.len());
assert_eq!(plain.len(), mixed.len(), "{fps}");
for (a, b) in plain.iter().zip(&mixed) {
assert_eq!((a.id, a.sample), (b.id, b.sample), "{fps}");
let shift = (a.residual - b.residual).abs();
assert!(
shift < 1e-6,
"{fps}: frame {} moved {:.3} µs",
a.id,
shift * 1e6
);
}
}
}
}