diff --git a/.gitignore b/.gitignore index 6da3e83..c429dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build-conan/ module.tar.gz audio-module settings.json +CLAUDE.md diff --git a/README.md b/README.md index e621abd..8671d79 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,25 @@ The following attributes are available for the `viam:audio:speaker` model: | `latency` | int | **Optional** | Suggested output latency in milliseconds. This controls how much audio PortAudio buffers before making it available. Lower values (5-20ms) provide faster audio output but use more CPU time. Higher values (50-100ms) are more stable but less responsive. If not specified, uses the device's default low latency setting (typically 10-20ms). | | `volume` | int | **Optional** | Output volume as percentage (0-100). Supported on Linux devices only. On macOS, use the system volume controls (keyboard keys). | +#### DoCommand + +The speaker supports the following DoCommands: + +**`set_volume`** — Set the speaker output volume. +```json +{"set_volume": 75} +``` +- Value must be between 0 and 100. +- **Linux only.** On macOS, use the system volume controls (keyboard keys). +- Returns: `{"volume": 75}` + +**`stop`** — Immediately stop audio playback. +```json +{"stop": true} +``` +- Interrupts any in-progress `Play` call and silences the output. +- Returns: `{"stopped": true}` + ## Model viam:audio:discovery This model is used to discover audio devices on your machine. diff --git a/src/speaker.cpp b/src/speaker.cpp index 7502f38..36452cb 100644 --- a/src/speaker.cpp +++ b/src/speaker.cpp @@ -172,6 +172,17 @@ viam::sdk::ProtoStruct Speaker::do_command(const viam::sdk::ProtoStruct& command return viam::sdk::ProtoStruct{{"volume", static_cast(vol)}}; } + if (command.count("stop")) { + VIAM_SDK_LOG(info) << "Stop command received, interrupting playback"; + stop_requested_.store(true); + // Advance playback position to write position so no more audio is played. + std::lock_guard lock(stream_mu_); + if (audio_context_) { + audio_context_->playback_position.store(audio_context_->get_write_position(), std::memory_order_relaxed); + } + return viam::sdk::ProtoStruct{{"stopped", true}}; + } + throw std::invalid_argument("unknown command"); } @@ -189,6 +200,7 @@ void Speaker::play(std::vector const& audio_data, boost::optional info, const viam::sdk::ProtoStruct& extra) { std::lock_guard playback_lock(playback_mu_); + stop_requested_.store(false); VIAM_SDK_LOG(debug) << "Play called, adding samples to playback buffer"; @@ -307,6 +319,10 @@ void Speaker::play(std::vector const& audio_data, // Block until playback position catches up VIAM_SDK_LOG(debug) << "Waiting for playback to complete..."; while (playback_context->playback_position.load() - start_position < final_num_samples) { + if (stop_requested_.load()) { + VIAM_SDK_LOG(debug) << "Playback stopped by stop command"; + return; + } // Check if context changed (reconfigure happened) { std::lock_guard lock(stream_mu_); diff --git a/src/speaker.hpp b/src/speaker.hpp index fac120d..d7dd080 100644 --- a/src/speaker.hpp +++ b/src/speaker.hpp @@ -75,6 +75,9 @@ class Speaker final : public viam::sdk::AudioOut, public viam::sdk::Reconfigurab // Audio context for speaker playback (includes buffer and playback position tracking) std::shared_ptr audio_context_; + + // Flag to interrupt playback + std::atomic stop_requested_{false}; }; } // namespace speaker diff --git a/test/speaker_test.cpp b/test/speaker_test.cpp index 9bb9a6f..f263acb 100644 --- a/test/speaker_test.cpp +++ b/test/speaker_test.cpp @@ -203,6 +203,25 @@ TEST_F(SpeakerTest, DoCommandSetVolumeOutOfRange) { EXPECT_THROW(speaker.do_command(command_low), std::invalid_argument); } + +TEST_F(SpeakerTest, DoCommandStop) { + auto attributes = ProtoStruct{}; + ResourceConfig config( + "rdk:component:speaker", "", test_name_, attributes, "", + speaker::Speaker::model, LinkConfig{}, log_level::info); + + Dependencies deps{}; + speaker::Speaker speaker(deps, config, mock_pa_.get()); + + ProtoStruct command{{"stop", true}}; + auto result = speaker.do_command(command); + + + ASSERT_TRUE(result.count("stopped")); + EXPECT_EQ(speaker.stop_requested_, true); +} + + TEST_F(SpeakerTest, DoCommandUnknown) { auto attributes = ProtoStruct{}; ResourceConfig config(