Skip to content
5 changes: 4 additions & 1 deletion include/scratchcpp/sound.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace libscratchcpp
{

class Thread;
class SoundPrivate;

/*! \brief The Sound class represents a Scratch sound. */
Expand All @@ -34,13 +35,15 @@ class LIBSCRATCHCPP_EXPORT Sound : public Asset
virtual void setVolume(double volume);
virtual void setEffect(Effect effect, double value);

virtual void start();
virtual void start(Thread *owner = nullptr);
virtual void stop();

virtual bool isPlaying() const;

std::shared_ptr<Sound> clone() const;

Thread *owner() const;

protected:
void processData(unsigned int size, void *data) override;
virtual bool isClone() const override;
Expand Down
237 changes: 237 additions & 0 deletions src/blocks/soundblocks.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// SPDX-License-Identifier: Apache-2.0

#include <scratchcpp/iengine.h>
#include <scratchcpp/compiler.h>
#include <scratchcpp/compilerconstant.h>
#include <scratchcpp/field.h>
#include <scratchcpp/target.h>
#include <scratchcpp/thread.h>
#include <scratchcpp/value.h>
#include <scratchcpp/executioncontext.h>
#include <algorithm>

#include "soundblocks.h"

using namespace libscratchcpp;
Expand All @@ -21,4 +31,231 @@ Rgb SoundBlocks::color() const

void SoundBlocks::registerBlocks(IEngine *engine)
{
engine->addCompileFunction(this, "sound_play", &compilePlay);
engine->addCompileFunction(this, "sound_playuntildone", &compilePlayUntilDone);
engine->addCompileFunction(this, "sound_stopallsounds", &compileStopAllSounds);
engine->addCompileFunction(this, "sound_seteffectto", &compileSetEffectTo);
engine->addCompileFunction(this, "sound_changeeffectby", &compileChangeEffectBy);
engine->addCompileFunction(this, "sound_cleareffects", &compileClearEffects);
engine->addCompileFunction(this, "sound_changevolumeby", &compileChangeVolumeBy);
engine->addCompileFunction(this, "sound_setvolumeto", &compileSetVolumeTo);
engine->addCompileFunction(this, "sound_volume", &compileVolume);
}

void SoundBlocks::onInit(IEngine *engine)
{
engine->threadAboutToStop().connect([](Thread *thread) {
Target *target = thread->target();
const auto &sounds = target->sounds();

for (auto sound : sounds) {
if (sound->owner() == thread) {
sound->stop();
break;
}
}
});
}

CompilerValue *SoundBlocks::compilePlay(Compiler *compiler)
{
auto sound = compiler->addInput("SOUND_MENU");
auto storeOwner = compiler->addConstValue(false);
compiler->addFunctionCallWithCtx("sound_play", Compiler::StaticType::Pointer, { Compiler::StaticType::Unknown, Compiler::StaticType::Bool }, { sound, storeOwner });
return nullptr;
}

CompilerValue *SoundBlocks::compilePlayUntilDone(Compiler *compiler)
{
auto sound = compiler->addInput("SOUND_MENU");
auto storeOwner = compiler->addConstValue(true);
auto soundPtr = compiler->addFunctionCallWithCtx("sound_play", Compiler::StaticType::Pointer, { Compiler::StaticType::Unknown, Compiler::StaticType::Bool }, { sound, storeOwner });

compiler->beginLoopCondition();
auto waiting = compiler->addFunctionCallWithCtx("sound_is_waiting", Compiler::StaticType::Bool, { Compiler::StaticType::Pointer }, { soundPtr });
compiler->beginWhileLoop(waiting);
compiler->endLoop();

return nullptr;
}

CompilerValue *SoundBlocks::compileStopAllSounds(Compiler *compiler)
{
compiler->addFunctionCallWithCtx("sound_stopallsounds");
return nullptr;
}

CompilerValue *SoundBlocks::compileSetEffectTo(Compiler *compiler)
{
Field *field = compiler->field("EFFECT");

if (!field)
return nullptr;

const std::string &option = field->value().toString();

if (option == "PITCH") {
auto value = compiler->addInput("VALUE");
compiler->addTargetFunctionCall("sound_set_pitch_effect", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { value });
} else if (option == "PAN") {
auto value = compiler->addInput("VALUE");
compiler->addTargetFunctionCall("sound_set_pan_effect", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { value });
}

return nullptr;
}

CompilerValue *SoundBlocks::compileChangeEffectBy(Compiler *compiler)
{
Field *field = compiler->field("EFFECT");

if (!field)
return nullptr;

const std::string &option = field->value().toString();

if (option == "PITCH") {
auto value = compiler->addInput("VALUE");
compiler->addTargetFunctionCall("sound_change_pitch_effect", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { value });
} else if (option == "PAN") {
auto value = compiler->addInput("VALUE");
compiler->addTargetFunctionCall("sound_change_pan_effect", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { value });
}

return nullptr;
}

CompilerValue *SoundBlocks::compileClearEffects(Compiler *compiler)
{
compiler->addTargetFunctionCall("sound_cleareffects");
return nullptr;
}

CompilerValue *SoundBlocks::compileChangeVolumeBy(Compiler *compiler)
{
auto volume = compiler->addInput("VOLUME");
compiler->addTargetFunctionCall("sound_changevolumeby", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { volume });
return nullptr;
}

CompilerValue *SoundBlocks::compileSetVolumeTo(Compiler *compiler)
{
auto volume = compiler->addInput("VOLUME");
compiler->addTargetFunctionCall("sound_setvolumeto", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { volume });
return nullptr;
}

CompilerValue *SoundBlocks::compileVolume(Compiler *compiler)
{
return compiler->addTargetFunctionCall("sound_volume", Compiler::StaticType::Number);
}

int sound_wrap_clamp_index(Target *target, int index)
{
const long soundCount = target->sounds().size();

if (index < 0)
return (soundCount + index % (-soundCount)) % soundCount;
else if (index >= soundCount)
return index % soundCount;
else
return index;
}

int sound_get_index(Target *target, const ValueData *sound)
{
if (!value_isString(sound)) {
// Numbers should be treated as sound indices
if (value_isNaN(sound) || value_isInfinity(sound) || value_isNegativeInfinity(sound))
return -1;
else
return sound_wrap_clamp_index(target, value_toLong(sound) - 1);
} else {
// Strings should be treated as sound names, where possible
// TODO: Use UTF-16 in Target
// StringPtr *nameStr = value_toStringPtr(sound);
std::string nameStr;
value_toString(sound, &nameStr);
const int soundIndex = target->findSound(nameStr);

auto it = std::find_if(nameStr.begin(), nameStr.end(), [](char c) { return !std::isspace(c); });
bool isWhiteSpace = (it == nameStr.end());

if (soundIndex != -1) {
return soundIndex;
// Try to cast the string to a number (and treat it as a costume index)
// Pure whitespace should not be treated as a number
} else if (value_isValidNumber(sound) && !isWhiteSpace)
return sound_wrap_clamp_index(target, value_toLong(sound) - 1);
}

return -1;
}

extern "C" Sound *sound_play(ExecutionContext *ctx, const ValueData *soundName, bool storeOwner)
{
Thread *thread = ctx->thread();
Target *target = thread->target();
int index = sound_get_index(target, soundName);
auto sound = target->soundAt(index);

if (sound) {
sound->start(storeOwner ? thread : nullptr);
return sound.get();
}

return nullptr;
}

extern "C" bool sound_is_waiting(ExecutionContext *ctx, Sound *sound)
{
if (!sound)
return false;

return sound->owner() == ctx->thread() && sound->isPlaying();
}

extern "C" void sound_stopallsounds(ExecutionContext *ctx)
{
ctx->engine()->stopSounds();
}

extern "C" void sound_set_pitch_effect(Target *target, double value)
{
target->setSoundEffectValue(Sound::Effect::Pitch, value);
}

extern "C" void sound_set_pan_effect(Target *target, double value)
{
target->setSoundEffectValue(Sound::Effect::Pan, value);
}

extern "C" void sound_change_pitch_effect(Target *target, double value)
{
target->setSoundEffectValue(Sound::Effect::Pitch, target->soundEffectValue(Sound::Effect::Pitch) + value);
}

extern "C" void sound_change_pan_effect(Target *target, double value)
{
target->setSoundEffectValue(Sound::Effect::Pan, target->soundEffectValue(Sound::Effect::Pan) + value);
}

extern "C" void sound_cleareffects(Target *target)
{
target->clearSoundEffects();
}

extern "C" void sound_changevolumeby(Target *target, double volume)
{
target->setVolume(target->volume() + volume);
}

extern "C" void sound_setvolumeto(Target *target, double volume)
{
target->setVolume(volume);
}

extern "C" double sound_volume(Target *target)
{
return target->volume();
}
14 changes: 14 additions & 0 deletions src/blocks/soundblocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
namespace libscratchcpp
{

class IAudioOutput;

class SoundBlocks : public IExtension
{
public:
Expand All @@ -15,6 +17,18 @@ class SoundBlocks : public IExtension
Rgb color() const override;

void registerBlocks(IEngine *engine) override;
void onInit(IEngine *engine) override;

private:
static CompilerValue *compilePlay(Compiler *compiler);
static CompilerValue *compilePlayUntilDone(Compiler *compiler);
static CompilerValue *compileStopAllSounds(Compiler *compiler);
static CompilerValue *compileSetEffectTo(Compiler *compiler);
static CompilerValue *compileChangeEffectBy(Compiler *compiler);
static CompilerValue *compileClearEffects(Compiler *compiler);
static CompilerValue *compileChangeVolumeBy(Compiler *compiler);
static CompilerValue *compileSetVolumeTo(Compiler *compiler);
static CompilerValue *compileVolume(Compiler *compiler);
};

} // namespace libscratchcpp
9 changes: 8 additions & 1 deletion src/scratch/sound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ void Sound::setEffect(Effect effect, double value)
}

