From 8bc9b2cdf6d7291bf5fa0df8b627f6d41fe6b17b Mon Sep 17 00:00:00 2001 From: Devin Jean Date: Thu, 4 Jan 2024 16:42:54 -0600 Subject: [PATCH] support play notes --- Cargo.toml | 4 +-- src/bytecode.rs | 57 +++++++++++++++++++++------------- src/process.rs | 29 +++++++++++++++++ src/runtime.rs | 11 +++++++ src/test/blocks/play-notes.xml | 1 + src/test/mod.rs | 9 ++++-- src/test/process.rs | 46 +++++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 src/test/blocks/play-notes.xml diff --git a/Cargo.toml b/Cargo.toml index 42dac8b..f2ac280 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 "] @@ -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 } diff --git a/src/bytecode.rs b/src/bytecode.rs index 1788f04..6eca107 100644 --- a/src/bytecode.rs +++ b/src/bytecode.rs @@ -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, @@ -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!(), } @@ -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), } } } @@ -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()); diff --git a/src/process.rs b/src/process.rs index a58324f..558418e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1311,6 +1311,23 @@ impl<'gc, C: CustomTypes, S: System> 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::>()?, + 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 { @@ -1468,6 +1485,18 @@ mod ops { if good { Some(vals) } else { None } } + pub(super) fn prep_note, S: System>(value: &Value<'_, C, S>) -> Result> { + 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, S: System>(index: &Value<'_, C, S>, len: usize) -> Result> { let raw_index = index.as_number()?.get(); let index = raw_index as i64; diff --git a/src/runtime.rs b/src/runtime.rs index caf2fee..132878b 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -99,6 +99,10 @@ pub enum ErrorCause, S: System> { 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. @@ -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, @@ -1648,6 +1655,9 @@ pub enum Command<'gc, 'a, C: CustomTypes, S: System> { /// 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