use crate::rng::StreamRng;
pub const AUTO_HARD_CUT_MODES: u8 = 7;
const BASS_LEVEL: f64 = 1.5;
const TREB_LEVEL: f64 = 2.9f32 as f64;
const TREB_LOUD: f64 = 8.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Effect {
Solarize,
Split,
Multiplicate,
Rotate,
Flip,
Invert,
}
pub const MODE7_EFFECTS: [Effect; 6] = [
Effect::Solarize,
Effect::Split,
Effect::Multiplicate,
Effect::Rotate,
Effect::Flip,
Effect::Invert,
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Site {
BodyStart,
BodyEnd,
}
impl Effect {
pub fn marker(self) -> &'static str {
match self {
Effect::Solarize => "//MilkDrop3 Solarize Mode:",
Effect::Split => "//MilkDrop3 Split Mode:",
Effect::Multiplicate => "//MilkDrop3 Multiplicate Mode:",
Effect::Rotate => "//MilkDrop3 Rotate Mode:",
Effect::Flip => "//MilkDrop3 Flip Mode:",
Effect::Invert => "//MilkDrop3 Invert Mode:",
}
}
pub fn code(self) -> &'static str {
match self {
Effect::Solarize => "ret = ret*(1-ret)*4;",
Effect::Split => "uv = float2 (abs(uv.x-0.5), uv.y);",
Effect::Multiplicate => "uv = 1- abs( frac((uv-.5)*1.5 * 0.5 ) * 2 - 1.0 );",
Effect::Rotate => "uv = uv.yx;",
Effect::Flip => "uv = 1-uv;",
Effect::Invert => "ret = 1 - ret;",
}
}
fn site(self) -> Site {
match self {
Effect::Solarize | Effect::Invert => Site::BodyEnd,
_ => Site::BodyStart,
}
}
}
const SEP: char = '\u{1}';
fn line_end(s: &str, from: usize) -> usize {
s[from..]
.find([SEP, '\n'])
.map_or(s.len(), |i| from + i + 1)
}
pub fn toggle_effect(src: &str, effect: Effect) -> Option<String> {
let marker = effect.marker();
if let Some(p) = src.find(marker) {
let end = line_end(src, line_end(src, p));
let mut out = String::with_capacity(src.len());
out.push_str(&src[..p]);
out.push_str(&src[end..]);
return Some(out);
}
let block = format!("{marker}{SEP}{}{SEP}", effect.code());
match effect.site() {
Site::BodyStart => {
let body = src.find("shader_body")?;
let brace = body + src[body..].find('{')? + 1;
let mut out = String::with_capacity(src.len() + block.len());
out.push_str(&src[..brace]);
out.push_str(&block);
out.push_str(&src[brace..]);
Some(out)
}
Site::BodyEnd => {
let last = src.rfind('}')?;
let mut out = String::with_capacity(last + block.len() + 2);
out.push_str(&src[..last]);
out.push_str(&block);
out.push('}');
out.push(SEP);
Some(out)
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AutoCutOutcome {
pub cuts: u8,
pub inject: Option<Effect>,
}
#[derive(Clone, Debug)]
pub struct AutoCut {
mode: u8,
last: Option<f64>,
counter: u64,
rng: StreamRng,
}
impl AutoCut {
pub fn new(seed: u64, mode: u8) -> Self {
Self {
mode: mode.min(AUTO_HARD_CUT_MODES),
last: None,
counter: 0,
rng: StreamRng::new(seed, "auto_cut_effect"),
}
}
pub fn mode(&self) -> u8 {
self.mode
}
pub fn set_mode(&mut self, mode: u8) {
self.mode = mode.min(AUTO_HARD_CUT_MODES);
}
pub fn cuts_total(&self) -> u64 {
self.counter
}
pub fn update(&mut self, now: f64, bass: f64, treb: f64) -> AutoCutOutcome {
let mut out = AutoCutOutcome::default();
if self.mode == 0 {
return out;
}
let last = match self.last {
Some(l) if l <= now => l,
_ => now,
};
self.last = Some(last);
let t = f64::from((now - last) as f32);
let treb_high = treb > TREB_LEVEL;
match self.mode {
1 => out.cuts = u8::from(bass > BASS_LEVEL && t >= 0.2),
2 => out.cuts = u8::from(treb_high && t >= 0.5),
3 => out.cuts = u8::from(treb_high && t >= 1.0),
4 if treb_high => {
out.cuts = u8::from(t >= 3.0) + u8::from(t >= 1.0 && treb > TREB_LOUD);
}
5 => out.cuts = u8::from(treb_high && t >= 5.0),
6 => out.cuts = u8::from(bass > BASS_LEVEL),
7 if treb_high && t >= 0.5 => {
let r = ((self.rng.next64() >> 49) % MODE7_EFFECTS.len() as u64) as usize;
out.inject = Some(MODE7_EFFECTS[r]);
self.last = Some(now);
}
_ => {}
}
if out.cuts > 0 {
self.last = Some(now);
self.counter += u64::from(out.cuts);
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f64 = 0.16;
fn run(
mode: u8,
frames: usize,
treb: impl Fn(usize) -> f64,
bass: impl Fn(usize) -> f64,
) -> Vec<(usize, AutoCutOutcome)> {
let mut a = AutoCut::new(1, mode);
(0..frames)
.filter_map(|f| {
let o = a.update(f as f64 * DT, bass(f), treb(f));
(o != AutoCutOutcome::default()).then_some((f, o))
})
.collect()
}
fn input_a(mode: u8) -> Vec<(usize, AutoCutOutcome)> {
run(
mode,
40,
|f| match f {
16 | 28 | 38 => 9.0,
2 | 3 | 8 | 9 | 10 | 22 | 23 | 35 => 3.5,
_ => 1.0,
},
|f| {
if matches!(f, 3 | 4 | 5 | 10 | 11 | 25) {
2.0
} else {
1.0
}
},
)
}
fn frames(v: &[(usize, AutoCutOutcome)]) -> Vec<usize> {
v.iter().map(|(f, _)| *f).collect()
}
fn cuts(v: &[(usize, AutoCutOutcome)]) -> Vec<u8> {
v.iter().map(|(_, o)| o.cuts).collect()
}
#[test]
fn mode_1_bass_with_a_200_ms_delay() {
assert_eq!(frames(&input_a(1)), [3, 5, 10, 25]);
}
#[test]
fn mode_2_treble_with_a_500_ms_delay() {
assert_eq!(frames(&input_a(2)), [8, 16, 22, 28, 35]);
}
#[test]
fn mode_3_treble_with_a_1_s_delay() {
assert_eq!(frames(&input_a(3)), [8, 16, 23, 35]);
}
#[test]
fn mode_4_only_the_loud_branch_fires_on_input_a() {
let v = input_a(4);
assert_eq!(frames(&v), [16, 28, 38]);
assert_eq!(cuts(&v), [1, 1, 1]);
}
#[test]
fn mode_4_can_cut_twice_in_one_frame() {
let v = run(
4,
60,
|f| match f {
30 | 55 => 9.0,
2 | 20 => 3.5,
_ => 1.0,
},
|_| 1.0,
);
assert_eq!(frames(&v), [20, 30, 55]);
assert_eq!(cuts(&v), [1, 1, 2]);
}
#[test]
fn mode_5_treble_with_a_5_s_delay() {
assert_eq!(frames(&input_a(5)), [35]);
let v = run(
5,
40,
|f| {
if matches!(f, 10 | 20 | 33 | 34) {
3.0
} else {
1.0
}
},
|_| 1.0,
);
assert_eq!(frames(&v), [33]);
}
#[test]
fn mode_6_cuts_on_every_loud_bass_frame() {
assert_eq!(frames(&input_a(6)), [3, 4, 5, 10, 11, 25]);
}
#[test]
fn mode_7_injects_without_cutting() {
let v = input_a(7);
assert_eq!(frames(&v), [8, 16, 22, 28, 35]);
for (_, o) in &v {
assert_eq!(o.cuts, 0);
assert!(o.inject.is_some());
}
}
#[test]
fn mode_7_effect_choice_is_seeded_and_reaches_all_six() {
let pick = |seed| {
let mut a = AutoCut::new(seed, 7);
(0..600)
.filter_map(|f| a.update(f as f64, 1.0, 3.5).inject)
.collect::<Vec<_>>()
};
let a = pick(7);
assert_eq!(a, pick(7));
for e in MODE7_EFFECTS {
assert!(a.contains(&e), "{e:?} never picked");
}
}
#[test]
fn modes_off_and_no_audio_never_fire() {
for mode in 0..=7 {
assert!(run(mode, 400, |_| 1.0, |_| 1.0).is_empty(), "mode {mode}");
}
assert!(run(0, 40, |_| 9.0, |_| 9.0).is_empty());
}
#[test]
fn thresholds_are_strict_and_the_treble_one_is_the_float_2_9() {
assert!(run(2, 10, |_| 2.9, |_| 1.0).is_empty());
assert_eq!(frames(&run(2, 10, |_| 2.91, |_| 1.0)), [4, 8]);
assert!(run(1, 10, |_| 1.0, |_| 1.5).is_empty());
}
#[test]
fn the_first_evaluation_anchors_the_timer_and_a_backwards_clock_re_anchors_it() {
let mut a = AutoCut::new(1, 3);
assert_eq!(a.update(100.0, 1.0, 3.5).cuts, 0);
assert_eq!(a.update(100.9, 1.0, 3.5).cuts, 0);
assert_eq!(a.update(101.1, 1.0, 3.5).cuts, 1);
assert_eq!(a.update(0.0, 1.0, 3.5).cuts, 0);
assert_eq!(a.update(1.1, 1.0, 3.5).cuts, 1);
assert_eq!(a.cuts_total(), 2);
}
#[test]
fn a_mode_change_keeps_the_last_event() {
let mut a = AutoCut::new(1, 2);
assert_eq!(a.update(0.0, 1.0, 1.0).cuts, 0);
a.set_mode(0);
assert_eq!(a.update(5.0, 1.0, 9.0), AutoCutOutcome::default());
a.set_mode(200);
assert_eq!(a.mode(), 7);
a.set_mode(3);
assert_eq!(a.update(6.0, 1.0, 9.0).cuts, 1);
}
#[test]
fn effects_toggle_in_the_shader_text() {
let src = "shader_body\u{1}{\u{1}ret = tex2D(sampler_main, uv).xyz;\u{1}}\u{1}";
let a = toggle_effect(src, Effect::Flip).unwrap();
assert_eq!(
a,
"shader_body\u{1}{//MilkDrop3 Flip Mode:\u{1}uv = 1-uv;\u{1}\u{1}ret = tex2D(sampler_main, uv).xyz;\u{1}}\u{1}"
);
assert_eq!(toggle_effect(&a, Effect::Flip).unwrap(), src);
let b = toggle_effect(src, Effect::Invert).unwrap();
assert_eq!(
b,
"shader_body\u{1}{\u{1}ret = tex2D(sampler_main, uv).xyz;\u{1}//MilkDrop3 Invert Mode:\u{1}ret = 1 - ret;\u{1}}\u{1}"
);
assert_eq!(
toggle_effect(&b, Effect::Invert).unwrap(),
"shader_body\u{1}{\u{1}ret = tex2D(sampler_main, uv).xyz;\u{1}}\u{1}"
);
let c = toggle_effect(&b, Effect::Split).unwrap();
assert!(c.contains("Split Mode") && c.contains("Invert Mode"));
let d = toggle_effect(&c, Effect::Split).unwrap();
assert_eq!(d, b);
let pre = "float f(float x) { return x; }\u{1}shader_body\u{1}{\u{1}}\u{1}";
let e = toggle_effect(pre, Effect::Rotate).unwrap();
assert!(e.starts_with(
"float f(float x) { return x; }\u{1}shader_body\u{1}{//MilkDrop3 Rotate Mode:"
));
}
#[test]
fn effects_need_an_insertion_point() {
assert_eq!(toggle_effect("ret = 1;", Effect::Flip), None);
assert_eq!(toggle_effect("shader_body ret = 1;", Effect::Flip), None);
assert_eq!(
toggle_effect("shader_body { ret = 1;", Effect::Invert),
None
);
}
#[test]
fn removal_takes_the_marker_line_and_the_code_line_only() {
let s = "a;\u{1}//MilkDrop3 Rotate Mode:\u{1}uv = uv.yx;\u{1}b;\u{1}";
assert_eq!(toggle_effect(s, Effect::Rotate).unwrap(), "a;\u{1}b;\u{1}");
assert_eq!(
toggle_effect("x;\u{1}//MilkDrop3 Rotate Mode:", Effect::Rotate).unwrap(),
"x;\u{1}"
);
}
#[test]
fn injected_composite_shaders_still_translate() {
use lactea_abi::PassKind;
use lactea_hlsl::{Options, default_comp_shader, translate};
let texts = [
default_comp_shader(1.2, 0.5, 1.25, 1, 0.3, true, false, true, false, true)
.replace('\n', "\u{1}"),
"shader_body\u{1}{\u{1} ret = tex2D(sampler_main, uv).xyz;\u{1}}\u{1}".to_string(),
"float3 tint(float3 c) { return c * 0.5; }\u{1}shader_body\u{1}{ // body\u{1} float2 d = uv - 0.5;\u{1} ret = tint(tex2D(sampler_main, uv + d * 0.1).xyz); }\u{1}"
.to_string(),
];
let opts = Options::default();
for text in &texts {
for effect in MODE7_EFFECTS {
let once = toggle_effect(text, effect).unwrap();
translate(PassKind::Comp, &once.replace(SEP, "\n"), &opts)
.unwrap_or_else(|e| panic!("{effect:?} on {text:?}: {e}"));
let mut all = text.clone();
for e in MODE7_EFFECTS {
all = toggle_effect(&all, e).unwrap();
}
translate(PassKind::Comp, &all.replace(SEP, "\n"), &opts)
.unwrap_or_else(|e| panic!("all effects on {text:?}: {e}"));
}
}
}
}