/*! Starts the playback of the sound. */
void Sound::start()
void Sound::start(Thread *owner)
{
// Stop sounds in clones (#538)
stopCloneSounds();
impl->owner = owner;
impl->player->start();
}

Expand All @@ -78,6 +79,7 @@ void Sound::stop()
{
// Stop sounds in clones (#538)
stopCloneSounds();
impl->owner = nullptr;
impl->player->stop();
}

Expand Down Expand Up @@ -107,6 +109,11 @@ std::shared_ptr<Sound> Sound::clone() const
return sound;
}

Thread *Sound::owner() const
{
return impl->owner;
}

void Sound::processData(unsigned int size, void *data)
{
if (impl->player->isLoaded())
Expand Down
2 changes: 2 additions & 0 deletions src/scratch/sound_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace libscratchcpp
{

class Sound;
class Thread;

struct SoundPrivate
{
Expand All @@ -22,6 +23,7 @@ struct SoundPrivate
static IAudioOutput *audioOutput;
std::shared_ptr<IAudioPlayer> player = nullptr;
const Sound *cloneRoot = nullptr;
Thread *owner = nullptr;
};

} // namespace libscratchcpp
25 changes: 25 additions & 0 deletions test/assets/sound_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <scratchcpp/sound.h>
#include <scratchcpp/sprite.h>
#include <scratchcpp/thread.h>
#include <scratch/sound_p.h>
#include <audiooutputmock.h>
#include <audioplayermock.h>
Expand Down Expand Up @@ -128,6 +129,16 @@ TEST_F(SoundTest, Start)
sound.start();
}

TEST_F(SoundTest, StartWithOwner)
{
Sound sound("sound1", "a", "wav");
Thread thread(nullptr, nullptr, nullptr);

EXPECT_CALL(*m_player, start());
sound.start(&thread);
ASSERT_EQ(sound.owner(), &thread);
}

TEST_F(SoundTest, Stop)
{
Sound sound("sound1", "a", "wav");
Expand All @@ -136,6 +147,20 @@ TEST_F(SoundTest, Stop)
sound.stop();
}

TEST_F(SoundTest, StartAndStopWithOwner)
{
Sound sound("sound1", "a", "wav");
Thread thread(nullptr, nullptr, nullptr);

EXPECT_CALL(*m_player, start());
sound.start(&thread);
ASSERT_EQ(sound.owner(), &thread);

EXPECT_CALL(*m_player, stop());
sound.stop();
ASSERT_EQ(sound.owner(), nullptr);
}

TEST_F(SoundTest, IsPlaying)
{
Sound sound("sound1", "a", "wav");
Expand Down
Loading