Skip to content

Commit

Permalink
support play notes
Browse files Browse the repository at this point in the history
  • Loading branch information
dragazo committed Jan 4, 2024
1 parent e222754 commit 8bc9b2c
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 27 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "netsblox-vm"
version = "0.4.1"
version = "0.4.2"
edition = "2021"
license = "MIT OR Apache-2.0"
authors = ["Devin Jean <[email protected]>"]
Expand Down Expand Up @@ -72,7 +72,7 @@ rustls-tls-webpki-roots = [
# core deps
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
gc-arena = { version = "=0.4.0", default-features = false }
netsblox-ast = { version = "=0.5.2", default-features = false }
netsblox-ast = { version = "=0.5.4", default-features = false }
# netsblox-ast = { path = "../netsblox-ast", default-features = false }
num-traits = { version = "0.2.17", default-features = false }
num-derive = { version = "0.4.1", default-features = false }
Expand Down
57 changes: 35 additions & 22 deletions src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ pub(crate) enum Instruction<'a> {
/// This can be an audio object or the name of a static sound on the entity.
/// Empty string can be used as a no-op.
PlaySound { blocking: bool },
/// Consumes 2 values, `duration` and `notes`, from the value stack and plays them.
/// `notes` can be a single or list of notes in either MIDI value form or (extended) English names of notes.
PlayNotes { blocking: bool },
/// Stops playback of all currently-playing sounds.
StopSounds,

Expand Down Expand Up @@ -863,23 +866,25 @@ impl<'a> BinaryRead<'a> for Instruction<'a> {
124 => read_prefixed!(Instruction::PushSoundList),
125 => read_prefixed!(Instruction::PlaySound { blocking: true }),
126 => read_prefixed!(Instruction::PlaySound { blocking: false }),
127 => read_prefixed!(Instruction::StopSounds),
127 => read_prefixed!(Instruction::PlayNotes { blocking: true }),
128 => read_prefixed!(Instruction::PlayNotes { blocking: false }),
129 => read_prefixed!(Instruction::StopSounds),

128 => read_prefixed!(Instruction::Clone),
129 => read_prefixed!(Instruction::DeleteClone),
130 => read_prefixed!(Instruction::Clone),
131 => read_prefixed!(Instruction::DeleteClone),

130 => read_prefixed!(Instruction::ClearEffects),
131 => read_prefixed!(Instruction::ClearDrawings),
132 => read_prefixed!(Instruction::ClearEffects),
133 => read_prefixed!(Instruction::ClearDrawings),

132 => read_prefixed!(Instruction::GotoXY),
133 => read_prefixed!(Instruction::Goto),
134 => read_prefixed!(Instruction::GotoXY),
135 => read_prefixed!(Instruction::Goto),

134 => read_prefixed!(Instruction::PointTowardsXY),
135 => read_prefixed!(Instruction::PointTowards),
136 => read_prefixed!(Instruction::PointTowardsXY),
137 => read_prefixed!(Instruction::PointTowards),

136 => read_prefixed!(Instruction::Forward),
138 => read_prefixed!(Instruction::Forward),

137 => read_prefixed!(Instruction::UnknownBlock {} : name, args),
139 => read_prefixed!(Instruction::UnknownBlock {} : name, args),

_ => unreachable!(),
}
Expand Down Expand Up @@ -1072,23 +1077,25 @@ impl BinaryWrite for Instruction<'_> {
Instruction::PushSoundList => append_prefixed!(124),
Instruction::PlaySound { blocking: true } => append_prefixed!(125),
Instruction::PlaySound { blocking: false } => append_prefixed!(126),
Instruction::StopSounds => append_prefixed!(127),
Instruction::PlayNotes { blocking: true } => append_prefixed!(127),
Instruction::PlayNotes { blocking: false } => append_prefixed!(128),
Instruction::StopSounds => append_prefixed!(129),

Instruction::Clone => append_prefixed!(128),
Instruction::DeleteClone => append_prefixed!(129),
Instruction::Clone => append_prefixed!(130),
Instruction::DeleteClone => append_prefixed!(131),

Instruction::ClearEffects => append_prefixed!(130),
Instruction::ClearDrawings => append_prefixed!(131),
Instruction::ClearEffects => append_prefixed!(132),
Instruction::ClearDrawings => append_prefixed!(133),

Instruction::GotoXY => append_prefixed!(132),
Instruction::Goto => append_prefixed!(133),
Instruction::GotoXY => append_prefixed!(134),
Instruction::Goto => append_prefixed!(135),

Instruction::PointTowardsXY => append_prefixed!(134),
Instruction::PointTowards => append_prefixed!(135),
Instruction::PointTowardsXY => append_prefixed!(136),
Instruction::PointTowards => append_prefixed!(137),

Instruction::Forward => append_prefixed!(136),
Instruction::Forward => append_prefixed!(138),

