diff --git a/include/interface/platform/File.h b/include/interface/platform/File.h index b991d7e40b..df16153fc1 100644 --- a/include/interface/platform/File.h +++ b/include/interface/platform/File.h @@ -21,6 +21,8 @@ struct File { virtual auto read(std::span buffer) -> std::size_t = 0; virtual auto write(std::span data) -> std::size_t = 0; + virtual auto read(std::span buffer) -> std::size_t = 0; + virtual auto read(std::span buffer) -> std::size_t = 0; virtual auto write(std::span data) -> std::size_t = 0; diff --git a/libs/AudioKit/CMakeLists.txt b/libs/AudioKit/CMakeLists.txt new file mode 100644 index 0000000000..ee44148731 --- /dev/null +++ b/libs/AudioKit/CMakeLists.txt @@ -0,0 +1,24 @@ +# Leka - LekaOS +# Copyright 2024 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_library(AudioKit STATIC) + +target_include_directories(AudioKit + PUBLIC + include +) + +target_sources(AudioKit + PRIVATE + source/AudioKit.cpp + source/WavFile.cpp +) + +target_link_libraries(AudioKit + mbed-os + CoreSTM32Hal + CoreDAC + CoreEventQueue + FileManagerKit +) diff --git a/libs/AudioKit/include/AudioKit.h b/libs/AudioKit/include/AudioKit.h new file mode 100644 index 0000000000..79f1e74e22 --- /dev/null +++ b/libs/AudioKit/include/AudioKit.h @@ -0,0 +1,53 @@ +// Leka - LekaOS +// Copyright 2024 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CoreEventQueue.h" +#include "DigitalOut.h" +#include "FileManagerKit.h" +#include "WavFile.h" +#include "interface/drivers/DAC.h" +#include "interface/drivers/STM32HalBasicTimer.h" + +namespace leka { + +class AudioKit +{ + public: + explicit AudioKit(interface::STM32HalBasicTimer &dac_timer, interface::DACDMA &dac) + : _dac_timer(dac_timer), _dac(dac) + { + // nothing to do + } + + void initialize(); + + void enableAudio(); + void disableAudio(); + + void play(const std::filesystem::path &path); + void stop(); + + void setData(uint32_t offset); + void run(); + + private: + mbed::DigitalOut _audio_enable {SOUND_ENABLE, 1}; + + interface::STM32HalBasicTimer &_dac_timer; + interface::DACDMA &_dac; + + FileManagerKit::File _file {}; + WavFile _wav_file {_file}; + + CoreEventQueue _event_queue {}; + + static constexpr uint32_t played_data_size {2000}; + std::array played_data {}; + + static constexpr uint8_t _repetition {10}; +}; + +} // namespace leka diff --git a/libs/AudioKit/include/WavFile.h b/libs/AudioKit/include/WavFile.h new file mode 100644 index 0000000000..3de031ac90 --- /dev/null +++ b/libs/AudioKit/include/WavFile.h @@ -0,0 +1,57 @@ +// Leka - LekaOS +// Copyright 2024 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "interface/platform/File.h" + +namespace leka { + +class WavFile +{ + constexpr static uint32_t blockID_fileType = 0x46464952; // corresponds to the letters 'RIFF' in reverse order + constexpr static uint32_t blockID_fileFormat = 0x45564157; // corresponds to the letters 'WAVE' in reverse order + constexpr static uint32_t blockID_format = 0x20746D66; // corresponds to the letters 'fmt ' in reverse order + constexpr static uint32_t blockID_data = 0x61746164; // corresponds to the letters 'data' in reverse order + + public: + using WavHeader = struct wavHeader { + uint32_t FileTypeBlockID; + uint32_t FileSize; // Nb bytes in file -8 bytes + uint32_t FileFormatID; + uint32_t FormatBlockID; + uint32_t FormatBlockSize; // Nb bytes in format block -16 bytes + uint16_t AudioFormat; + uint16_t NumChannels; + uint32_t SamplingRate; + uint32_t BytePerSec; + uint16_t BytePerSampleBlock; + uint16_t BitsPerSample; + uint32_t DataBlockID; + uint32_t DataSize; + }; + + explicit WavFile(interface::File &file) : _file(file) {} + + auto open(std::filesystem::path filename) -> bool; + + [[nodiscard]] auto getHeader() const -> const WavHeader &; + + auto read(std::span buffer) -> bool; + [[nodiscard]] auto isEndOfFile() const -> bool; + + private: + void readHeader(); + + interface::File &_file; + + const std::filesystem::path directory {"/fs/home/wav"}; + const std::string extension {".wav"}; + + WavHeader _header {}; + + bool _is_eof {false}; +}; + +} // namespace leka diff --git a/libs/AudioKit/source/AudioKit.cpp b/libs/AudioKit/source/AudioKit.cpp new file mode 100644 index 0000000000..b312a8adf8 --- /dev/null +++ b/libs/AudioKit/source/AudioKit.cpp @@ -0,0 +1,86 @@ +// Leka - LekaOS +// Copyright 2024 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "AudioKit.h" + +#include "rtos/ThisThread.h" + +#include "LogKit.h" + +using namespace leka; +using namespace std::chrono_literals; + +void AudioKit::initialize() +{ + _event_queue.dispatch_forever(); + + auto onHalfTransfer = [this] { setData(0); }; + auto onCompleteTransfer = [this] { setData(played_data_size / 2); }; + + _dac.registerDMACallbacks([this, onHalfTransfer] { _event_queue.call(onHalfTransfer); }, + [this, onCompleteTransfer] { _event_queue.call(onCompleteTransfer); }); + _dac.registerDataToPlay(played_data); + + constexpr auto file_sample_rate = float {44'100.0}; + constexpr auto timer_rate = float {file_sample_rate * _repetition}; + + _dac_timer.initialize(timer_rate); + _dac.initialize(); +} + +void AudioKit::enableAudio() +{ + _audio_enable = 1; +} + +void AudioKit::disableAudio() +{ + stop(); + _audio_enable = 0; +} + +void AudioKit::play(const std::filesystem::path &path) +{ + auto is_open = _wav_file.open(path); + if (!is_open) { + return; + } + + setData(0); + setData(played_data_size / 2); + + enableAudio(); + + run(); +} + +void AudioKit::stop() +{ + _dac.stop(); +} + +void AudioKit::setData(uint32_t offset) +{ + if (_wav_file.isEndOfFile()) { + stop(); + return; + } + + constexpr auto file_data_size = played_data_size / _repetition / 2; + auto file_data = std::array {}; + + _wav_file.read(file_data); + + for (uint32_t i = 0; i < file_data.size(); i++) { + auto normalized_value = static_cast((file_data.at(i) + 0x8000) >> 4); + std::fill_n(played_data.begin() + offset + i * _repetition, _repetition, normalized_value); + } + + rtos::ThisThread::sleep_for(2ms); // Related to played_data_size and _repetition +} + +void AudioKit::run() +{ + _dac.start(); +} diff --git a/libs/AudioKit/source/WavFile.cpp b/libs/AudioKit/source/WavFile.cpp new file mode 100644 index 0000000000..6183c5be90 --- /dev/null +++ b/libs/AudioKit/source/WavFile.cpp @@ -0,0 +1,56 @@ +// Leka - LekaOS +// Copyright 2024 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "WavFile.h" +#include + +#include "FileManagerKit.h" + +using namespace leka; + +auto WavFile::open(std::filesystem::path filename) -> bool +{ + auto path = directory / filename; + path += extension; + + if (FileManagerKit::file_is_missing(path)) { + return false; + } + + auto is_open = _file.open(path, "r"); + + readHeader(); + + _is_eof = false; + + return is_open; +} + +void WavFile::readHeader() +{ + std::array buffer {}; + + _file.read(buffer); + + std::memcpy(&_header, &buffer, sizeof(_header)); +} + +auto WavFile::getHeader() const -> const WavHeader & +{ + return _header; +} + +auto WavFile::read(std::span buffer) -> bool +{ + auto bytes_read = _file.read(buffer); + + _is_eof = bytes_read != std::size(buffer); + auto is_not_eof = !_is_eof; + return is_not_eof; +} + +auto WavFile::isEndOfFile() const -> bool +{ + return _is_eof; +} diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index f498e76cf5..ce0d24ddce 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 add_subdirectory(${LIBS_DIR}/ActivityKit) +add_subdirectory(${LIBS_DIR}/AudioKit) add_subdirectory(${LIBS_DIR}/BatteryKit) add_subdirectory(${LIBS_DIR}/BehaviorKit) add_subdirectory(${LIBS_DIR}/BLEKit) diff --git a/libs/FileManagerKit/include/FileManagerKit.h b/libs/FileManagerKit/include/FileManagerKit.h index b519adeddc..75fe88c64c 100644 --- a/libs/FileManagerKit/include/FileManagerKit.h +++ b/libs/FileManagerKit/include/FileManagerKit.h @@ -25,6 +25,8 @@ struct File : public interface::File, public mbed::NonCopyable { auto read(std::span buffer) -> std::size_t final; auto write(std::span data) -> std::size_t final; + auto read(std::span buffer) -> std::size_t final; + auto read(std::span buffer) -> std::size_t final; auto write(std::span data) -> std::size_t final; diff --git a/libs/FileManagerKit/source/File.cpp b/libs/FileManagerKit/source/File.cpp index ba30698850..35bcfdba53 100644 --- a/libs/FileManagerKit/source/File.cpp +++ b/libs/FileManagerKit/source/File.cpp @@ -60,6 +60,11 @@ auto File::read(std::span buffer) -> std::size_t return std::fread(buffer.data(), sizeof(uint8_t), buffer.size(), _file.get()); } +auto File::read(std::span buffer) -> std::size_t +{ + return std::fread(buffer.data(), sizeof(int16_t), buffer.size(), _file.get()); +} + auto File::write(std::span data) -> std::size_t { return std::fwrite(data.data(), sizeof(uint8_t), data.size(), _file.get()); diff --git a/spikes/lk_audio/CMakeLists.txt b/spikes/lk_audio/CMakeLists.txt index afa028ed84..79b81d2cac 100644 --- a/spikes/lk_audio/CMakeLists.txt +++ b/spikes/lk_audio/CMakeLists.txt @@ -15,7 +15,10 @@ target_sources(spike_lk_audio ) target_link_libraries(spike_lk_audio - FileManagerKit + CoreSTM32Hal + CoreDAC + AudioKit + BLEKit ) target_link_custom_leka_targets(spike_lk_audio) diff --git a/spikes/lk_audio/main.cpp b/spikes/lk_audio/main.cpp index 3483f70263..991017ef8b 100644 --- a/spikes/lk_audio/main.cpp +++ b/spikes/lk_audio/main.cpp @@ -1,34 +1,45 @@ // Leka - LekaOS -// Copyright 2022 APF France handicap +// Copyright 2024 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include -#include - -#include "drivers/AnalogOut.h" -#include "drivers/DigitalOut.h" -#include "platform/mbed_wait_api.h" #include "rtos/ThisThread.h" -#include "rtos/Thread.h" +#include "BLEKit.h" +#include "BLEServiceConfig.h" + +#include "AudioKit.h" +#include "CoreDAC.h" +#include "CoreSTM32Hal.h" +#include "CoreSTM32HalBasicTimer.h" #include "FATFileSystem.h" -#include "FileManagerKit.h" #include "LogKit.h" #include "SDBlockDevice.h" using namespace leka; using namespace std::chrono_literals; -auto sd_bd = SDBlockDevice {SD_SPI_MOSI, SD_SPI_MISO, SD_SPI_SCK}; -auto fatfs = FATFileSystem {"fs"}; +auto sd_bd = SDBlockDevice {SD_SPI_MOSI, SD_SPI_MISO, SD_SPI_SCK}; +auto fatfs = FATFileSystem {"fs"}; +auto filename = std::filesystem::path {}; + +auto hal = CoreSTM32Hal {}; + +namespace audio { + +namespace internal { + + extern "C" auto hal_timer = CoreSTM32HalBasicTimer {hal}; + extern "C" auto coredac = CoreDAC {hal, hal_timer}; -const auto sound_file_path = std::filesystem::path {"/fs/home/wav/fur-elise.wav"}; -auto file = FileManagerKit::File {sound_file_path}; +} // namespace internal -auto thread_audio = rtos::Thread {}; +auto kit = AudioKit {internal::hal_timer, internal::coredac}; -auto audio_enable = mbed::DigitalOut {SOUND_ENABLE, 1}; -auto audio_output = mbed::AnalogOut {MCU_SOUND_OUT}; +} // namespace audio + +auto service_config = BLEServiceConfig {}; +auto services = std::to_array({&service_config}); +auto blekit = BLEKit {}; void initializeSD() { @@ -40,29 +51,10 @@ void initializeSD() fatfs.mount(&sd_bd); } -void playSound() +void play(std::string filename) { - static const auto _n_bytes_to_read = int {512}; // arbitrary - auto _buffer = std::array {0}; - - auto _ns_sample_rate = uint32_t {22676}; // 1,000,000,000 / 44,100 (in ns) - auto _ns_sample_rate_adapted = _ns_sample_rate * 1.7; // arbitrary, 1s in MCU is not exactly 1s in real life - auto bytesread = uint32_t {_n_bytes_to_read}; - - /* START READ WAV */ - while (bytesread == _n_bytes_to_read) { - // Read "_n_bytes_to_read" from file at each iteration. Real bytes read is given by "bytesread" - if (bytesread = file.read(_buffer.data(), _n_bytes_to_read); bytesread != 0) { - // Play every 2-bytes (sound encoded in 16 bits) - for (uint32_t j = 0; j < bytesread; j += 4) { // Play one channel, data for stereo are alternate - audio_output.write_u16((_buffer.at(j + 1) + 0x8000) >> - 1); // offset for int16 data (0x8000) and volume 50% (>>1) - - wait_ns(_ns_sample_rate_adapted); // adjust play speed - } - } - } - /* END READ WAV*/ + log_info("Play file: %s", filename.c_str()); + audio::kit.play(filename); } auto main() -> int @@ -70,18 +62,22 @@ auto main() -> int logger::init(); log_info("Hello, World!\n\n"); + rtos::ThisThread::sleep_for(1s); initializeSD(); - if (FileManagerKit::file_is_missing(sound_file_path)) { - return 1; - } + audio::kit.initialize(); - while (true) { - file.open(sound_file_path); - playSound(); - file.close(); + blekit.setServices(services); + blekit.init(); + + service_config.onRobotNameUpdated([](const std::array &robot_name) { + const auto *end_index = std::find(robot_name.begin(), robot_name.end(), '\0'); + filename = std::string {robot_name.begin(), end_index}; + play(filename); + }); - rtos::ThisThread::sleep_for(1s); + while (true) { + rtos::ThisThread::sleep_for(1min); } } diff --git a/tests/unit/mocks/mocks/leka/File.h b/tests/unit/mocks/mocks/leka/File.h index eab081d9be..87c14adbcd 100644 --- a/tests/unit/mocks/mocks/leka/File.h +++ b/tests/unit/mocks/mocks/leka/File.h @@ -27,6 +27,8 @@ class File : public interface::File MOCK_METHOD(size_t, read, (std::span), (override)); MOCK_METHOD(size_t, write, (std::span), (override)); + MOCK_METHOD(size_t, read, (std::span), (override)); + MOCK_METHOD(size_t, read, (uint8_t *, uint32_t), (override)); MOCK_METHOD(size_t, write, (const uint8_t *, uint32_t), (override));