-
Notifications
You must be signed in to change notification settings - Fork 26
RSDK-12150 Add AudioIn Component #493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
012902e
572f247
8a7b980
e98cf28
ab0bfdf
b773f7a
0d2725d
fc0869b
75fde70
41b598d
65de978
6d495c7
716790b
728d566
b997866
7616ba3
abc6bd5
9910fad
7628375
ff84ded
2febd1f
5787c7b
54ddbac
08e96f6
dd97070
0c7e883
faad112
5f66fb2
deab294
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,3 +15,4 @@ | |
| add_subdirectory(tflite) | ||
| add_subdirectory(simple) | ||
| add_subdirectory(complex) | ||
| add_subdirectory(audioin) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Copyright 2023 Viam Inc. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| add_executable(audioin_module | ||
| main.cpp | ||
| ) | ||
|
|
||
| target_link_libraries(audioin_module | ||
| PRIVATE Threads::Threads | ||
| viam-cpp-sdk::viamsdk | ||
| ) | ||
|
|
||
| install( | ||
| TARGETS audioin_module | ||
| COMPONENT examples | ||
| ) | ||
|
|
||
| add_executable(audioin_client) | ||
| target_sources(audioin_client | ||
| PRIVATE | ||
| client.cpp | ||
| ) | ||
|
|
||
|
|
||
| target_link_libraries(audioin_client | ||
| viam-cpp-sdk::viamsdk | ||
| ) | ||
|
|
||
| install( | ||
| TARGETS audioin_client | ||
| COMPONENT examples | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # VIAM Simple Module Example | ||
| This example goes through how to create a custom audio input resource using Viam's C++ SDK, and how to connect it to a Robot. | ||
|
|
||
| This is a limited document. For a more in-depth understanding of modules, see the [documentation](https://docs.viam.com/registry/). | ||
|
|
||
| Refer to main.cpp and the comments throughout for more information. For other C++ module examples, refer to the [simple module example](https://github.com/viamrobotics/viam-cpp-sdk/tree/main/src/viam/examples/modules/simple) or [complex module example](https://github.com/viamrobotics/viam-cpp-sdk/tree/main/src/viam/examples/modules/complex). | ||
|
|
||
| For a fully fleshed-out example of a C++ module that uses Github CI to upload to the Viam Registry, take a look at [module-example-cpp](https://github.com/viamrobotics/module-example-cpp). For a list of example modules in different Viam SDKs, take a look [here](https://github.com/viamrobotics/upload-module/#example-repos). | ||
|
|
||
| This is a limited document. For a more in-depth understanding of modules generally, see the [documentation](https://docs.viam.com/program/extend/modular-resources/). | ||
|
|
||
|
|
||
| Example Configuration: | ||
| ```json{ | ||
| "components": [ | ||
| { | ||
| "name": "sinewave-audio", | ||
| "api": "rdk:component:audio_in", | ||
| "model": "viam:audio_in:sinewave", | ||
| "attributes": { | ||
| "frequency": 440 | ||
| } | ||
| } | ||
| ], | ||
| "modules": [ | ||
| { | ||
| "type": "local", | ||
| "name": "my-module", | ||
| "executable_path": "/home/viam-cpp-sdk/build/src/viam/examples/modules/audioin/audioin_module" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| #include <fstream> | ||
| #include <iostream> | ||
| #include <memory> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #include <viam/sdk/common/instance.hpp> | ||
| #include <viam/sdk/common/proto_value.hpp> | ||
| #include <viam/sdk/components/audio_in.hpp> | ||
| #include <viam/sdk/log/logging.hpp> | ||
| #include <viam/sdk/robot/client.hpp> | ||
| #include <viam/sdk/rpc/dial.hpp> | ||
|
|
||
| using namespace viam::sdk; | ||
|
|
||
| int main() { | ||
| // Every Viam C++ SDK program must have one and only one Instance object which is created before | ||
| // any other C++ SDK objects and stays alive until all Viam C++ SDK objects are destroyed. | ||
| Instance inst; | ||
|
|
||
| // Update these with your robot's connection details | ||
| const char* uri = ""; // replace with your robot's URI | ||
| DialOptions dial_options; | ||
| dial_options.set_allow_insecure_downgrade(true); // set to false if connecting securely | ||
|
|
||
| // Uncomment and fill out your credentials details if connecting securely | ||
| // std::string type = "api-key"; | ||
| // std::string payload = "your-api-key-here"; | ||
| // Credentials credentials(type, payload); | ||
| // dial_options.set_credentials(credentials); | ||
|
|
||
| boost::optional<DialOptions> opts(dial_options); | ||
| std::string address(uri); | ||
| Options options(1, opts); | ||
|
|
||
| std::shared_ptr<RobotClient> robot = RobotClient::at_address(address, options); | ||
|
|
||
| // Print resources | ||
| VIAM_SDK_LOG(info) << "Resources:"; | ||
| std::vector<Name> resource_names = robot->resource_names(); | ||
| for (const Name& resource : resource_names) { | ||
| VIAM_SDK_LOG(info) << " " << resource; | ||
| } | ||
|
|
||
| // Get the AudioIn component (update with your component name) | ||
| auto audio_in = robot->resource_by_name<AudioIn>("sinewave-audio"); | ||
| if (!audio_in) { | ||
| VIAM_SDK_LOG(error) << "could not get 'sinewave-audio' resource from robot"; | ||
| return EXIT_FAILURE; | ||
| } | ||
|
|
||
| // Get audio properties | ||
| VIAM_SDK_LOG(info) << "Getting audio properties..."; | ||
| AudioIn::properties props = audio_in->get_properties(); | ||
| VIAM_SDK_LOG(info) << "Audio properties:"; | ||
| VIAM_SDK_LOG(info) << " sample_rate_hz: " << props.sample_rate_hz; | ||
| VIAM_SDK_LOG(info) << " num_channels: " << props.num_channels; | ||
| VIAM_SDK_LOG(info) << " supported_codecs: " << props.supported_codecs.size() << " codecs"; | ||
|
|
||
| VIAM_SDK_LOG(info) << "Retrieving 2 seconds of audio..."; | ||
|
|
||
| std::vector<uint8_t> all_audio_data; | ||
| int chunk_count = 0; | ||
|
|
||
| // Define chunk handler to collect audio data | ||
| auto chunk_handler = [&](AudioIn::audio_chunk&& chunk) -> bool { | ||
| chunk_count++; | ||
| VIAM_SDK_LOG(info) << "Received chunk " << chunk_count | ||
| << " - length: " << chunk.audio_data.size() | ||
| << " bytes, timestamp: " << chunk.start_timestamp_ns; | ||
|
|
||
| for (const auto& byte : chunk.audio_data) { | ||
| all_audio_data.push_back(static_cast<uint8_t>(byte)); | ||
| } | ||
|
|
||
| return true; // Continue receiving chunks | ||
| }; | ||
|
|
||
| // Get 2 seconds of audio (with previous_timestamp = 0 to start from now) | ||
| audio_in->get_audio("pcm16", chunk_handler, 2.0, 0); | ||
|
|
||
| VIAM_SDK_LOG(info) << "Total audio data received: " << all_audio_data.size() << " bytes"; | ||
| VIAM_SDK_LOG(info) << "Total chunks: " << chunk_count; | ||
|
|
||
| // Save to a WAV file | ||
| std::string filename = "sine_wave_audio.wav"; | ||
| std::ofstream outfile(filename, std::ios::binary); | ||
| if (!outfile.is_open()) { | ||
| VIAM_SDK_LOG(error) << "Failed to open file for writing"; | ||
| return EXIT_FAILURE; | ||
| } | ||
|
|
||
| // WAV file parameters | ||
| uint32_t sample_rate = props.sample_rate_hz; | ||
| uint16_t num_channels = props.num_channels; | ||
| uint16_t bits_per_sample = 16; // 16-bit PCM | ||
| uint32_t data_size = all_audio_data.size(); | ||
| uint32_t byte_rate = sample_rate * num_channels * (bits_per_sample / 8); | ||
| uint16_t block_align = num_channels * (bits_per_sample / 8); | ||
|
|
||
| // Write WAV header | ||
| // RIFF chunk descriptor | ||
| outfile.write("RIFF", 4); | ||
| uint32_t chunk_size = 36 + data_size; | ||
| outfile.write(reinterpret_cast<const char*>(&chunk_size), 4); | ||
| outfile.write("WAVE", 4); | ||
|
|
||
| outfile.write("fmt ", 4); | ||
| uint32_t subchunk1_size = 16; // PCM | ||
| outfile.write(reinterpret_cast<const char*>(&subchunk1_size), 4); | ||
| uint16_t audio_format = 1; // PCM | ||
| outfile.write(reinterpret_cast<const char*>(&audio_format), 2); | ||
| outfile.write(reinterpret_cast<const char*>(&num_channels), 2); | ||
| outfile.write(reinterpret_cast<const char*>(&sample_rate), 4); | ||
| outfile.write(reinterpret_cast<const char*>(&byte_rate), 4); | ||
| outfile.write(reinterpret_cast<const char*>(&block_align), 2); | ||
| outfile.write(reinterpret_cast<const char*>(&bits_per_sample), 2); | ||
|
|
||
| outfile.write("data", 4); | ||
| outfile.write(reinterpret_cast<const char*>(&data_size), 4); | ||
| outfile.write(reinterpret_cast<const char*>(all_audio_data.data()), data_size); | ||
|
|
||
| outfile.close(); | ||
| VIAM_SDK_LOG(info) << "Audio saved to " << filename; | ||
| VIAM_SDK_LOG(info) << "To play: open " << filename << " (or use any audio player)"; | ||
|
|
||
| return EXIT_SUCCESS; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| #include <chrono> | ||
| #include <cmath> | ||
| #include <cstring> | ||
| #include <iostream> | ||
| #include <memory> | ||
| #include <sstream> | ||
| #include <vector> | ||
|
|
||
| #include <viam/sdk/common/exception.hpp> | ||
| #include <viam/sdk/common/instance.hpp> | ||
| #include <viam/sdk/common/proto_value.hpp> | ||
| #include <viam/sdk/components/audio_in.hpp> | ||
| #include <viam/sdk/components/sensor.hpp> | ||
| #include <viam/sdk/config/resource.hpp> | ||
| #include <viam/sdk/log/logging.hpp> | ||
| #include <viam/sdk/module/service.hpp> | ||
| #include <viam/sdk/registry/registry.hpp> | ||
| #include <viam/sdk/resource/reconfigurable.hpp> | ||
|
|
||
| using namespace viam::sdk; | ||
|
|
||
| // Implements an AudioIn component that generates a sine wave for testing | ||
| class SineWaveAudioIn : public AudioIn, public Reconfigurable { | ||
| public: | ||
| SineWaveAudioIn(const ResourceConfig& cfg) : AudioIn(cfg.name()) { | ||
| this->reconfigure({}, cfg); | ||
| } | ||
|
|
||
| static std::vector<std::string> validate(const ResourceConfig&); | ||
|
|
||
| void reconfigure(const Dependencies&, const ResourceConfig&) override; | ||
|
|
||
| ProtoStruct do_command(const ProtoStruct&) override; | ||
|
|
||
| std::vector<GeometryConfig> get_geometries(const ProtoStruct&) override { | ||
| throw Exception("method not supported"); | ||
| } | ||
|
|
||
| properties get_properties(const ProtoStruct&) override; | ||
|
|
||
| void get_audio(std::string const& codec, | ||
| std::function<bool(audio_chunk&& chunk)> const& chunk_handler, | ||
| double const& duration_seconds, | ||
| int64_t const& previous_timestamp, | ||
| const ProtoStruct& extra) override; | ||
|
|
||
| private: | ||
| double frequency_{440.0}; | ||
| }; | ||
|
|
||
| std::vector<std::string> SineWaveAudioIn::validate(const ResourceConfig& cfg) { | ||
| auto itr = cfg.attributes().find("frequency"); | ||
| if (itr != cfg.attributes().end()) { | ||
| const double* freq = itr->second.get<double>(); | ||
| if (!freq) { | ||
| throw Exception("frequency must be a number value"); | ||
| } | ||
| if (*freq <= 0.0 || *freq > 20000.0) { | ||
| throw Exception("frequency must be between 0 and 20000 Hz"); | ||
| } | ||
| } | ||
| return {}; | ||
| } | ||
|
|
||
| void SineWaveAudioIn::reconfigure(const Dependencies&, const ResourceConfig& cfg) { | ||
| auto itr = cfg.attributes().find("frequency"); | ||
| if (itr != cfg.attributes().end()) { | ||
| const double* freq = itr->second.get<double>(); | ||
| if (freq) { | ||
| frequency_ = *freq; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ProtoStruct SineWaveAudioIn::do_command(const ProtoStruct& command) { | ||
| for (const auto& entry : command) { | ||
| VIAM_RESOURCE_LOG(info) << "Command entry " << entry.first; | ||
| } | ||
| return command; | ||
| } | ||
|
|
||
| AudioIn::properties SineWaveAudioIn::get_properties(const ProtoStruct&) { | ||
| AudioIn::properties props; | ||
| props.supported_codecs = {"pcm16"}; | ||
| props.sample_rate_hz = 44100; | ||
| props.num_channels = 1; | ||
| return props; | ||
| } | ||
|
|
||
| void SineWaveAudioIn::get_audio(std::string const& codec, | ||
| std::function<bool(audio_chunk&& chunk)> const& chunk_handler, | ||
| double const& duration_seconds, | ||
| int64_t const& previous_timestamp, | ||
| const ProtoStruct& extra) { | ||
| const int sample_rate = 44100; // 44.1 kHz | ||
| const double amplitude = 0.5; // Half volume | ||
| const int chunk_size = 1024; // Samples per chunk | ||
|
|
||
| int total_samples = static_cast<int>(duration_seconds * sample_rate); | ||
| int num_chunks = (total_samples + chunk_size - 1) / chunk_size; | ||
|
|
||
| VIAM_RESOURCE_LOG(info) << "Generating sine wave: " << frequency_ << "Hz, " << duration_seconds | ||
| << "s, " << num_chunks << " chunks"; | ||
|
|
||
| for (int chunk_idx = 0; chunk_idx < num_chunks; ++chunk_idx) { | ||
| int samples_in_chunk = std::min(chunk_size, total_samples - (chunk_idx * chunk_size)); | ||
| std::vector<int16_t> samples; | ||
| samples.reserve(samples_in_chunk); | ||
|
|
||
| // Generate sine wave samples | ||
| for (int i = 0; i < samples_in_chunk; ++i) { | ||
| int sample_idx = chunk_idx * chunk_size + i; | ||
| double t = static_cast<double>(sample_idx) / sample_rate; | ||
| double sample_value = amplitude * std::sin(2.0 * M_PI * frequency_ * t); | ||
|
||
|
|
||
| // Convert to 16-bit PCM | ||
| int16_t pcm_sample = static_cast<int16_t>(sample_value * 32767.0); | ||
| samples.push_back(pcm_sample); | ||
| } | ||
|
|
||
| audio_chunk chunk; | ||
|
|
||
| // Convert int16_t samples to uint8_t bytes | ||
| chunk.audio_data.resize(samples.size() * sizeof(int16_t)); | ||
|
||
| std::memcpy(chunk.audio_data.data(), samples.data(), chunk.audio_data.size()); | ||
|
|
||
| // Set audio info | ||
| chunk.info.codec = codec; | ||
| chunk.info.sample_rate_hz = sample_rate; | ||
| chunk.info.num_channels = 1; | ||
|
|
||
| // Get current timestamp in nanoseconds | ||
| auto now = std::chrono::system_clock::now(); | ||
| auto nanos = | ||
| std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count(); | ||
| chunk.start_timestamp_ns = nanos; | ||
| chunk.end_timestamp_ns = nanos; | ||
| chunk.sequence = chunk_idx; | ||
|
|
||
| // Call the chunk handler callback | ||
| if (!chunk_handler(std::move(chunk))) { | ||
| VIAM_RESOURCE_LOG(info) << "Chunk handler returned false, stopping"; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| VIAM_RESOURCE_LOG(info) << "Finished generating sine wave"; | ||
| } | ||
|
|
||
| int main(int argc, char** argv) try { | ||
| // Every Viam C++ SDK program must have one and only one Instance object which is created before | ||
| // any other C++ SDK objects and stays alive until all Viam C++ SDK objects are destroyed. | ||
| Instance inst; | ||
|
|
||
| Model sinewave_model("viam", "audio_in", "sinewave"); | ||
|
|
||
| std::shared_ptr<ModelRegistration> mr = std::make_shared<ModelRegistration>( | ||
| API::get<AudioIn>(), | ||
| sinewave_model, | ||
| [](Dependencies, ResourceConfig cfg) { return std::make_unique<SineWaveAudioIn>(cfg); }, | ||
| &SineWaveAudioIn::validate); | ||
|
|
||
| std::vector<std::shared_ptr<ModelRegistration>> mrs = {mr}; | ||
| auto my_mod = std::make_shared<ModuleService>(argc, argv, mrs); | ||
| my_mod->serve(); | ||
|
|
||
| return EXIT_SUCCESS; | ||
| } catch (const viam::sdk::Exception& ex) { | ||
| std::cerr << "main failed with exception: " << ex.what() << "\n"; | ||
| return EXIT_FAILURE; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of opaque as written, I think let's do a helper function template (or generic lambda) that expresses this in terms of
sizeof, which is what I assume is going onThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a template and helper function for writing wav files