Instruction::UnknownBlock { name, args } => append_prefixed!(137: move str name, args),
Instruction::UnknownBlock { name, args } => append_prefixed!(139: move str name, args),
}
}
}
Expand Down Expand Up @@ -1894,6 +1901,12 @@ impl<'a: 'b, 'b> ByteCodeBuilder<'a, 'b> {
ast::StmtKind::Stop { mode: ast::StopMode::OtherScriptsInSprite } => self.ins.push(Instruction::Abort { mode: AbortMode::MyOthers }.into()),
ast::StmtKind::SetCostume { costume } => self.append_simple_ins(entity, &[costume], Instruction::SetCostume)?,
ast::StmtKind::PlaySound { sound, blocking } => self.append_simple_ins(entity, &[sound], Instruction::PlaySound { blocking: *blocking })?,
ast::StmtKind::PlayNotes { notes, beats, blocking } => self.append_simple_ins(entity, &[notes, beats], Instruction::PlayNotes { blocking: *blocking })?,
ast::StmtKind::Rest { beats } => {
self.ins.push(Instruction::VariadicOp { op: VariadicOp::MakeList, len: VariadicLen::Fixed(0) }.into());
self.append_expr(beats, entity)?;
self.ins.push(Instruction::PlayNotes { blocking: true }.into());
}
ast::StmtKind::StopSounds => self.ins.push(Instruction::StopSounds.into()),
ast::StmtKind::Stop { mode: ast::StopMode::ThisBlock } => {
self.ins.push(Instruction::PushString { value: "" }.into());
Expand Down
29 changes: 29 additions & 0 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,23 @@ impl<'gc, C: CustomTypes<S>, S: System<C>> Process<'gc, C, S> {
self.pos = aft_pos;
}
}
Instruction::PlayNotes { blocking } => {
let beats = self.value_stack.pop().unwrap().as_number()?;
let notes = match self.value_stack.pop().unwrap() {
Value::List(x) => x.borrow().iter().map(ops::prep_note).collect::<Result<_,_>>()?,
x => vec![ops::prep_note(&x)?],
};

if beats.get() > 0.0 {
drop(global_context_raw);
self.defer = Some(Defer::Command {
key: system.perform_command(mc, Command::PlayNotes { notes, beats, blocking }, self)?,
aft_pos,
});
} else {
self.pos = aft_pos;
}
}
Instruction::StopSounds => {
drop(global_context_raw);
self.defer = Some(Defer::Command {
Expand Down Expand Up @@ -1468,6 +1485,18 @@ mod ops {
if good { Some(vals) } else { None }
}

pub(super) fn prep_note<C: CustomTypes<S>, S: System<C>>(value: &Value<'_, C, S>) -> Result<Note, ErrorCause<C, S>> {
if let Ok(v) = value.as_number().map(Number::get) {
let vv = v as i64;
if v != vv as f64 { return Err(ErrorCause::NoteNotInteger { note: v }); }
let res = Note::from_midi(vv as u8);
if vv < 0 || res.is_none() { return Err(ErrorCause::NoteNotMidi { note: vv.to_compact_string() }); }
return Ok(res.unwrap());
}
let s = value.as_string()?;
Note::from_name(&s).ok_or_else(|| ErrorCause::NoteNotMidi { note: s.into_owned() })
}

pub(super) fn prep_index<C: CustomTypes<S>, S: System<C>>(index: &Value<'_, C, S>, len: usize) -> Result<usize, ErrorCause<C, S>> {
let raw_index = index.as_number()?.get();
let index = raw_index as i64;
Expand Down
11 changes: 11 additions & 0 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ pub enum ErrorCause<C: CustomTypes<S>, S: System<C>> {
IndexOutOfBounds { index: i64, len: usize },
/// Attempt to index a list with a non-integer numeric value, `index`.
IndexNotInteger { index: f64 },
/// Attempt to create a MIDI note value which was not an integer.
NoteNotInteger { note: f64 },
/// Attempt to create a MIDI note with a value outside the allowed MIDI range.
NoteNotMidi { note: CompactString },
/// Attempt to use a number which was not a valid size (must be convertible to [`usize`]).
InvalidSize { value: f64 },
/// Attempt to interpret an invalid unicode code point (number) as a character.
Expand Down Expand Up @@ -1571,8 +1575,11 @@ pub enum Feature {

/// The ability of an entity to change the current costume.
SetCostume,

/// The ability of an entity to play a sound, optionally blocking until completion.
PlaySound { blocking: bool },
/// The ability of an entity to play musical notes, optionally blocking until completion.
PlayNotes { blocking: bool },
/// The ability of an entity to stop playback of currently-playing sounds.
StopSounds,

Expand Down Expand Up @@ -1648,6 +1655,9 @@ pub enum Command<'gc, 'a, C: CustomTypes<S>, S: System<C>> {
/// Plays a sound, optionally with a request to block until the sound is finished playing.
/// It is up to the receiver to actually satisfy this blocking aspect, if desired.
PlaySound { sound: Rc<Audio>, blocking: bool },
/// Plays zero or more notes, optionally with a request to block until the notes are finished playing.
/// It is up to the receiver to actually satisfy this blocking aspect, if desired.
PlayNotes { notes: Vec<Note>, beats: Number, blocking: bool },
/// Requests to stop playback of all currently-playing sounds.
StopSounds,

Expand All @@ -1673,6 +1683,7 @@ impl<'gc, C: CustomTypes<S>, S: System<C>> Command<'gc, '_, C, S> {
Command::ChangeProperty { prop, .. } => Feature::ChangeProperty { prop: *prop },
Command::SetCostume => Feature::SetCostume,
Command::PlaySound { blocking, .. } => Feature::PlaySound { blocking: *blocking },
Command::PlayNotes { blocking, .. } => Feature::PlayNotes { blocking: *blocking },
Command::StopSounds => Feature::StopSounds,
Command::ClearEffects => Feature::ClearEffects,
Command::ClearDrawings => Feature::ClearDrawings,
Expand Down
1 change: 1 addition & 0 deletions src/test/blocks/play-notes.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<blocks app="NetsBlox 2.3.2, http://netsblox.org" version="2.3.2"><block-definition collabId="item_-1_2" s="main" type="reporter" category="custom"><header></header><code></code><translations></translations><inputs></inputs><script><block collabId="item_2" s="doPlayNote"><l>60</l><l>0.5</l></block><block collabId="item_6" s="doPlayNote"><l>51</l><l>1</l></block><block collabId="item_7" s="doPlayNote"><block collabId="item_8" s="reportJoinWords"><list><l>82</l></list></block><l>2.333</l></block><block collabId="item_47" s="doPlayNote"><block collabId="item_47_1" s="reportJoinWords"><list><l>C#6</l></list></block><l>0</l></block><block collabId="item_11" s="doPlayNote"><block collabId="item_11_1" s="reportJoinWords"><list><l>C##6</l></list></block><l>0.5</l></block><block collabId="item_13" s="doPlayNote"><block collabId="item_13_1" s="reportJoinWords"><list><l>Ab3ss</l></list></block><l>4</l></block><block collabId="item_22" s="doPlayNote"><block collabId="item_22_1" s="reportNewList"><list><l>76</l></list></block><l>1.5</l></block><block collabId="item_49" s="doPlayNote"><block collabId="item_49_1" s="reportNewList"><list><l>G#7</l></list></block><l>-2</l></block><block collabId="item_24" s="doPlayNote"><block collabId="item_24_1" s="reportNewList"><list><l>G#7</l></list></block><l>0.5</l></block><block collabId="item_15" s="doPlayNote"><block collabId="item_16" s="reportNewList"><list><l>45</l><l>B3b</l></list></block><l>0.9</l></block><block collabId="item_26" s="doPlayNote"><block collabId="item_26_1" s="reportNewList"><list><l>A2</l><l>A3</l><l>A4</l><l>C#5</l></list></block><l>0.1</l></block><block collabId="item_1" s="doRest"><l>0.2</l></block><block collabId="item_44" s="doRest"><l>1.75</l></block><block collabId="item_46" s="doRest"><l>0</l></block><block collabId="item_52" s="doRest"><l>2.4</l></block><block collabId="item_54" s="doRest"><l>-0.3</l></block><block collabId="item_56" s="doRest"><l>4</l></block><block collabId="item_58" s="doReport"><l>done!</l></block></script></block-definition></blocks>
9 changes: 6 additions & 3 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,14 +575,17 @@ fn test_note() {

assert_eq!(Note::from_name("G#9"), None);
assert_eq!(Note::from_name("G##9"), None);
assert_eq!(Note::from_name("g##9"), None);
assert_eq!(Note::from_name("Cb-1"), None);
assert_eq!(Note::from_name("G10bbbbbbbbbbbb").unwrap().get_midi(), 127);
assert_eq!(Note::from_name("G10bbbbbbbbbbbbb").unwrap().get_midi(), 126);
assert_eq!(Note::from_name("g10bbbbbbbbbbbbb").unwrap().get_midi(), 126);
assert_eq!(Note::from_name("Gb10bbbbbbbbbbbbb").unwrap().get_midi(), 125);
assert_eq!(Note::from_name("Gbb10bb♭bbb♭♭♭bb♭b").unwrap().get_midi(), 124);
assert_eq!(Note::from_name("gbb10bb♭bbb♭♭♭bb♭b").unwrap().get_midi(), 124);
assert_eq!(Note::from_name("G♭bb10bbbbbbbbbbbbb").unwrap().get_midi(), 123);
assert_eq!(Note::from_name("Gbbb+10bbbbbbbbbbbbb").unwrap().get_midi(), 123);
assert_eq!(Note::from_name("C######-2######").unwrap().get_midi(), 0);
assert_eq!(Note::from_name("C######-2#######").unwrap().get_midi(), 1);
assert_eq!(Note::from_name("c######-2#######").unwrap().get_midi(), 1);
assert_eq!(Note::from_name("C#♯♯ss#-2#♯##♯#s#").unwrap().get_midi(), 2);
assert_eq!(Note::from_name(" C#♯♯ss#-2#♯##♯#s#").unwrap().get_midi(), 2);
assert_eq!(Note::from_name(" \t C#♯♯ss#-2#♯##♯#s# ").unwrap().get_midi(), 2);
}
46 changes: 46 additions & 0 deletions src/test/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2542,3 +2542,49 @@ fn test_proc_extra_blocks() {
];
assert_eq!(&*actions.borrow(), &expected);
}

#[test]
fn test_proc_play_notes() {
let sound_events = Rc::new(RefCell::new(vec![]));
let sound_events_clone = sound_events.clone();
let config = Config::<C, StdSystem<C>> {
request: None,
command: Some(Rc::new(move |_, key, command, _| match command {
Command::PlayNotes { notes, beats, blocking } => {
sound_events_clone.borrow_mut().push((notes, beats, blocking));
key.complete(Ok(()));
CommandStatus::Handled
}
_ => CommandStatus::UseDefault { key, command },
})),
};
let system = Rc::new(StdSystem::new_sync(CompactString::new(BASE_URL), None, config, Arc::new(Clock::new(UtcOffset::UTC, None))));
let (mut env, _) = get_running_proc(&format!(include_str!("templates/generic-static.xml"),
globals = "",
fields = "",
funcs = include_str!("blocks/play-notes.xml"),
methods = "",
), Settings { rpc_error_scheme: ErrorScheme::Soft, ..Default::default() }, system, |_| SymbolTable::default());

run_till_term(&mut env, |mc, _, res| {
let expect = Value::from_simple(mc, SimpleValue::from_json(json!("done!")).unwrap());
assert_values_eq(&res.unwrap().0, &expect, 1e-5, "play notes");
});

let sound_events = &*sound_events.borrow();
assert_eq!(sound_events.len(), 13);

assert!(sound_events[0].0 == [Note::from_midi(60).unwrap()] && (sound_events[0].1.get() - 0.5).abs() < 1e-10 && sound_events[0].2);
assert!(sound_events[1].0 == [Note::from_midi(51).unwrap()] && (sound_events[1].1.get() - 1.0).abs() < 1e-10 && sound_events[1].2);
assert!(sound_events[2].0 == [Note::from_midi(82).unwrap()] && (sound_events[2].1.get() - 2.333).abs() < 1e-10 && sound_events[2].2);
assert!(sound_events[3].0 == [Note::from_midi(86).unwrap()] && (sound_events[3].1.get() - 0.5).abs() < 1e-10 && sound_events[3].2);
assert!(sound_events[4].0 == [Note::from_midi(58).unwrap()] && (sound_events[4].1.get() - 4.0).abs() < 1e-10 && sound_events[4].2);
assert!(sound_events[5].0 == [Note::from_midi(76).unwrap()] && (sound_events[5].1.get() - 1.5).abs() < 1e-10 && sound_events[5].2);
assert!(sound_events[6].0 == [Note::from_midi(104).unwrap()] && (sound_events[6].1.get() - 0.5).abs() < 1e-10 && sound_events[6].2);
assert!(sound_events[7].0 == [Note::from_midi(45).unwrap(), Note::from_midi(58).unwrap()] && (sound_events[7].1.get() - 0.9).abs() < 1e-10 && sound_events[7].2);
assert!(sound_events[8].0 == [Note::from_midi(45).unwrap(), Note::from_midi(57).unwrap(), Note::from_midi(69).unwrap(), Note::from_midi(73).unwrap()] && (sound_events[8].1.get() - 0.1).abs() < 1e-10 && sound_events[8].2);
assert!(sound_events[9].0 == [] && (sound_events[9].1.get() - 0.2).abs() < 1e-10 && sound_events[9].2);
assert!(sound_events[10].0 == [] && (sound_events[10].1.get() - 1.75).abs() < 1e-10 && sound_events[10].2);
assert!(sound_events[11].0 == [] && (sound_events[11].1.get() - 2.4).abs() < 1e-10 && sound_events[11].2);
assert!(sound_events[12].0 == [] && (sound_events[12].1.get() - 4.0).abs() < 1e-10 && sound_events[12].2);
}

0 comments on commit 8bc9b2c

Please sign in to comment.