Feat: demo songs
This commit is contained in:
@@ -95,6 +95,8 @@ pub enum Op {
|
|||||||
Ramp,
|
Ramp,
|
||||||
Triangle,
|
Triangle,
|
||||||
Range,
|
Range,
|
||||||
|
LinMap,
|
||||||
|
ExpMap,
|
||||||
Perlin,
|
Perlin,
|
||||||
Loop,
|
Loop,
|
||||||
Degree(&'static [i64]),
|
Degree(&'static [i64]),
|
||||||
|
|||||||
@@ -1083,6 +1083,28 @@ impl Forth {
|
|||||||
let val = pop_float(stack)?;
|
let val = pop_float(stack)?;
|
||||||
stack.push(Value::Float(min + val * (max - min), None));
|
stack.push(Value::Float(min + val * (max - min), None));
|
||||||
}
|
}
|
||||||
|
Op::LinMap => {
|
||||||
|
let out_hi = pop_float(stack)?;
|
||||||
|
let out_lo = pop_float(stack)?;
|
||||||
|
let in_hi = pop_float(stack)?;
|
||||||
|
let in_lo = pop_float(stack)?;
|
||||||
|
let val = pop_float(stack)?;
|
||||||
|
let t = if (in_hi - in_lo).abs() < f64::EPSILON {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(val - in_lo) / (in_hi - in_lo)
|
||||||
|
};
|
||||||
|
stack.push(Value::Float(out_lo + t * (out_hi - out_lo), None));
|
||||||
|
}
|
||||||
|
Op::ExpMap => {
|
||||||
|
let hi = pop_float(stack)?;
|
||||||
|
let lo = pop_float(stack)?;
|
||||||
|
let val = pop_float(stack)?;
|
||||||
|
if lo <= 0.0 || hi <= 0.0 {
|
||||||
|
return Err("expmap requires positive bounds".into());
|
||||||
|
}
|
||||||
|
stack.push(Value::Float(lo * (hi / lo).powf(val), None));
|
||||||
|
}
|
||||||
Op::Perlin => {
|
Op::Perlin => {
|
||||||
let freq = pop_float(stack)?;
|
let freq = pop_float(stack)?;
|
||||||
let val = perlin_noise_1d(freq * ctx.beat);
|
let val = perlin_noise_1d(freq * ctx.beat);
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"ramp" => Op::Ramp,
|
"ramp" => Op::Ramp,
|
||||||
"triangle" => Op::Triangle,
|
"triangle" => Op::Triangle,
|
||||||
"range" => Op::Range,
|
"range" => Op::Range,
|
||||||
|
"linmap" => Op::LinMap,
|
||||||
|
"expmap" => Op::ExpMap,
|
||||||
"perlin" => Op::Perlin,
|
"perlin" => Op::Perlin,
|
||||||
"loop" => Op::Loop,
|
"loop" => Op::Loop,
|
||||||
"oct" => Op::Oct,
|
"oct" => Op::Oct,
|
||||||
|
|||||||
@@ -354,6 +354,26 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "linmap",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Arithmetic",
|
||||||
|
stack: "(val inlo inhi outlo outhi -- mapped)",
|
||||||
|
desc: "Linear map from [inlo,inhi] to [outlo,outhi]",
|
||||||
|
example: "64 0 127 200 2000 linmap => 1007.87",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "expmap",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Arithmetic",
|
||||||
|
stack: "(val lo hi -- mapped)",
|
||||||
|
desc: "Exponential map from [0,1] to [lo,hi]",
|
||||||
|
example: "0.5 200 8000 expmap => 1264.91",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
// Comparison
|
// Comparison
|
||||||
Word {
|
Word {
|
||||||
name: "=",
|
name: "=",
|
||||||
|
|||||||
@@ -101,7 +101,11 @@ pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
|||||||
|
|
||||||
pub fn load(path: &Path) -> Result<Project, FileError> {
|
pub fn load(path: &Path) -> Result<Project, FileError> {
|
||||||
let json = fs::read_to_string(path)?;
|
let json = fs::read_to_string(path)?;
|
||||||
let file: ProjectFile = serde_json::from_str(&json)?;
|
load_str(&json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_str(json: &str) -> Result<Project, FileError> {
|
||||||
|
let file: ProjectFile = serde_json::from_str(json)?;
|
||||||
if file.version > VERSION {
|
if file.version > VERSION {
|
||||||
return Err(FileError::Version(file.version));
|
return Err(FileError::Version(file.version));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ pub const MAX_PATTERNS: usize = 32;
|
|||||||
pub const MAX_STEPS: usize = 1024;
|
pub const MAX_STEPS: usize = 1024;
|
||||||
pub const DEFAULT_LENGTH: usize = 16;
|
pub const DEFAULT_LENGTH: usize = 16;
|
||||||
|
|
||||||
pub use file::{load, save, FileError};
|
pub use file::{load, load_str, save, FileError};
|
||||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||||
|
|||||||
8370
demos/01.cagire
Normal file
8370
demos/01.cagire
Normal file
File diff suppressed because it is too large
Load Diff
8370
demos/02.cagire
Normal file
8370
demos/02.cagire
Normal file
File diff suppressed because it is too large
Load Diff
8370
demos/03.cagire
Normal file
8370
demos/03.cagire
Normal file
File diff suppressed because it is too large
Load Diff
1
demos/04.cagire
Normal file
1
demos/04.cagire
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"banks":[],"tempo":120.0,"playing_patterns":[[0,0]],"prelude":""}
|
||||||
1
demos/05.cagire
Normal file
1
demos/05.cagire
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"banks":[],"tempo":120.0,"playing_patterns":[[0,0]],"prelude":""}
|
||||||
1
demos/06.cagire
Normal file
1
demos/06.cagire
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"banks":[],"tempo":120.0,"playing_patterns":[[0,0]],"prelude":""}
|
||||||
1
demos/07.cagire
Normal file
1
demos/07.cagire
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"banks":[],"tempo":120.0,"playing_patterns":[[0,0]],"prelude":""}
|
||||||
1
demos/08.cagire
Normal file
1
demos/08.cagire
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"banks":[],"tempo":120.0,"playing_patterns":[[0,0]],"prelude":""}
|
||||||
1
demos/09.cagire
Normal file
1
demos/09.cagire
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"banks":[],"tempo":120.0,"playing_patterns":[[0,0]],"prelude":""}
|
||||||
1
demos/10.cagire
Normal file
1
demos/10.cagire
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"banks":[],"tempo":120.0,"playing_patterns":[[0,0]],"prelude":""}
|
||||||
@@ -31,6 +31,8 @@ impl App {
|
|||||||
layout: self.audio.config.layout,
|
layout: self.audio.config.layout,
|
||||||
hue_rotation: self.ui.hue_rotation,
|
hue_rotation: self.ui.hue_rotation,
|
||||||
onboarding_dismissed: self.ui.onboarding_dismissed.clone(),
|
onboarding_dismissed: self.ui.onboarding_dismissed.clone(),
|
||||||
|
load_demo_on_startup: self.ui.load_demo_on_startup,
|
||||||
|
demo_index: self.ui.demo_index,
|
||||||
font: self.ui.font.clone(),
|
font: self.ui.font.clone(),
|
||||||
zoom_factor: self.ui.zoom_factor,
|
zoom_factor: self.ui.zoom_factor,
|
||||||
},
|
},
|
||||||
@@ -91,6 +93,16 @@ impl App {
|
|||||||
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
||||||
match model::load(&path) {
|
match model::load(&path) {
|
||||||
Ok(project) => {
|
Ok(project) => {
|
||||||
|
self.apply_project(project, format!("Loaded: {}", path.display()), link);
|
||||||
|
self.project_state.file_path = Some(path);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.ui.set_status(format!("Load error: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_project(&mut self, project: model::Project, label: String, link: &LinkState) {
|
||||||
let tempo = project.tempo;
|
let tempo = project.tempo;
|
||||||
let playing = project.playing_patterns.clone();
|
let playing = project.playing_patterns.clone();
|
||||||
|
|
||||||
@@ -116,12 +128,6 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ui.set_status(format!("Loaded: {}", path.display()));
|
self.ui.set_status(label);
|
||||||
self.project_state.file_path = Some(path);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.ui.set_status(format!("Load error: {e}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/init.rs
24
src/init.rs
@@ -65,6 +65,29 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
|
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
|
app.ui.load_demo_on_startup = settings.display.load_demo_on_startup;
|
||||||
|
app.ui.demo_index = settings.display.demo_index;
|
||||||
|
|
||||||
|
if app.ui.load_demo_on_startup && !model::demos::DEMOS.is_empty() {
|
||||||
|
let count = model::demos::DEMOS.len();
|
||||||
|
let index = settings.display.demo_index % count;
|
||||||
|
let demo = &model::demos::DEMOS[index];
|
||||||
|
if let Ok(project) = model::load_str(demo.json) {
|
||||||
|
let playing = project.playing_patterns.clone();
|
||||||
|
let tempo = project.tempo;
|
||||||
|
app.project_state.project = project;
|
||||||
|
link.set_tempo(tempo);
|
||||||
|
for (bank, pattern) in playing {
|
||||||
|
app.playback.queued_changes.push(StagedChange {
|
||||||
|
change: PatternChange::Start { bank, pattern },
|
||||||
|
quantization: model::LaunchQuantization::Immediate,
|
||||||
|
sync_mode: model::SyncMode::PhaseLock,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
app.ui.set_status(format!("Demo: {}", demo.name));
|
||||||
|
}
|
||||||
|
app.ui.demo_index = (index + 1) % count;
|
||||||
|
} else {
|
||||||
app.playback.queued_changes.push(StagedChange {
|
app.playback.queued_changes.push(StagedChange {
|
||||||
change: PatternChange::Start {
|
change: PatternChange::Start {
|
||||||
bank: 0,
|
bank: 0,
|
||||||
@@ -73,6 +96,7 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
quantization: model::LaunchQuantization::Immediate,
|
quantization: model::LaunchQuantization::Immediate,
|
||||||
sync_mode: model::SyncMode::PhaseLock,
|
sync_mode: model::SyncMode::PhaseLock,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());
|
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());
|
||||||
app.audio.config.input_device = args.input.or(settings.audio.input_device.clone());
|
app.audio.config.input_device = args.input.or(settings.audio.input_device.clone());
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
|||||||
OptionsFocus::ResetOnboarding => {
|
OptionsFocus::ResetOnboarding => {
|
||||||
ctx.dispatch(AppCommand::ResetOnboarding);
|
ctx.dispatch(AppCommand::ResetOnboarding);
|
||||||
}
|
}
|
||||||
|
OptionsFocus::LoadDemoOnStartup => {
|
||||||
|
ctx.app.ui.load_demo_on_startup = !ctx.app.ui.load_demo_on_startup;
|
||||||
|
}
|
||||||
OptionsFocus::MidiInput0
|
OptionsFocus::MidiInput0
|
||||||
| OptionsFocus::MidiInput1
|
| OptionsFocus::MidiInput1
|
||||||
| OptionsFocus::MidiInput2
|
| OptionsFocus::MidiInput2
|
||||||
|
|||||||
17
src/model/demos.rs
Normal file
17
src/model/demos.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
pub struct DemoEntry {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub json: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DEMOS: &[DemoEntry] = &[
|
||||||
|
DemoEntry { name: "Demo 01", json: include_str!("../../demos/01.cagire") },
|
||||||
|
DemoEntry { name: "Demo 02", json: include_str!("../../demos/02.cagire") },
|
||||||
|
DemoEntry { name: "Demo 03", json: include_str!("../../demos/03.cagire") },
|
||||||
|
DemoEntry { name: "Demo 04", json: include_str!("../../demos/04.cagire") },
|
||||||
|
DemoEntry { name: "Demo 05", json: include_str!("../../demos/05.cagire") },
|
||||||
|
DemoEntry { name: "Demo 06", json: include_str!("../../demos/06.cagire") },
|
||||||
|
DemoEntry { name: "Demo 07", json: include_str!("../../demos/07.cagire") },
|
||||||
|
DemoEntry { name: "Demo 08", json: include_str!("../../demos/08.cagire") },
|
||||||
|
DemoEntry { name: "Demo 09", json: include_str!("../../demos/09.cagire") },
|
||||||
|
DemoEntry { name: "Demo 10", json: include_str!("../../demos/10.cagire") },
|
||||||
|
];
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod categories;
|
pub mod categories;
|
||||||
|
pub mod demos;
|
||||||
pub mod docs;
|
pub mod docs;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
mod script;
|
mod script;
|
||||||
@@ -8,8 +9,8 @@ pub use cagire_forth::{
|
|||||||
Variables, Word, WordCompile, WORDS,
|
Variables, Word, WordCompile, WORDS,
|
||||||
};
|
};
|
||||||
pub use cagire_project::{
|
pub use cagire_project::{
|
||||||
load, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode,
|
load, load_str, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project,
|
||||||
MAX_BANKS, MAX_PATTERNS,
|
SyncMode, MAX_BANKS, MAX_PATTERNS,
|
||||||
};
|
};
|
||||||
pub use script::ScriptEngine;
|
pub use script::ScriptEngine;
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ pub struct DisplaySettings {
|
|||||||
pub hue_rotation: f32,
|
pub hue_rotation: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub onboarding_dismissed: Vec<String>,
|
pub onboarding_dismissed: Vec<String>,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub load_demo_on_startup: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub demo_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_font() -> String {
|
fn default_font() -> String {
|
||||||
@@ -105,6 +109,8 @@ impl Default for DisplaySettings {
|
|||||||
layout: MainLayout::default(),
|
layout: MainLayout::default(),
|
||||||
hue_rotation: 0.0,
|
hue_rotation: 0.0,
|
||||||
onboarding_dismissed: Vec::new(),
|
onboarding_dismissed: Vec::new(),
|
||||||
|
load_demo_on_startup: true,
|
||||||
|
demo_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub enum OptionsFocus {
|
|||||||
MidiInput2,
|
MidiInput2,
|
||||||
MidiInput3,
|
MidiInput3,
|
||||||
ResetOnboarding,
|
ResetOnboarding,
|
||||||
|
LoadDemoOnStartup,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CyclicEnum for OptionsFocus {
|
impl CyclicEnum for OptionsFocus {
|
||||||
@@ -55,6 +56,7 @@ impl CyclicEnum for OptionsFocus {
|
|||||||
Self::MidiInput2,
|
Self::MidiInput2,
|
||||||
Self::MidiInput3,
|
Self::MidiInput3,
|
||||||
Self::ResetOnboarding,
|
Self::ResetOnboarding,
|
||||||
|
Self::LoadDemoOnStartup,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +79,7 @@ const STANDALONE_ONLY: &[OptionsFocus] = &[
|
|||||||
OptionsFocus::MidiInput2,
|
OptionsFocus::MidiInput2,
|
||||||
OptionsFocus::MidiInput3,
|
OptionsFocus::MidiInput3,
|
||||||
OptionsFocus::ResetOnboarding,
|
OptionsFocus::ResetOnboarding,
|
||||||
|
OptionsFocus::LoadDemoOnStartup,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Section layout: header line, divider line, then option lines.
|
/// Section layout: header line, divider line, then option lines.
|
||||||
@@ -113,6 +116,7 @@ const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
|
|||||||
(OptionsFocus::MidiInput3, 39),
|
(OptionsFocus::MidiInput3, 39),
|
||||||
// blank=40, ONBOARDING header=41, divider=42
|
// blank=40, ONBOARDING header=41, divider=42
|
||||||
(OptionsFocus::ResetOnboarding, 43),
|
(OptionsFocus::ResetOnboarding, 43),
|
||||||
|
(OptionsFocus::LoadDemoOnStartup, 44),
|
||||||
];
|
];
|
||||||
|
|
||||||
impl OptionsFocus {
|
impl OptionsFocus {
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ pub struct UiState {
|
|||||||
pub zoom_factor: f32,
|
pub zoom_factor: f32,
|
||||||
pub window_width: u32,
|
pub window_width: u32,
|
||||||
pub window_height: u32,
|
pub window_height: u32,
|
||||||
|
pub load_demo_on_startup: bool,
|
||||||
|
pub demo_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
@@ -127,6 +129,8 @@ impl Default for UiState {
|
|||||||
zoom_factor: 1.5,
|
zoom_factor: 1.5,
|
||||||
window_width: 1200,
|
window_width: 1200,
|
||||||
window_height: 800,
|
window_height: 800,
|
||||||
|
load_demo_on_startup: true,
|
||||||
|
demo_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,12 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
focus == OptionsFocus::ResetOnboarding,
|
focus == OptionsFocus::ResetOnboarding,
|
||||||
&theme,
|
&theme,
|
||||||
),
|
),
|
||||||
|
render_option_line(
|
||||||
|
"Demo on startup",
|
||||||
|
if app.ui.load_demo_on_startup { "On" } else { "Off" },
|
||||||
|
focus == OptionsFocus::LoadDemoOnStartup,
|
||||||
|
&theme,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +364,7 @@ fn option_description(focus: OptionsFocus) -> Option<&'static str> {
|
|||||||
OptionsFocus::MidiInput2 => Some("MIDI input device for channel group 3"),
|
OptionsFocus::MidiInput2 => Some("MIDI input device for channel group 3"),
|
||||||
OptionsFocus::MidiInput3 => Some("MIDI input device for channel group 4"),
|
OptionsFocus::MidiInput3 => Some("MIDI input device for channel group 4"),
|
||||||
OptionsFocus::ResetOnboarding => Some("Re-enable all dismissed guide popups"),
|
OptionsFocus::ResetOnboarding => Some("Re-enable all dismissed guide popups"),
|
||||||
|
OptionsFocus::LoadDemoOnStartup => Some("Load a rotating demo song on fresh startup"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -559,6 +559,9 @@ fn render_mini_tile_grid(
|
|||||||
let row_gap: u16 = 1;
|
let row_gap: u16 = 1;
|
||||||
let max_tile_height: u16 = 4;
|
let max_tile_height: u16 = 4;
|
||||||
|
|
||||||
|
let max_rows = (area.height as usize + row_gap as usize) / (1 + row_gap as usize);
|
||||||
|
let num_rows = num_rows.min(max_rows.max(1));
|
||||||
|
|
||||||
let available_for_rows =
|
let available_for_rows =
|
||||||
area.height.saturating_sub((num_rows.saturating_sub(1) as u16) * row_gap);
|
area.height.saturating_sub((num_rows.saturating_sub(1) as u16) * row_gap);
|
||||||
let tile_height = (available_for_rows / num_rows as u16).min(max_tile_height).max(1);
|
let tile_height = (available_for_rows / num_rows as u16).min(max_tile_height).max(1);
|
||||||
|
|||||||
@@ -210,3 +210,47 @@ fn shorthand_float() {
|
|||||||
fn shorthand_float_negative() {
|
fn shorthand_float_negative() {
|
||||||
expect_float("-.5 1 +", 0.5);
|
expect_float("-.5 1 +", 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// linmap
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linmap_basic() {
|
||||||
|
expect_float("64 0 127 200 2000 linmap", 200.0 + (64.0 / 127.0) * 1800.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linmap_at_bounds() {
|
||||||
|
expect_float("0 0 127 200 2000 linmap", 200.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linmap_at_upper() {
|
||||||
|
expect_float("127 0 127 200 2000 linmap", 2000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linmap_equal_input_range() {
|
||||||
|
expect_float("5 5 5 100 200 linmap", 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// expmap
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expmap_at_zero() {
|
||||||
|
expect_float("0 200 8000 expmap", 200.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expmap_at_one() {
|
||||||
|
expect_float("1 200 8000 expmap", 8000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expmap_midpoint() {
|
||||||
|
expect_float("0.5 100 10000 expmap", 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expmap_negative_bound() {
|
||||||
|
expect_error("0.5 -100 8000 expmap", "expmap requires positive bounds");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user