diff --git a/.gitmodules b/.gitmodules
index e642f64a..09bc02f6 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -28,3 +28,6 @@
[submodule "thirdparty/esp-idf-components/embedded_cli/EmbeddedCLI"]
path = thirdparty/esp-idf-components/embedded_cli/EmbeddedCLI
url = https://github.com/AndreRenaud/EmbeddedCLI.git
+[submodule "thirdparty/uWebSockets/uWebSockets"]
+ path = thirdparty/uWebSockets/uWebSockets
+ url = https://github.com/uNetworking/uWebSockets.git
diff --git a/effects/CMakeLists.txt b/effects/CMakeLists.txt
new file mode 100644
index 00000000..eb68ea90
--- /dev/null
+++ b/effects/CMakeLists.txt
@@ -0,0 +1,5 @@
+add_subdirectory(abstract_dsp)
+add_subdirectory(chorus)
+add_subdirectory(gate)
+add_subdirectory(valvestate)
+add_subdirectory(wah)
diff --git a/firmware/.idea/.gitignore b/firmware/.idea/.gitignore
deleted file mode 100644
index 13566b81..00000000
--- a/firmware/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/firmware/.idea/.name b/firmware/.idea/.name
deleted file mode 100644
index 8a3960e9..00000000
--- a/firmware/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-esp32-dsp
\ No newline at end of file
diff --git a/firmware/.idea/codeStyles/Project.xml b/firmware/.idea/codeStyles/Project.xml
deleted file mode 100644
index 7cc81a4f..00000000
--- a/firmware/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/firmware/.idea/codeStyles/codeStyleConfig.xml b/firmware/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123c..00000000
--- a/firmware/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/firmware/.idea/discord.xml b/firmware/.idea/discord.xml
deleted file mode 100644
index d8e95616..00000000
--- a/firmware/.idea/discord.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/firmware/.idea/firmware.iml b/firmware/.idea/firmware.iml
deleted file mode 100644
index f08604bb..00000000
--- a/firmware/.idea/firmware.iml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/firmware/.idea/misc.xml b/firmware/.idea/misc.xml
deleted file mode 100644
index 4421b5f9..00000000
--- a/firmware/.idea/misc.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/firmware/.idea/modules.xml b/firmware/.idea/modules.xml
deleted file mode 100644
index cb860b65..00000000
--- a/firmware/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/firmware/.idea/protoeditor.xml b/firmware/.idea/protoeditor.xml
deleted file mode 100644
index 37f5878a..00000000
--- a/firmware/.idea/protoeditor.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/firmware/.idea/vcs.xml b/firmware/.idea/vcs.xml
deleted file mode 100644
index 4b3dd066..00000000
--- a/firmware/.idea/vcs.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt
index dcb434af..1b56bace 100644
--- a/firmware/CMakeLists.txt
+++ b/firmware/CMakeLists.txt
@@ -1,58 +1,35 @@
-if(ESP_PLATFORM)
- cmake_minimum_required(VERSION 3.13)
+cmake_minimum_required(VERSION 3.13)
- set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD 20)
- option(SHRAPNEL_RESET_WIFI_CREDENTIALS
- "Remove the provisioned Wi-Fi credentials at power up"
- OFF
- )
+option(SHRAPNEL_RESET_WIFI_CREDENTIALS
+ "Remove the provisioned Wi-Fi credentials at power up"
+ OFF
+)
- if(SHRAPNEL_RESET_WIFI_CREDENTIALS)
- add_compile_definitions(SHRAPNEL_RESET_WIFI_CREDENTIALS=1)
- endif()
-
- list(APPEND EXTRA_COMPONENT_DIRS ../thirdparty/esp-idf-components/esp-dsp)
-
- include($ENV{IDF_PATH}/tools/cmake/project.cmake)
- project(esp32-dsp)
-
- add_subdirectory(../effects/abstract_dsp abstract_dsp)
- add_subdirectory(../effects/chorus chorus)
- add_subdirectory(../effects/gate gate)
- add_subdirectory(../effects/valvestate valvestate)
- add_subdirectory(../effects/wah wah)
- add_subdirectory(../thirdparty/esp-idf-components/cppcodec cppcodec)
- add_subdirectory(../thirdparty/esp-idf-components/embedded_cli embedded_cli)
- add_subdirectory(../thirdparty/esp-idf-components/esp32-fft esp32-fft)
- add_subdirectory(../thirdparty/esp-idf-components/etl etl)
- add_subdirectory(../thirdparty/esp-idf-components/nanopb nanopb)
- add_subdirectory(components/api)
- add_subdirectory(components/audio)
- add_subdirectory(components/audio_events)
- add_subdirectory(components/audio_param)
- add_subdirectory(components/cmd_handling)
- add_subdirectory(components/compiler_warning_flags)
- add_subdirectory(components/esp_persistence)
- add_subdirectory(components/float_convert)
- add_subdirectory(components/hardware)
- add_subdirectory(components/heap_tracing)
- add_subdirectory(components/i2c)
- add_subdirectory(components/i2s)
- add_subdirectory(components/messages)
- add_subdirectory(components/midi_handling)
- add_subdirectory(components/midi_mapping)
- add_subdirectory(components/midi_protocol)
- add_subdirectory(components/os)
- add_subdirectory(components/pcm3060)
- add_subdirectory(components/persistence)
- add_subdirectory(components/presets)
- add_subdirectory(components/server)
- add_subdirectory(components/shrapnel_console)
- add_subdirectory(components/wifi)
-
- return()
+if(SHRAPNEL_RESET_WIFI_CREDENTIALS)
+ add_compile_definitions(SHRAPNEL_RESET_WIFI_CREDENTIALS=1)
endif()
-add_subdirectory(main)
-add_subdirectory(components)
+list(APPEND EXTRA_COMPONENT_DIRS ../thirdparty/esp-idf-components/esp-dsp)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+project(esp32-dsp)
+
+add_subdirectory(../effects effects)
+add_subdirectory(../shrapnel shrapnel)
+add_subdirectory(../thirdparty/esp-idf-components/cppcodec cppcodec)
+add_subdirectory(../thirdparty/esp-idf-components/embedded_cli embedded_cli)
+add_subdirectory(../thirdparty/esp-idf-components/esp32-fft esp32-fft)
+add_subdirectory(../thirdparty/esp-idf-components/etl etl)
+add_subdirectory(../thirdparty/esp-idf-components/nanopb nanopb)
+add_subdirectory(components/esp_midi_uart)
+add_subdirectory(components/esp_persistence)
+add_subdirectory(components/hardware)
+add_subdirectory(components/heap_tracing)
+add_subdirectory(components/i2c)
+add_subdirectory(components/i2s)
+add_subdirectory(components/pcm3060)
+add_subdirectory(components/server)
+add_subdirectory(components/shrapnel_console)
+add_subdirectory(components/wifi)
diff --git a/firmware/components/CMakeLists.txt b/firmware/components/CMakeLists.txt
index a925f8a9..e69de29b 100644
--- a/firmware/components/CMakeLists.txt
+++ b/firmware/components/CMakeLists.txt
@@ -1,18 +0,0 @@
-if(ESP_PLATFORM)
- return()
-endif()
-add_subdirectory(api)
-add_subdirectory(audio_events)
-add_subdirectory(audio_param)
-add_subdirectory(cmd_handling)
-add_subdirectory(compiler_warning_flags)
-add_subdirectory(float_convert)
-add_subdirectory(hardware)
-add_subdirectory(heap_tracing)
-add_subdirectory(messages)
-add_subdirectory(midi_handling)
-add_subdirectory(midi_mapping)
-add_subdirectory(midi_protocol)
-add_subdirectory(os)
-add_subdirectory(persistence)
-add_subdirectory(presets)
diff --git a/firmware/components/cmd_handling/include/cmd_handling.h b/firmware/components/cmd_handling/include/cmd_handling.h
deleted file mode 100644
index 57a5da90..00000000
--- a/firmware/components/cmd_handling/include/cmd_handling.h
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2022 Barabas Raffai
- *
- * This file is part of ShrapnelDSP.
- *
- * ShrapnelDSP is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free
- * Software Foundation, either version 3 of the License, or (at your option)
- * any later version.
- *
- * ShrapnelDSP is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * ShrapnelDSP. If not, see .
- */
-
-#pragma once
-
-#include "audio_param.h"
-#include "cmd_handling_api.h"
-#include "esp_err.h"
-#include "esp_log.h"
-#include "etl/list.h"
-#include "freertos/FreeRTOS.h"
-#include "freertos/task.h"
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace shrapnel::parameters {
-
-template
-class CommandHandling final
-{
-public:
- using SendMessageCallback =
- etl::delegate)>;
-
- /** \brief
- *
- * \param[in] a_param Data received through \ref dispatch() is
- * translated to binary and sent to this object.
- */
- CommandHandling(std::shared_ptr a_param,
- SendMessageCallback a_send_message)
- : param(a_param),
- send_message(a_send_message)
- {
- }
-
- void dispatch(const ApiMessage &a_message, int fd)
- {
-#if !defined(TESTING)
- ESP_LOGI(TAG,
- "%s stack %d",
- __FUNCTION__,
- uxTaskGetStackHighWaterMark(NULL));
-#endif
-
- std::visit(
- [&](const auto &message) -> void
- {
- using T = std::decay_t;
-
- if constexpr(std::is_same_v)
- {
- parameter_update(message, fd);
- }
- else if constexpr(std::is_same_v)
- {
- initialise_parameters();
- }
- else
- {
- ESP_LOGE(TAG, "Unhandled message type");
- }
- },
- a_message);
- }
-
-private:
- void parameter_update(const Update &message, int fd)
- {
- int rc = param->update(message.id, message.value);
- if(rc != 0)
- {
- ESP_LOGE(TAG,
- "Failed to update parameter (%s) with value %f",
- message.id.data(),
- message.value);
- }
-
- send_message(message, fd);
- }
-
- void initialise_parameters()
- {
- for(const auto &[key, value] : *param)
- {
- Update message = {
- .id{id_t{key}},
- .value{value->get()},
- };
-
- send_message(message, std::nullopt);
- }
- }
-
- std::shared_ptr param;
- SendMessageCallback send_message;
-
- static inline const char *TAG = "cmd_handling";
-};
-
-} // namespace shrapnel::parameters
diff --git a/firmware/components/esp_midi_uart/CMakeLists.txt b/firmware/components/esp_midi_uart/CMakeLists.txt
new file mode 100644
index 00000000..f69d7fc3
--- /dev/null
+++ b/firmware/components/esp_midi_uart/CMakeLists.txt
@@ -0,0 +1,12 @@
+add_library(shrapnel_esp_midi_uart STATIC)
+add_library(shrapnel::esp_midi_uart ALIAS shrapnel_esp_midi_uart)
+
+target_sources(shrapnel_esp_midi_uart PRIVATE src/esp_midi_uart.cpp)
+
+target_include_directories(shrapnel_esp_midi_uart PUBLIC include)
+
+target_link_libraries(shrapnel_esp_midi_uart PUBLIC
+ shrapnel::midi_protocol
+ idf::freertos
+ idf::driver
+)
\ No newline at end of file
diff --git a/firmware/components/midi_protocol/include/midi_uart.h b/firmware/components/esp_midi_uart/include/esp_midi_uart.h
similarity index 77%
rename from firmware/components/midi_protocol/include/midi_uart.h
rename to firmware/components/esp_midi_uart/include/esp_midi_uart.h
index 57ade201..1cb131c8 100644
--- a/firmware/components/midi_protocol/include/midi_uart.h
+++ b/firmware/components/esp_midi_uart/include/esp_midi_uart.h
@@ -20,22 +20,13 @@
#pragma once
#include "freertos/FreeRTOS.h"
-#include
+#include "midi_uart.h"
#include
#include
-#include
-#include
-namespace shrapnel {
-namespace midi {
+namespace shrapnel::midi {
-class MidiUartBase
-{
-public:
- virtual std::optional get_byte(TickType_t timeout) = 0;
-};
-
-class EspMidiUart final : public MidiUartBase
+class EspMidiUart final : public MidiUartBase
{
public:
EspMidiUart(uart_port_t uart, gpio_num_t rx_io);
@@ -46,5 +37,4 @@ class EspMidiUart final : public MidiUartBase
uart_port_t uart;
};
-} // namespace midi
-} // namespace shrapnel
+} // namespace shrapnel::midi
\ No newline at end of file
diff --git a/firmware/components/midi_protocol/src/midi_uart.cpp b/firmware/components/esp_midi_uart/src/esp_midi_uart.cpp
similarity index 97%
rename from firmware/components/midi_protocol/src/midi_uart.cpp
rename to firmware/components/esp_midi_uart/src/esp_midi_uart.cpp
index e56841d1..58f70cd5 100644
--- a/firmware/components/midi_protocol/src/midi_uart.cpp
+++ b/firmware/components/esp_midi_uart/src/esp_midi_uart.cpp
@@ -17,7 +17,7 @@
* ShrapnelDSP. If not, see .
*/
-#include "midi_uart.h"
+#include "esp_midi_uart.h"
#include
namespace shrapnel {
@@ -62,4 +62,4 @@ std::optional EspMidiUart::get_byte(TickType_t timeout)
}
} // namespace midi
-} // namespace shrapnel
+} // namespace shrapnel
\ No newline at end of file
diff --git a/firmware/components/messages/include/messages.h b/firmware/components/messages/include/messages.h
deleted file mode 100644
index 90ae889b..00000000
--- a/firmware/components/messages/include/messages.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2022 Barabas Raffai
- *
- * This file is part of ShrapnelDSP.
- *
- * ShrapnelDSP is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free
- * Software Foundation, either version 3 of the License, or (at your option)
- * any later version.
- *
- * ShrapnelDSP is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * ShrapnelDSP. If not, see .
- */
-
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-using ApiMessage =
- std::variant;
-using FileDescriptor = std::optional;
-using AppMessage = std::pair;
-
-namespace shrapnel::api {
-
-template <>
-std::optional> to_bytes(const ApiMessage &message,
- std::span buffer);
-
-template <>
-std::optional from_bytes(std::span buffer);
-
-} // namespace shrapnel::api
\ No newline at end of file
diff --git a/firmware/components/server/include/server.h b/firmware/components/server/include/server.h
index 69c7e995..fce3b411 100644
--- a/firmware/components/server/include/server.h
+++ b/firmware/components/server/include/server.h
@@ -30,18 +30,22 @@ namespace shrapnel {
class Server
{
public:
- Server(QueueBase *in_queue, QueueBase *out_queue);
+ Server(etl::delegate &in,
+ uint32_t time_to_wait)> output_message,
+ QueueBase>> *out_queue);
void start();
void stop();
- void send_message(const AppMessage &message);
+ void send_message(const std::pair> &message);
private:
httpd_handle_t server = nullptr;
- QueueBase *in_queue;
- QueueBase *out_queue;
+ etl::delegate &in,
+ uint32_t time_to_wait)>
+ output_message;
+ QueueBase>> *out_queue;
/*
* TODO espressif's http server drops some calls to the work function when
@@ -53,7 +57,8 @@ class Server
SemaphoreHandle_t work_semaphore;
friend esp_err_t websocket_get_handler(httpd_req_t *req);
friend void websocket_send(void *arg);
- friend void send_websocket_message(Server &self, const AppMessage &message);
+ friend void send_websocket_message(
+ Server &self, const std::pair> &message);
};
} // namespace shrapnel
diff --git a/firmware/components/server/src/server.cpp b/firmware/components/server/src/server.cpp
index 5b8c5ecc..c693fa0f 100644
--- a/firmware/components/server/src/server.cpp
+++ b/firmware/components/server/src/server.cpp
@@ -34,10 +34,12 @@ namespace shrapnel {
static void debug_print_sent_message(const ApiMessage &message);
static void debug_print_received_message(const ApiMessage &message);
-Server::Server(QueueBase *a_in_queue,
- QueueBase *a_out_queue)
+Server::Server(
+ etl::delegate &in,
+ uint32_t time_to_wait)> a_output_message,
+ QueueBase>> *a_out_queue)
{
- in_queue = a_in_queue;
+ output_message = a_output_message;
out_queue = a_out_queue;
work_semaphore = xSemaphoreCreateBinary();
assert(work_semaphore);
@@ -145,12 +147,8 @@ esp_err_t websocket_get_handler(httpd_req_t *req)
if(message.has_value())
{
debug_print_received_message(*message);
- auto out = AppMessage{*message, fd};
- int queue_rc = self->in_queue->send(&out, pdMS_TO_TICKS(100));
- if(queue_rc != pdPASS)
- {
- ESP_LOGE(TAG, "in_queue message dropped");
- }
+ auto out = std::pair{*message, fd};
+ self->output_message(out, pdMS_TO_TICKS(100));
}
else
{
@@ -168,9 +166,9 @@ void websocket_send(void *arg)
{
auto self = reinterpret_cast(arg);
- AppMessage message;
- int rc = self->out_queue->receive(&message, 0);
- if(!rc)
+ std::pair> message;
+ auto rc = self->out_queue->receive(&message, 0);
+ if(rc != queue_error::SUCCESS)
{
ESP_LOGE(TAG, "%s failed to receive from queue", __FUNCTION__);
return;
@@ -192,7 +190,8 @@ void websocket_send(void *arg)
xSemaphoreGive(self->work_semaphore);
}
-void send_websocket_message(Server &self, const AppMessage &message)
+void send_websocket_message(
+ Server &self, const std::pair> &message)
{
std::array memory{};
auto buffer = std::span{memory};
@@ -259,34 +258,25 @@ static void debug_print_sent_message(const ApiMessage &message)
{
etl::string<128> debug;
etl::string_stream debug_stream{debug};
- std::visit(
- [&](const auto &message) -> void
- {
- debug_stream << message;
- ESP_LOGD(TAG, "sending message: %s", debug.data());
- },
- message);
+ debug_stream << message;
+ ESP_LOGD(TAG, "sending message: %s", debug.data());
}
static void debug_print_received_message(const ApiMessage &message)
{
etl::string<128> debug;
etl::string_stream debug_stream{debug};
- std::visit(
- [&](const auto &message) -> void
- {
- debug_stream << message;
- ESP_LOGD(TAG, "received message: %s", debug.data());
- },
- message);
+ debug_stream << message;
+ ESP_LOGD(TAG, "received message: %s", debug.data());
}
-void Server::send_message(const AppMessage &message)
+void Server::send_message(
+ const std::pair> &message)
{
ESP_LOGD(
TAG, "%s called from task: %s", __FUNCTION__, pcTaskGetName(nullptr));
- if(errQUEUE_FULL == out_queue->send(&message, pdMS_TO_TICKS(100)))
+ if(queue_error::SUCCESS != out_queue->send(&message, pdMS_TO_TICKS(100)))
{
ESP_LOGE(TAG, "Failed to send message to websocket");
return;
diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt
index 9a9dee69..d9e96b5b 100644
--- a/firmware/main/CMakeLists.txt
+++ b/firmware/main/CMakeLists.txt
@@ -1,54 +1,33 @@
-if(ESP_PLATFORM)
- # FIXME: move the main_thread to a separate component so the build system can
- # be used more effectively to keep it free from hardware dependencies
- # It should link to no idf:: targets, or any espressif specific targets from
- # shrapnel::
- idf_component_register(SRCS main.cpp
- INCLUDE_DIRS include
- REQUIRES
- driver
- esp_http_server
- mdns
- nvs_flash
- LDFRAGMENTS esp-dsp.lf
- )
+idf_component_register(SRCS main.cpp
+ REQUIRES
+ driver
+ esp_http_server
+ mdns
+ nvs_flash
+ LDFRAGMENTS esp-dsp.lf
+)
- target_link_libraries(${COMPONENT_LIB}
- PRIVATE
- shrapnel::abstract_dsp
- shrapnel::audio_events
- shrapnel::cmd_handling
- shrapnel::console
- shrapnel::esp_persistence
- shrapnel::etl
- shrapnel::hardware
- shrapnel::heap_tracing
- shrapnel::i2s
- shrapnel::messages
- shrapnel::midi_handling
- shrapnel::midi_mapping
- shrapnel::midi_protocol
- shrapnel::os
- shrapnel::pcm3060
- shrapnel::persistence
- shrapnel::presets
- shrapnel::server
- shrapnel::wifi
- shrapnel::compiler_warning_flags)
-else()
- add_library(main INTERFACE)
- add_library(shrapnel::main ALIAS main)
-
- target_include_directories(main INTERFACE include)
- target_link_libraries(main INTERFACE
- shrapnel::audio_events
- shrapnel::cmd_handling
- shrapnel::etl
- shrapnel::messages
- shrapnel::midi_handling
- shrapnel::midi_mapping
- shrapnel::midi_protocol
- shrapnel::os
- shrapnel::persistence
- shrapnel::presets)
-endif()
+target_link_libraries(${COMPONENT_LIB}
+ PRIVATE
+ shrapnel::abstract_dsp
+ shrapnel::audio_events
+ shrapnel::cmd_handling
+ shrapnel::console
+ shrapnel::esp_midi_uart
+ shrapnel::esp_persistence
+ shrapnel::etl
+ shrapnel::hardware
+ shrapnel::heap_tracing
+ shrapnel::i2s
+ shrapnel::main_thread
+ shrapnel::messages
+ shrapnel::midi_handling
+ shrapnel::midi_mapping
+ shrapnel::midi_protocol
+ shrapnel::os
+ shrapnel::pcm3060
+ shrapnel::persistence
+ shrapnel::presets
+ shrapnel::server
+ shrapnel::wifi
+ shrapnel::compiler_warning_flags)
diff --git a/firmware/main/main.cpp b/firmware/main/main.cpp
index 6a0127e3..f5892104 100644
--- a/firmware/main/main.cpp
+++ b/firmware/main/main.cpp
@@ -57,8 +57,8 @@
#include "audio_events.h"
#include "audio_param.h"
-#include "cmd_handling.h"
#include "esp_crud.h"
+#include "esp_midi_uart.h"
#include "esp_persistence.h"
#include "hardware.h"
#include "i2s.h"
@@ -86,6 +86,83 @@ namespace shrapnel {
using Crud = persistence::EspCrud<256>;
+template
+class ParameterObserver final : public parameters::ParameterObserver
+{
+public:
+ explicit ParameterObserver(
+ std::shared_ptr a_persistence)
+ : is_save_throttled{true},
+ persistence{std::move(a_persistence)},
+ timer{"param save throttle",
+ os::ms_to_ticks(10'000),
+ false,
+ etl::delegate::create<
+ ParameterObserver,
+ &ParameterObserver::timer_callback>(*this)}
+ {
+ }
+
+ void
+ notification(std::pair parameter) override
+ {
+ auto &[id, value] = parameter;
+ ESP_LOGI(
+ TAG, "notified about parameter change %s %f", id.data(), value);
+ if(!updated_parameters.available())
+ {
+ ESP_LOGE(TAG, "no space available");
+ return;
+ }
+
+ updated_parameters[id] = value;
+
+ if(!timer.is_active())
+ {
+ if(os::timer_error::TIMER_START_SUCCESS !=
+ timer.start(os::ms_to_ticks(5)))
+ {
+ ESP_LOGE(TAG, "Failed to start parameter observer timer");
+ }
+ }
+ }
+
+ void loop()
+ {
+ if(!is_save_throttled.test_and_set())
+ {
+ persist_parameters();
+ ESP_LOGI(TAG, "Parameters saved to NVS");
+ }
+ }
+
+private:
+ void timer_callback()
+ {
+ is_save_throttled.clear();
+ is_save_throttled.notify_all();
+ }
+
+ void persist_parameters()
+ {
+ for(const auto ¶m : updated_parameters)
+ {
+ auto rc = persistence->save(param.first.data(), param.second);
+ if(rc != 0)
+ {
+ ESP_LOGE(TAG, "failed to save parameters %d", rc);
+ }
+ }
+
+ updated_parameters.clear();
+ }
+
+ std::atomic_flag is_save_throttled;
+ std::shared_ptr persistence;
+ os::Timer timer;
+ etl::map updated_parameters;
+};
+
extern "C" {
static void disconnect_handler(void *arg,
@@ -112,8 +189,8 @@ static void disconnect_handler(void *arg,
ESP_LOGI(TAG, "WiFi disconnected");
auto queue{reinterpret_cast(arg)};
wifi::InternalEvent event{wifi::InternalEvent::DISCONNECT};
- int rc{queue->send(&event, 0)};
- if(rc != pdPASS)
+ auto rc{queue->send(&event, 0)};
+ if(rc != queue_error::SUCCESS)
{
ESP_LOGE(TAG, "Failed to send disconnect event to queue");
}
@@ -131,8 +208,8 @@ static void connect_handler(void *arg,
ESP_LOGI(TAG, "WiFi connected");
auto queue{reinterpret_cast(arg)};
wifi::InternalEvent event{wifi::InternalEvent::CONNECT_SUCCESS};
- int rc{queue->send(&event, 0)};
- if(rc != pdPASS)
+ auto rc{queue->send(&event, 0)};
+ if(rc != queue_error::SUCCESS)
{
ESP_LOGE(TAG, "Failed to send connect event to queue");
}
@@ -149,8 +226,8 @@ static void wifi_start_handler(void *arg,
auto queue{reinterpret_cast(arg)};
wifi::InternalEvent event{wifi::InternalEvent::STARTED};
- int rc{queue->send(&event, 0)};
- if(rc != pdPASS)
+ auto rc{queue->send(&event, 0)};
+ if(rc != queue_error::SUCCESS)
{
ESP_LOGE(TAG, "Failed to send start event to queue");
}
@@ -247,7 +324,63 @@ extern "C" void app_main(void)
ESP_ERROR_CHECK(esp_event_loop_create_default());
auto persistence = std::make_shared();
- auto audio_params = std::make_shared();
+ auto audio_params = std::make_shared>();
+
+ auto create_and_load_parameter = [&](const parameters::id_t &name,
+ float minimum,
+ float maximum,
+ float default_value)
+ {
+ std::optional loaded_value;
+ float value;
+ int rc = persistence->load(name.data(), value);
+ if(rc != 0)
+ {
+ ESP_LOGW(TAG, "Parameter %s failed to load", name.data());
+ goto out;
+ }
+
+ loaded_value = value;
+ out:
+ auto range = maximum - minimum;
+ rc = audio_params->create_and_add_parameter(
+ name,
+ minimum,
+ maximum,
+ loaded_value.has_value() ? *loaded_value * range + minimum
+ : default_value);
+ if(rc != 0)
+ {
+ ESP_LOGE(TAG, "Failed to create parameter %s", name.c_str());
+ }
+ };
+
+ // XXX: These are duplicated in the JUCE plugin, be sure to update both at
+ // the same time
+ create_and_load_parameter("ampGain", 0, 1, 0.5);
+ create_and_load_parameter("ampChannel", 0, 1, 0);
+ create_and_load_parameter("bass", 0, 1, 0.5);
+ create_and_load_parameter("middle", 0, 1, 0.5);
+ create_and_load_parameter("treble", 0, 1, 0.5);
+ //contour gets unstable when set to 0
+ create_and_load_parameter("contour", 0.01, 1, 0.5);
+ create_and_load_parameter("volume", -30, 0, -15);
+
+ create_and_load_parameter("noiseGateThreshold", -80, 0, -60);
+ create_and_load_parameter("noiseGateHysteresis", 0, 5, 0);
+ create_and_load_parameter("noiseGateAttack", 1, 50, 10);
+ create_and_load_parameter("noiseGateHold", 1, 250, 50);
+ create_and_load_parameter("noiseGateRelease", 1, 250, 50);
+ create_and_load_parameter("noiseGateBypass", 0, 1, 0);
+
+ create_and_load_parameter("chorusRate", 0.1, 4, 0.95);
+ create_and_load_parameter("chorusDepth", 0, 1, 0.3);
+ create_and_load_parameter("chorusMix", 0, 1, 0.8);
+ create_and_load_parameter("chorusBypass", 0, 1, 1);
+
+ create_and_load_parameter("wahPosition", 0, 1, 0.5);
+ create_and_load_parameter("wahVocal", 0, 1, 0);
+ create_and_load_parameter("wahBypass", 0, 1, 1);
i2c_setup();
profiling_init(DMA_BUF_SIZE, SAMPLE_RATE);
@@ -322,15 +455,113 @@ extern "C" void app_main(void)
auto wifi_send_event = [&](wifi::InternalEvent event)
{
auto rc = wifi_queue.send(&event, 0);
- if(rc != pdPASS)
+ if(rc != queue_error::SUCCESS)
{
ESP_LOGE(TAG, "Failed to post wifi event to queue");
}
};
+#if 0
+ auto convert_to =
+ [](const AppMessage &message) -> std::pair
+ {
+ return std::visit(
+ overloaded{
+ [](const parameters::ApiMessage &message) {
+ return std::pair{message, std::nullopt};
+ },
+ [](const midi::MappingApiMessage &message) {
+ return std::pair{message, std::nullopt};
+ },
+ [](const events::ApiMessage &message) {
+ return std::pair{message, std::nullopt};
+ },
+ [](const selected_preset::SelectedPresetApiMessage &message) {
+ return std::pair{message, std::nullopt};
+ },
+ [](const presets::PresetsApiMessage &message) {
+ return std::pair{message, std::nullopt};
+ },
+ [](const midi::Message &message) {
+ return std::pair{message, std::nullopt};
+ },
+ [](const ParameterUpdateMessage &message)
+ {
+ return std::visit(
+ overloaded{
+ [](const ParameterUpdateApi &message) {
+ return std::pair{
+ message.update, std::nullopt};
+ },
+ [](const ParameterUpdateHost &message) {
+ return std::pair{
+ message.update, std::nullopt};
+ },
+ [](const ParameterUpdateOther &message) {
+ return std::pair{
+ message.update, std::nullopt};
+ },
+ },
+ message);
+ },
+ },
+ message);
+ };
+#endif
+
auto in_queue = new Queue;
- auto out_queue = new Queue;
- auto server = new Server(in_queue, out_queue);
+ auto out_queue =
+ new Queue>, QUEUE_LEN>;
+
+ auto convert_from =
+ [](const std::pair &message) -> AppMessage
+ {
+ int fd = message.second;
+
+ return std::visit(
+ overloaded{
+ [=](const parameters::ApiMessage &message)
+ {
+ return std::visit(
+ overloaded{
+ [](const parameters::Initialise &message)
+ { return AppMessage{message}; },
+ [=](const parameters::Update &message)
+ {
+ return AppMessage{ParameterUpdateApi{
+ .update{message},
+ .fd{fd},
+ }};
+ },
+ },
+ message);
+ },
+ [](const midi::MappingApiMessage &message)
+ { return AppMessage{message}; },
+ [](const events::ApiMessage &message)
+ { return AppMessage{message}; },
+ [](const selected_preset::SelectedPresetApiMessage &message)
+ { return AppMessage{message}; },
+ [](const presets::PresetsApiMessage &message)
+ { return AppMessage{message}; },
+ [](const midi::Message &message)
+ { return AppMessage{message}; },
+ },
+ message.first);
+ };
+
+ auto server_send_output = [&](const std::pair &in,
+ uint32_t time_to_wait) -> void
+ {
+ auto message = convert_from(in);
+ auto rc = in_queue->send(&message, time_to_wait);
+ if(rc != queue_error::SUCCESS)
+ {
+ ESP_LOGE(TAG, "in_queue message dropped");
+ }
+ };
+
+ auto server = new Server(server_send_output, out_queue);
auto app_send_event = [&](wifi::UserEvent event)
{
@@ -377,26 +608,38 @@ extern "C" void app_main(void)
debug_dump_task_list();
- auto send_message = [&](const AppMessage &message)
- { server->send_message(message); };
+ auto send_message = [&](const ApiMessage &message) {
+ server->send_message({message, std::nullopt});
+ };
+
+ auto send_message2 = [&](const ApiMessage &message,
+ const std::optional fd) {
+ server->send_message({message, fd});
+ };
+
+ auto get_midi_byte = [&]() -> std::optional
+ { return midi_uart->get_byte(0); };
- auto main_thread = MainThread(
- send_message,
- *in_queue,
- midi_uart,
- audio_params,
- persistence,
- std::make_unique("nvs", "midi_mapping"),
- std::make_unique("nvs", "presets"));
+ auto main_thread =
+ MainThread>(
+ send_message,
+ send_message2,
+ *in_queue,
+ audio_params,
+ persistence,
+ std::make_unique("nvs", "midi_mapping"),
+ std::make_unique("nvs", "presets"),
+ get_midi_byte);
auto send_midi_message = [&](const midi::Message &message)
{
- auto app_message = AppMessage{ApiMessage{message}, std::nullopt};
- int rc = in_queue->send(&app_message, portMAX_DELAY);
+ auto app_message = AppMessage{message};
+ auto rc = in_queue->send(&app_message, portMAX_DELAY);
- if(rc != pdPASS)
+ if(rc != queue_error::SUCCESS)
{
- ESP_LOGE(TAG, "Failed to send to main queue %d", rc);
+ ESP_LOGE(
+ TAG, "Failed to send to main queue %d", static_cast(rc));
}
};
@@ -437,6 +680,9 @@ extern "C" void app_main(void)
audio::i2s_setup(PROFILING_GPIO, audio_params.get());
+ ParameterObserver<20> parameter_observer{persistence};
+ audio_params->add_observer(parameter_observer);
+
ESP_LOGI(TAG, "setup done");
ESP_LOGI(TAG, "stack: %d", uxTaskGetStackHighWaterMark(NULL));
heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);
@@ -448,6 +694,8 @@ extern "C" void app_main(void)
main_thread.loop();
+ parameter_observer.loop();
+
{
// i2s produces an event on each buffer TX/RX. We process all the
// events in the current iteration, so that the queue doesn't fill
@@ -460,7 +708,7 @@ extern "C" void app_main(void)
}
wifi::InternalEvent wifi_event;
- while(pdPASS == wifi_queue.receive(&wifi_event, 0))
+ while(queue_error::SUCCESS == wifi_queue.receive(&wifi_event, 0))
{
wifi::State state{wifi_state_chart.get_state_id()};
ESP_LOGI(
diff --git a/frontend/test/firmware_api_test/util.dart b/frontend/test/firmware_api_test/util.dart
index 7f8c7833..e414b016 100644
--- a/frontend/test/firmware_api_test/util.dart
+++ b/frontend/test/firmware_api_test/util.dart
@@ -332,7 +332,16 @@ class ShrapnelUart {
.cast>()
.transform(utf8.decoder)
.transform(const LineSplitter())
- .asBroadcastStream();
+ // Sometimes a log line fails to decode, possibly due to UART data
+ // corruption or because the firmware prints random binary data. Prevent
+ // it from causing a failing test.
+ .handleError((Object error, StackTrace stackTrace) {
+ _logger.severe(
+ 'Failed to decode logs, possible data corruption',
+ error,
+ stackTrace,
+ );
+ }, test: (error) => error is FormatException).asBroadcastStream();
Stream get log => _log;
diff --git a/plugins/shrapnel/.idea/.gitignore b/plugins/shrapnel/.idea/.gitignore
deleted file mode 100644
index 13566b81..00000000
--- a/plugins/shrapnel/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/plugins/shrapnel/.idea/.name b/plugins/shrapnel/.idea/.name
deleted file mode 100644
index 0f71c1c8..00000000
--- a/plugins/shrapnel/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-ShrapnelDSP
\ No newline at end of file
diff --git a/plugins/shrapnel/.idea/cmake.xml b/plugins/shrapnel/.idea/cmake.xml
deleted file mode 100644
index 2ad2554e..00000000
--- a/plugins/shrapnel/.idea/cmake.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/plugins/shrapnel/.idea/codeStyles/codeStyleConfig.xml b/plugins/shrapnel/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index a55e7a17..00000000
--- a/plugins/shrapnel/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/plugins/shrapnel/.idea/discord.xml b/plugins/shrapnel/.idea/discord.xml
deleted file mode 100644
index 30bab2ab..00000000
--- a/plugins/shrapnel/.idea/discord.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/plugins/shrapnel/.idea/misc.xml b/plugins/shrapnel/.idea/misc.xml
deleted file mode 100644
index 392db40a..00000000
--- a/plugins/shrapnel/.idea/misc.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/plugins/shrapnel/.idea/modules.xml b/plugins/shrapnel/.idea/modules.xml
deleted file mode 100644
index 3561054b..00000000
--- a/plugins/shrapnel/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/plugins/shrapnel/.idea/shrapnel.iml b/plugins/shrapnel/.idea/shrapnel.iml
deleted file mode 100644
index f08604bb..00000000
--- a/plugins/shrapnel/.idea/shrapnel.iml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/shrapnel/.idea/vcs.xml b/plugins/shrapnel/.idea/vcs.xml
deleted file mode 100644
index b2bdec2d..00000000
--- a/plugins/shrapnel/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/plugins/shrapnel/CMakeLists.txt b/plugins/shrapnel/CMakeLists.txt
index 874ed480..687c04bf 100644
--- a/plugins/shrapnel/CMakeLists.txt
+++ b/plugins/shrapnel/CMakeLists.txt
@@ -7,20 +7,20 @@ set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
-add_subdirectory(../../effects/abstract_dsp abstract_dsp)
-add_subdirectory(../../effects/chorus chorus)
-add_subdirectory(../../effects/gate gate)
-add_subdirectory(../../effects/valvestate valvestate)
-add_subdirectory(../../effects/wah wah)
-add_subdirectory(../../firmware/components/audio audio)
-add_subdirectory(../../firmware/components/compiler_warning_flags compiler_warning_flags)
+add_subdirectory(../../effects effects)
+add_subdirectory(../../shrapnel shrapnel)
+add_subdirectory(../../thirdparty/JUCE JUCE)
+add_subdirectory(../../thirdparty/uWebSockets uWebSockets)
+add_subdirectory(../JUCE_static JUCE_static)
+add_subdirectory(../../thirdparty/esp-idf-components/nanopb nanopb)
+add_subdirectory(../../thirdparty/esp-idf-components/etl etl)
+
+# Fake esp-idf components are required for esp-dsp to build
add_subdirectory(../../test/support/esp_common esp_common)
add_subdirectory(../../test/support/esp_hw_support esp_hw_support)
add_subdirectory(../../test/support/log log)
-add_subdirectory(../../thirdparty/JUCE JUCE)
add_subdirectory(../../thirdparty/esp-idf-components/esp-dsp esp-dsp)
add_subdirectory(../../thirdparty/esp-idf-components/esp32-fft esp32-fft)
-add_subdirectory(../JUCE_static JUCE_static)
juce_add_plugin(ShrapnelDSP
COMPANY_NAME Barabas Raffai
@@ -31,9 +31,12 @@ juce_add_plugin(ShrapnelDSP
target_sources(ShrapnelDSP
PRIVATE
- PluginProcessor.cpp)
+ PluginProcessor.cpp
+ server.cpp)
target_link_libraries(ShrapnelDSP
PRIVATE
- audio
+ shrapnel::audio
+ shrapnel::main_thread
+ shrapnel::uwebsockets
juce_modules)
diff --git a/plugins/shrapnel/PluginProcessor.cpp b/plugins/shrapnel/PluginProcessor.cpp
index 62ce8d5a..312f7aaf 100644
--- a/plugins/shrapnel/PluginProcessor.cpp
+++ b/plugins/shrapnel/PluginProcessor.cpp
@@ -81,6 +81,9 @@ AudioPluginAudioProcessor::AudioPluginAudioProcessor()
"wahBypass", "Wah Bypass", 0.f, 1.f, 1.f),
},
},
+ parameter_adapter{std::make_shared>>(parameters)},
+ main_thread{parameter_adapter},
processor{
// clang-format off
ShrapnelAudioProcessor{
@@ -118,6 +121,8 @@ AudioPluginAudioProcessor::AudioPluginAudioProcessor()
// clang-format on
}
{
+ bool success = main_thread.startThread();
+ jassert(success);
}
AudioPluginAudioProcessor::~AudioPluginAudioProcessor() = default;
@@ -203,7 +208,7 @@ bool AudioPluginAudioProcessor::isBusesLayoutSupported(
if(layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
- // This checks if the input layout matches the output layout
+ // This checks if the input layout matches the output layout
#if !JucePlugin_IsSynth
if(layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
return false;
diff --git a/plugins/shrapnel/PluginProcessor.h b/plugins/shrapnel/PluginProcessor.h
index edf364ae..612c171c 100644
--- a/plugins/shrapnel/PluginProcessor.h
+++ b/plugins/shrapnel/PluginProcessor.h
@@ -19,6 +19,7 @@
#pragma once
+#include "plugin_main_thread.h"
#include "shrapnel_processor.h"
#include
@@ -57,8 +58,15 @@ class AudioPluginAudioProcessor : public juce::AudioProcessor
private:
juce::AudioProcessorValueTreeState parameters;
+ std::shared_ptr>>
+ parameter_adapter;
+ shrapnel::PluginMainThread>
+ main_thread;
+
shrapnel::dsp::BlockProcessor
processor;
+
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessor)
};
diff --git a/plugins/shrapnel/parameter_adapter.h b/plugins/shrapnel/parameter_adapter.h
new file mode 100644
index 00000000..7d9079be
--- /dev/null
+++ b/plugins/shrapnel/parameter_adapter.h
@@ -0,0 +1,83 @@
+#include "main_thread.h"
+#include
+
+/** Adapts a JUCE AudioProcessorValueTreeState to work with shrapnel main thread
+ *
+ */
+
+namespace shrapnel {
+
+template
+ requires UpdatableParameter &&
+ GettableParameter
+class ParameterAdapter : private juce::AudioProcessorValueTreeState::Listener
+{
+ static constexpr char TAG[] = "ParameterAdapter";
+
+public:
+ explicit ParameterAdapter(juce::AudioProcessorValueTreeState &a_parameters)
+ : apvts{a_parameters}
+ {
+ apvts.addParameterListener("ampGain", this);
+ apvts.addParameterListener("ampChannel", this);
+ apvts.addParameterListener("bass", this);
+ apvts.addParameterListener("middle", this);
+ apvts.addParameterListener("treble", this);
+ apvts.addParameterListener("contour", this);
+ apvts.addParameterListener("volume", this);
+ apvts.addParameterListener("noiseGateThreshold", this);
+ apvts.addParameterListener("noiseGateHysteresis", this);
+ apvts.addParameterListener("noiseGateAttack", this);
+ apvts.addParameterListener("noiseGateHold", this);
+ apvts.addParameterListener("noiseGateRelease", this);
+ apvts.addParameterListener("noiseGateBypass", this);
+ apvts.addParameterListener("chorusRate", this);
+ apvts.addParameterListener("chorusDepth", this);
+ apvts.addParameterListener("chorusMix", this);
+ apvts.addParameterListener("chorusBypass", this);
+ apvts.addParameterListener("wahPosition", this);
+ apvts.addParameterListener("wahVocal", this);
+ apvts.addParameterListener("wahBypass", this);
+ }
+
+ int update(const parameters::id_t &id, float value)
+ {
+ ESP_LOGD(TAG, "update %s %f", id.c_str(), value);
+
+ // TODO:
+ // Note that to make sure the host correctly handles automation, you should call
+ // the beginChangeGesture() and endChangeGesture() methods to tell the host when
+ // the user has started and stopped changing the parameter.
+ //
+ // This is not implemented in the GUI at the moment
+ auto parameter = apvts.getParameter(id.data());
+ if(parameter == nullptr)
+ {
+ return -1;
+ }
+
+ // It should be safe to call this from a background thread:
+ // https://forum.juce.com/t/calling-setvaluenotifyinghost-from-processblock/26073/9
+
+ // TODO this may expect a value between 0 and 1 like getValue returns
+ parameter->setValueNotifyingHost(parameter->convertFrom0to1(value));
+ return 0;
+ }
+
+private:
+ void parameterChanged(const juce::String &id, float value) override
+ {
+ // FIXME: what thread is calling this? Probably need to send a message
+ // and handle on the main thread.
+ auto id_str = id.toStdString();
+ ESP_LOGD(TAG, "parameterChanged %s %f", id_str.c_str(), value);
+
+ on_parameter_updated_by_host(id_str.c_str(), value);
+ }
+
+ juce::AudioProcessorValueTreeState &apvts;
+ etl::delegate
+ on_parameter_updated_by_host;
+};
+
+} // namespace shrapnel
diff --git a/plugins/shrapnel/plugin_main_thread.h b/plugins/shrapnel/plugin_main_thread.h
new file mode 100644
index 00000000..1dd88aaf
--- /dev/null
+++ b/plugins/shrapnel/plugin_main_thread.h
@@ -0,0 +1,408 @@
+/*
+ * Copyright 2022 Barabas Raffai
+ *
+ * This file is part of ShrapnelDSP.
+ *
+ * ShrapnelDSP is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * ShrapnelDSP is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ShrapnelDSP. If not, see .
+ */
+
+#pragma once
+
+#include "main_thread.h"
+#include "parameter_adapter.h"
+#include
+
+#include "juce_core/juce_core.h"
+#include "server.h"
+#include
+
+// TODO:
+// server
+
+// TODO does it really make sense to save the parameters as a VST plugin? That
+// is the host's responsibility.
+//
+// We can keep saving presets, and that requires reading from parameters, but
+// maybe the parameter storage code from the firmware should be removed from the
+// main thread, and instead a wrapper on AudioProcessorValueTreeState should be
+// injected.
+
+namespace shrapnel {
+
+// TODO this is probably unsound. The juce::Timer internal to the PropertiesFile
+// will run the callback on some random thread. It locks the PropertiesFile
+// lock, but setValue etc. are inherited from the PropertySet and do not lock.
+// We need to lock it ourselves.
+class JuceCrud final : public shrapnel::persistence::Crud>
+{
+public:
+ explicit JuceCrud(const std::string &name) : options{}
+ {
+ options.applicationName = name;
+ options.filenameSuffix = "properties";
+ options.folderName = "ShrapnelDSP";
+ options.osxLibrarySubFolder = "Application Support";
+ options.commonToAllUsers = false;
+ options.ignoreCaseOfKeyNames = false;
+ options.doNotSave = false;
+ options.millisecondsBeforeSaving = 1000;
+ options.storageFormat = juce::PropertiesFile::storeAsXML;
+ options.processLock = nullptr;
+
+ propertiesFile = std::make_unique(options);
+ };
+
+ int create(const std::span &data, uint32_t &id_out) override
+ {
+ // start at 0 in case last ID has not been saved to disk
+ auto id =
+ static_cast(propertiesFile->getIntValue(last_id_key, 0));
+ id++;
+ id_out = id;
+
+ auto data_base64 = juce::Base64::toBase64(data.data(), data.size());
+ propertiesFile->setValue(id_to_key(id), juce::var(data_base64));
+ propertiesFile->setValue(last_id_key, juce::var(static_cast(id)));
+
+ return 0;
+ }
+
+ int read(uint32_t id, std::span &data_out) override
+ {
+ return _read(id_to_key(id), data_out);
+ }
+
+ int update(uint32_t id, const std::span &data) override
+ {
+ auto data_base64 = juce::Base64::toBase64(data.data(), data.size());
+ propertiesFile->setValue(id_to_key(id), juce::var(data_base64));
+
+ return 0;
+ }
+
+ int destroy(uint32_t id) override
+ {
+ propertiesFile->removeValue(id_to_key(id));
+
+ return 0;
+ }
+
+ void for_each(etl::delegate &)>
+ callback) override
+ {
+ auto all_keys = propertiesFile->getAllProperties().getAllKeys();
+
+ for(const auto &key : all_keys)
+ {
+ std::array memory;
+ std::span buffer{memory};
+
+ auto rc = _read(key, buffer);
+ if(rc == 0)
+ {
+ auto id = key_to_id(key);
+ callback(id, buffer);
+ }
+ }
+ }
+
+private:
+ static juce::String id_to_key(uint32_t id)
+ {
+ char hex[9];
+ int rc = snprintf(hex, sizeof hex, "%08" PRIx32, id);
+ assert(rc == 8);
+ return {hex, 9};
+ };
+
+ static uint32_t key_to_id(const juce::String &key)
+ {
+ id_t id;
+ int rc = sscanf(key.getCharPointer(), "%08" PRIx32, &id);
+ assert(rc == 1);
+ return id;
+ };
+
+ int _read(juce::StringRef id, std::span &data_out)
+ {
+ auto value = propertiesFile->getValue(id);
+ if(value.isEmpty())
+ return -1;
+
+ juce::MemoryOutputStream stream;
+
+ auto success = juce::Base64::convertFromBase64(stream, value);
+ if(!success)
+ {
+ return -1;
+ }
+
+ if(stream.getDataSize() > data_out.size())
+ {
+ return -1;
+ }
+
+ std::copy(stream.getMemoryBlock().begin(),
+ stream.getMemoryBlock().end(),
+ data_out.begin());
+ data_out = data_out.subspan(stream.getDataSize());
+
+ return 0;
+ }
+
+ static constexpr char last_id_key[] = "last_id";
+
+ juce::PropertiesFile::Options options;
+ std::unique_ptr propertiesFile;
+};
+
+class JuceStorage final : public shrapnel::persistence::Storage
+{
+public:
+ explicit JuceStorage() : options{}
+ {
+ options.applicationName = "storage";
+ options.filenameSuffix = "properties";
+ options.folderName = "ShrapnelDSP";
+ options.osxLibrarySubFolder = "Application Support";
+ options.commonToAllUsers = false;
+ options.ignoreCaseOfKeyNames = false;
+ options.doNotSave = false;
+ options.millisecondsBeforeSaving = 1000;
+ options.storageFormat = juce::PropertiesFile::storeAsXML;
+ options.processLock = nullptr;
+
+ propertiesFile = std::make_unique(options);
+ };
+
+ int save(const char *key, std::span data) override
+ {
+ auto data_base64 = juce::Base64::toBase64(data.data(), data.size());
+ propertiesFile->setValue(key, data_base64);
+ return 0;
+ }
+
+ int save(const char *key, etl::string_view data) override
+ {
+ propertiesFile->setValue(key, data.data());
+ return 0;
+ }
+
+ int save(const char *key, uint32_t data) override
+ {
+ propertiesFile->setValue(key, static_cast(data));
+ return 0;
+ }
+
+ int save(const char *key, float data) override
+ {
+ propertiesFile->setValue(key, data);
+ return 0;
+ }
+
+ int load(const char *key, std::span &data) override
+ {
+ if(!propertiesFile->containsKey(key))
+ {
+ return -1;
+ }
+
+ auto value = propertiesFile->getValue(key);
+
+ juce::MemoryOutputStream stream;
+
+ auto success = juce::Base64::convertFromBase64(stream, value);
+ if(!success)
+ {
+ return -1;
+ }
+
+ if(stream.getDataSize() > data.size())
+ {
+ return -1;
+ }
+
+ std::copy(stream.getMemoryBlock().begin(),
+ stream.getMemoryBlock().end(),
+ data.begin());
+ data = data.subspan(stream.getDataSize());
+ return 0;
+ }
+
+ int load(const char *key, etl::istring &data) override
+ {
+ if(!propertiesFile->containsKey(key))
+ {
+ return -1;
+ }
+
+ auto value = propertiesFile->getValue(key);
+ auto required_size = value.getNumBytesAsUTF8();
+
+ data.initialize_free_space();
+ data.uninitialized_resize(required_size);
+
+ if(data.size() != required_size)
+ {
+ return -1;
+ }
+
+ value.copyToUTF8(data.data(), data.max_size());
+ return 0;
+ }
+
+ int load(const char *key, uint32_t &data) override
+ {
+ if(!propertiesFile->containsKey(key))
+ {
+ return -1;
+ }
+
+ data = static_cast(propertiesFile->getIntValue(key));
+ return 0;
+ }
+
+ int load(const char *key, float &data) override
+ {
+ if(!propertiesFile->containsKey(key))
+ {
+ return -1;
+ }
+
+ data = static_cast(propertiesFile->getDoubleValue(key));
+ return 0;
+ }
+
+private:
+ juce::PropertiesFile::Options options;
+ std::unique_ptr propertiesFile;
+};
+
+template
+class PluginMainThread final : public juce::Thread
+{
+public:
+ explicit PluginMainThread(
+ std::shared_ptr> parameters)
+ : juce::Thread{"shrapnel"},
+ server{etl::delegate &in,
+ uint32_t time_to_wait)>::
+ create(*this)},
+ main_thread{
+ [&](const ApiMessage &message) {
+ server.send_message({message, std::nullopt});
+ },
+ [&](const ApiMessage &message, const std::optional fd) {
+ server.send_message({message, fd});
+ },
+ in_queue,
+ std::move(parameters),
+ std::make_shared(),
+ std::make_unique("midi_mapping"),
+ std::make_unique("presets"),
+ []() -> std::optional
+ {
+ // FIXME: connect to JUCE MIDI stream
+ return std::nullopt;
+ }}
+ {
+ server.start();
+ }
+
+ void run() override
+ {
+ juce::Logger::writeToLog("Main thread started");
+
+ while(!threadShouldExit())
+ {
+ wait(10);
+
+ main_thread.loop();
+ }
+ };
+
+private:
+ // A queue filled by the server with received API messages
+ shrapnel::Queue in_queue;
+ Server server;
+ std::shared_ptr audio_params;
+ shrapnel::MainThread<4, AudioParametersT> main_thread;
+
+ static AppMessage convert_from(const std::pair &message)
+ {
+ int fd = message.second;
+
+ return std::visit(
+ overloaded{
+ [=](const parameters::ApiMessage &message)
+ {
+ return std::visit(
+ overloaded{
+ [](const parameters::Initialise &message)
+ { return AppMessage{message}; },
+ [=](const parameters::Update &message)
+ {
+ return AppMessage{ParameterUpdateApi{
+ .update{message},
+ .fd{fd},
+ }};
+ },
+ },
+ message);
+ },
+ [](const midi::MappingApiMessage &message)
+ { return AppMessage{message}; },
+ [](const events::ApiMessage &message)
+ { return AppMessage{message}; },
+ [](const selected_preset::SelectedPresetApiMessage &message)
+ { return AppMessage{message}; },
+ [](const presets::PresetsApiMessage &message)
+ { return AppMessage{message}; },
+ [](const midi::Message &message)
+ { return AppMessage{message}; },
+ },
+ message.first);
+ };
+
+ void server_send_output(const std::pair &in,
+ uint32_t time_to_wait)
+ {
+ auto message = convert_from(in);
+ auto rc = in_queue.send(&message, time_to_wait);
+ if(rc != queue_error::SUCCESS)
+ {
+ ESP_LOGE(TAG, "in_queue message dropped");
+ }
+ };
+
+ void parameters_send_host_update(const char *id, float value)
+ {
+ auto message = AppMessage{
+ ParameterUpdateHost{
+ .update{
+ .id{id},
+ .value{value},
+ },
+ },
+ };
+ auto rc = in_queue.send(&message, 100);
+ if(rc != queue_error::SUCCESS)
+ {
+ ESP_LOGE(TAG, "in_queue message dropped");
+ }
+ };
+};
+
+} // namespace shrapnel
diff --git a/plugins/shrapnel/server.cpp b/plugins/shrapnel/server.cpp
new file mode 100644
index 00000000..8575e493
--- /dev/null
+++ b/plugins/shrapnel/server.cpp
@@ -0,0 +1,247 @@
+#include "server.h"
+#include "App.h"
+#include
+
+#define TAG "server"
+
+namespace shrapnel {
+
+static void debug_print_sent_message(const ApiMessage &message);
+static void debug_print_received_message(const ApiMessage &message);
+static void send_websocket_message(
+ const std::pair> &message);
+
+uWS::App *globalApp;
+uWS::Loop *globalLoop;
+
+struct UserData
+{
+};
+
+Server::Server(etl::delegate &in,
+ uint32_t time_to_wait)> a_output_message)
+ : Thread("server"),
+ output_message{a_output_message}
+{
+}
+
+void Server::start() { startThread(); }
+
+void Server::send_message(
+ const std::pair> &message)
+{
+ if(!message.second.has_value())
+ {
+ ESP_LOGD(TAG, "%s source fd is null", __FUNCTION__);
+ }
+ else
+ {
+ ESP_LOGD(TAG, "%s source fd = %d", __FUNCTION__, *message.second);
+ }
+
+ debug_print_sent_message(message.first);
+
+ send_websocket_message(message);
+}
+
+void send_websocket_message(
+ const std::pair> &message)
+{
+ std::array memory{};
+ auto buffer = std::span{memory};
+
+ auto encoded = api::to_bytes(message.first, buffer);
+ if(!encoded.has_value())
+ {
+ ESP_LOGE(TAG, "Failed to encode message");
+ return;
+ }
+
+ ESP_LOGD(TAG, "%s len = %zd", __FUNCTION__, encoded->size());
+#if 0
+ // TODO
+ ESP_LOG_BUFFER_HEXDUMP(
+ TAG, encoded->data(), encoded->size(), ESP_LOG_VERBOSE);
+#endif
+
+ // TODO do not send it to any clients where the fd matches the passed in fd,
+ // or remove that feature. Maybe put a client ID into the message itself, so
+ // the frontend can ignore it.
+
+ // Run the publish in the server thread
+ globalLoop->defer(
+ [=]()
+ {
+ globalApp->publish(
+ "broadcast",
+ std::string_view{(char *)encoded->data(), encoded->size()},
+ uWS::OpCode::BINARY);
+ });
+}
+
+void Server::run()
+{
+ // TODO monitor threadShouldExit and stop when required
+
+ auto app =
+ uWS::App()
+ .ws(
+ "/websocket",
+ {
+ .compression = uWS::CompressOptions::DISABLED,
+ .maxPayloadLength = 100 * 1024 * 1024,
+ .idleTimeout = 16,
+ .maxBackpressure = 100 * 1024 * 1024,
+ .closeOnBackpressureLimit = false,
+ .resetIdleTimeoutOnSend = false,
+ .sendPingsAutomatically = true,
+ .upgrade =
+ [](auto *res, auto *req, auto *webSocketContext)
+ {
+ /* Default handler copied from library */
+ std::cout << "upgrade" << std::endl;
+
+ std::string_view secWebSocketKey =
+ req->getHeader("sec-websocket-key");
+
+ std::string_view secWebSocketProtocol =
+ req->getHeader("sec-websocket-protocol");
+ std::string_view secWebSocketExtensions =
+ req->getHeader("sec-websocket-extensions");
+
+ /* Safari 15 hack */
+ if(uWS::hasBrokenCompression(
+ req->getHeader("user-agent")))
+ {
+ secWebSocketExtensions = "";
+ }
+
+ res->template upgrade({},
+ secWebSocketKey,
+ secWebSocketProtocol,
+ secWebSocketExtensions,
+ webSocketContext);
+ },
+
+ .open =
+ [](auto * /*ws*/)
+ {
+ /* Open event here, you may access ws->getUserData() which points to a PerSocketData struct */
+ std::cout << "open" << std::endl;
+ },
+ .message =
+ [this](uWS::WebSocket *ws,
+ std::string_view message,
+ uWS::OpCode opCode)
+ {
+ std::cout << "message" << std::endl;
+
+ // decode
+ {
+ /* We should never see any of these packets */
+ assert(opCode != uWS::OpCode::CONTINUATION);
+ assert(opCode != uWS::OpCode::TEXT);
+ assert(opCode != uWS::OpCode::CLOSE);
+ assert(opCode != uWS::OpCode::PING);
+ assert(opCode != uWS::OpCode::PONG);
+
+ auto fd = static_cast(
+ reinterpret_cast(
+ ws->getNativeHandle()));
+
+ ESP_LOGD(TAG,
+ "%s len = %zd",
+ __FUNCTION__,
+ message.size());
+#if 0
+ // TODO implement hexdump
+ ESP_LOG_BUFFER_HEXDUMP(TAG,
+ message.data(),
+ message.size(),
+ ESP_LOG_VERBOSE);
+#endif
+
+ auto decoded = api::from_bytes(
+ {reinterpret_cast(
+ message.data()),
+ message.size()});
+ if(decoded.has_value())
+ {
+ debug_print_received_message(*decoded);
+ auto out =
+ std::pair{*decoded, fd};
+ output_message(out, 100);
+ }
+ else
+ {
+ ESP_LOGE(TAG,
+ "failed to parse received message");
+#if 0
+ // TODO implement hexdump
+ ESP_LOG_BUFFER_HEXDUMP(TAG,
+ message.data(),
+ message.size(),
+ ESP_LOG_ERROR);
+#endif
+ }
+ }
+
+ // print
+ },
+ .dropped =
+ [](auto * /*ws*/,
+ std::string_view /*message*/,
+ uWS::OpCode /*opCode*/)
+ {
+ /* A message was dropped due to set maxBackpressure and closeOnBackpressureLimit limit */
+ std::cout << "dropped" << std::endl;
+ },
+ .drain =
+ [](auto * /*ws*/)
+ {
+ /* Check ws->getBufferedAmount() here */
+ std::cout << "drain" << std::endl;
+ },
+ .close =
+ [](auto * /*ws*/,
+ int /*code*/,
+ std::string_view /*message*/)
+ {
+ /* You may access ws->getUserData() here */
+ std::cout << "close" << std::endl;
+ },
+ })
+ .listen(3000,
+ [](auto *listen_socket)
+ {
+ if(listen_socket)
+ {
+ std::cout << "Listening on port " << 3000
+ << std::endl;
+ }
+ });
+
+ globalApp = &app;
+ globalLoop = uWS::Loop::get();
+ app.run();
+
+ std::cout << "Failed to listen on port 3000" << std::endl;
+}
+
+static void debug_print_sent_message(const ApiMessage &message)
+{
+ etl::string<128> debug;
+ etl::string_stream debug_stream{debug};
+ debug_stream << message;
+ ESP_LOGD(TAG, "sending message: %s", debug.data());
+}
+
+static void debug_print_received_message(const ApiMessage &message)
+{
+ etl::string<128> debug;
+ etl::string_stream debug_stream{debug};
+ debug_stream << message;
+ ESP_LOGD(TAG, "received message: %s", debug.data());
+}
+
+} // namespace shrapnel
diff --git a/plugins/shrapnel/server.h b/plugins/shrapnel/server.h
new file mode 100644
index 00000000..75e4cad1
--- /dev/null
+++ b/plugins/shrapnel/server.h
@@ -0,0 +1,25 @@
+#include "messages.h"
+#include "os/queue.h"
+#include
+
+namespace shrapnel {
+
+class Server final : private juce::Thread
+{
+public:
+ Server(etl::delegate &in,
+ uint32_t time_to_wait)> output_message);
+
+ void start();
+
+ void send_message(const std::pair> &message);
+
+private:
+ void run() override;
+
+ etl::delegate &in,
+ uint32_t time_to_wait)>
+ output_message;
+};
+
+} // namespace shrapnel
diff --git a/shrapnel/CMakeLists.txt b/shrapnel/CMakeLists.txt
new file mode 100644
index 00000000..c4f741b3
--- /dev/null
+++ b/shrapnel/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_subdirectory(api)
+add_subdirectory(audio)
+add_subdirectory(audio_events)
+add_subdirectory(audio_param)
+add_subdirectory(cmd_handling)
+add_subdirectory(compiler_warning_flags)
+add_subdirectory(float_convert)
+add_subdirectory(messages)
+add_subdirectory(main_thread)
+add_subdirectory(midi_handling)
+add_subdirectory(midi_mapping)
+add_subdirectory(midi_protocol)
+add_subdirectory(os)
+add_subdirectory(persistence)
+add_subdirectory(presets)
diff --git a/firmware/components/api/CMakeLists.txt b/shrapnel/api/CMakeLists.txt
similarity index 100%
rename from firmware/components/api/CMakeLists.txt
rename to shrapnel/api/CMakeLists.txt
diff --git a/firmware/components/api/include/api.h b/shrapnel/api/include/api.h
similarity index 100%
rename from firmware/components/api/include/api.h
rename to shrapnel/api/include/api.h
diff --git a/firmware/components/audio/CMakeLists.txt b/shrapnel/audio/CMakeLists.txt
similarity index 100%
rename from firmware/components/audio/CMakeLists.txt
rename to shrapnel/audio/CMakeLists.txt
diff --git a/firmware/components/audio/include/audio_processor.h b/shrapnel/audio/include/audio_processor.h
similarity index 100%
rename from firmware/components/audio/include/audio_processor.h
rename to shrapnel/audio/include/audio_processor.h
diff --git a/firmware/components/audio/src/audio_processor.cpp b/shrapnel/audio/src/audio_processor.cpp
similarity index 100%
rename from firmware/components/audio/src/audio_processor.cpp
rename to shrapnel/audio/src/audio_processor.cpp
diff --git a/firmware/components/audio/src/speaker_coeffs.h b/shrapnel/audio/src/speaker_coeffs.h
similarity index 100%
rename from firmware/components/audio/src/speaker_coeffs.h
rename to shrapnel/audio/src/speaker_coeffs.h
diff --git a/firmware/components/audio_events/CMakeLists.txt b/shrapnel/audio_events/CMakeLists.txt
similarity index 85%
rename from firmware/components/audio_events/CMakeLists.txt
rename to shrapnel/audio_events/CMakeLists.txt
index d3690cdd..d9395327 100644
--- a/firmware/components/audio_events/CMakeLists.txt
+++ b/shrapnel/audio_events/CMakeLists.txt
@@ -1,11 +1,11 @@
-include(../../../thirdparty/esp-idf-components/nanopb/nanopb-generate-cpp.cmake)
+include(../../thirdparty/esp-idf-components/nanopb/nanopb-generate-cpp.cmake)
add_library(audio_events STATIC)
add_library(shrapnel::audio_events ALIAS audio_events)
set(NANOPB_GENERATE_CPP_APPEND_PATH ON)
nanopb_generate_cpp(PROTO_SRCS PROTO_HDRS
- ../../../proto/audio_events.proto)
+ ../../proto/audio_events.proto)
target_include_directories(audio_events
PUBLIC
diff --git a/firmware/components/audio_events/include/audio_events.h b/shrapnel/audio_events/include/audio_events.h
similarity index 100%
rename from firmware/components/audio_events/include/audio_events.h
rename to shrapnel/audio_events/include/audio_events.h
diff --git a/firmware/components/audio_events/src/audio_events.cpp b/shrapnel/audio_events/src/audio_events.cpp
similarity index 100%
rename from firmware/components/audio_events/src/audio_events.cpp
rename to shrapnel/audio_events/src/audio_events.cpp
diff --git a/firmware/components/audio_param/CMakeLists.txt b/shrapnel/audio_param/CMakeLists.txt
similarity index 100%
rename from firmware/components/audio_param/CMakeLists.txt
rename to shrapnel/audio_param/CMakeLists.txt
diff --git a/firmware/components/audio_param/include/audio_param.h b/shrapnel/audio_param/include/audio_param.h
similarity index 98%
rename from firmware/components/audio_param/include/audio_param.h
rename to shrapnel/audio_param/include/audio_param.h
index e63b7df1..f1134959 100644
--- a/firmware/components/audio_param/include/audio_param.h
+++ b/shrapnel/audio_param/include/audio_param.h
@@ -57,6 +57,7 @@ class AudioParameterFloat
id_t name;
private:
+ static constexpr char TAG[] = "AudioParameterFloat";
std::atomic value;
float minimum;
float maximum;
diff --git a/firmware/components/audio_param/src/audio_param.cpp b/shrapnel/audio_param/src/audio_param.cpp
similarity index 89%
rename from firmware/components/audio_param/src/audio_param.cpp
rename to shrapnel/audio_param/src/audio_param.cpp
index 805a149b..dbe43181 100644
--- a/firmware/components/audio_param/src/audio_param.cpp
+++ b/shrapnel/audio_param/src/audio_param.cpp
@@ -36,6 +36,8 @@ AudioParameterFloat::AudioParameterFloat(const id_t &a_name,
void AudioParameterFloat::update(float a_value)
{
+ ESP_LOGD(TAG, "update %s %f", name.c_str(), a_value);
+
if(a_value > 1)
{
return;
@@ -53,7 +55,9 @@ void AudioParameterFloat::update(float a_value)
float AudioParameterFloat::get(void)
{
auto range = maximum - minimum;
- return (value - minimum) / range;
+ const float out_value = (value - minimum) / range;
+ ESP_LOGD(TAG, "get %s %f", name.c_str(), out_value);
+ return out_value;
}
std::atomic *AudioParameterFloat::get_raw_parameter(void)
diff --git a/firmware/components/audio_param/test/test_audio_param.cpp b/shrapnel/audio_param/test/test_audio_param.cpp
similarity index 100%
rename from firmware/components/audio_param/test/test_audio_param.cpp
rename to shrapnel/audio_param/test/test_audio_param.cpp
diff --git a/firmware/components/cmd_handling/CMakeLists.txt b/shrapnel/cmd_handling/CMakeLists.txt
similarity index 84%
rename from firmware/components/cmd_handling/CMakeLists.txt
rename to shrapnel/cmd_handling/CMakeLists.txt
index e14a57e9..4d29ef43 100644
--- a/firmware/components/cmd_handling/CMakeLists.txt
+++ b/shrapnel/cmd_handling/CMakeLists.txt
@@ -1,16 +1,15 @@
-include(../../../thirdparty/esp-idf-components/nanopb/nanopb-generate-cpp.cmake)
+include(../../thirdparty/esp-idf-components/nanopb/nanopb-generate-cpp.cmake)
add_library(cmd_handling STATIC)
add_library(shrapnel::cmd_handling ALIAS cmd_handling)
set(NANOPB_GENERATE_CPP_APPEND_PATH ON)
nanopb_generate_cpp(PROTO_SRCS PROTO_HDRS
- ../../../proto/cmd_handling.proto)
+ ../../proto/cmd_handling.proto)
target_sources(cmd_handling
PRIVATE
${PROTO_SRCS}
- src/cmd_handling.cpp
src/cmd_handling_api.cpp)
target_include_directories(cmd_handling
@@ -21,9 +20,7 @@ target_include_directories(cmd_handling
target_link_libraries(cmd_handling
PUBLIC
- idf::esp_common
idf::log
- idf::freertos
shrapnel::api
shrapnel::audio_param
shrapnel::etl
diff --git a/firmware/components/cmd_handling/include/cmd_handling_api.h b/shrapnel/cmd_handling/include/cmd_handling_api.h
similarity index 100%
rename from firmware/components/cmd_handling/include/cmd_handling_api.h
rename to shrapnel/cmd_handling/include/cmd_handling_api.h
diff --git a/firmware/components/cmd_handling/src/cmd_handling_api.cpp b/shrapnel/cmd_handling/src/cmd_handling_api.cpp
similarity index 100%
rename from firmware/components/cmd_handling/src/cmd_handling_api.cpp
rename to shrapnel/cmd_handling/src/cmd_handling_api.cpp
diff --git a/firmware/components/cmd_handling/test/test_cmd_handling.cpp b/shrapnel/cmd_handling/test/test_cmd_handling.cpp
similarity index 99%
rename from firmware/components/cmd_handling/test/test_cmd_handling.cpp
rename to shrapnel/cmd_handling/test/test_cmd_handling.cpp
index 3db3a6ea..310d11a0 100644
--- a/firmware/components/cmd_handling/test/test_cmd_handling.cpp
+++ b/shrapnel/cmd_handling/test/test_cmd_handling.cpp
@@ -17,6 +17,8 @@
* ShrapnelDSP. If not, see .
*/
+#if 0
+
#include "gmock/gmock.h"
#include "gtest/gtest.h"
@@ -161,3 +163,5 @@ TEST_F(CmdHandling, InitialiseParameters)
} // namespace
} // namespace
+
+#endif
\ No newline at end of file
diff --git a/firmware/components/compiler_warning_flags/CMakeLists.txt b/shrapnel/compiler_warning_flags/CMakeLists.txt
similarity index 100%
rename from firmware/components/compiler_warning_flags/CMakeLists.txt
rename to shrapnel/compiler_warning_flags/CMakeLists.txt
diff --git a/firmware/components/float_convert/CMakeLists.txt b/shrapnel/float_convert/CMakeLists.txt
similarity index 100%
rename from firmware/components/float_convert/CMakeLists.txt
rename to shrapnel/float_convert/CMakeLists.txt
diff --git a/firmware/components/float_convert/float_convert.h b/shrapnel/float_convert/float_convert.h
similarity index 100%
rename from firmware/components/float_convert/float_convert.h
rename to shrapnel/float_convert/float_convert.h
diff --git a/shrapnel/main_thread/CMakeLists.txt b/shrapnel/main_thread/CMakeLists.txt
new file mode 100644
index 00000000..446b122d
--- /dev/null
+++ b/shrapnel/main_thread/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_library(shrapnel_main_thread INTERFACE)
+add_library(shrapnel::main_thread ALIAS shrapnel_main_thread)
+
+target_include_directories(shrapnel_main_thread INTERFACE include)
+
+target_link_libraries(shrapnel_main_thread INTERFACE
+ shrapnel::midi_mapping
+ shrapnel::audio_param
+ shrapnel::cmd_handling
+ shrapnel::messages
+ shrapnel::midi_handling
+ shrapnel::midi_protocol
+ shrapnel::os
+ shrapnel::persistence
+ shrapnel::presets
+)
\ No newline at end of file
diff --git a/firmware/main/include/main_thread.h b/shrapnel/main_thread/include/main_thread.h
similarity index 59%
rename from firmware/main/include/main_thread.h
rename to shrapnel/main_thread/include/main_thread.h
index e1adb4e5..8bfe403f 100644
--- a/firmware/main/include/main_thread.h
+++ b/shrapnel/main_thread/include/main_thread.h
@@ -20,10 +20,10 @@
#pragma once
#include
+#include
#include
#include "audio_param.h"
-#include "cmd_handling.h"
#include "messages.h"
#include "midi_handling.h"
#include "midi_mapping.pb.h"
@@ -41,18 +41,32 @@ namespace {
constexpr const char *TAG = "main_thread";
} // namespace
-constexpr const size_t MAX_PARAMETERS = 20;
+template
+concept UpdatableParameter = requires(T a, parameters::id_t id, float value) {
+ {
+ a.update(id, value)
+ } -> std::same_as;
+};
-class ParameterUpdateNotifier;
+template
+concept GettableParameter = requires(T a, parameters::id_t id) {
+ {
+ a.get(id)
+ } -> std::same_as;
+};
-using AudioParameters = parameters::AudioParameters;
-using SendMessageCallback = etl::delegate;
+using SendMessageCallback = etl::delegate;
+using SendMessageCallback2 =
+ etl::delegate &)>;
using MidiMappingType = midi::MappingManager<10, 1>;
+template
+ requires GettableParameter &&
+ UpdatableParameter
class ParameterUpdateNotifier
{
public:
- ParameterUpdateNotifier(std::shared_ptr a_audio_params,
+ ParameterUpdateNotifier(std::shared_ptr a_audio_params,
SendMessageCallback a_send_message)
: audio_params{std::move(a_audio_params)},
send_message{a_send_message}
@@ -61,14 +75,7 @@ class ParameterUpdateNotifier
int update(const parameters::id_t ¶m, float value)
{
- auto message = AppMessage{
- parameters::Update{
- param,
- value,
- },
- std::nullopt,
- };
- send_message(message);
+ send_message({parameters::Update{param, value}});
return audio_params->update(param, value);
}
@@ -78,82 +85,10 @@ class ParameterUpdateNotifier
}
private:
- std::shared_ptr audio_params;
+ std::shared_ptr audio_params;
SendMessageCallback send_message;
};
-template
-class ParameterObserver final : public parameters::ParameterObserver
-{
-public:
- explicit ParameterObserver(
- std::shared_ptr a_persistence)
- : is_save_throttled{true},
- persistence{std::move(a_persistence)},
- timer{"param save throttle",
- pdMS_TO_TICKS(10'000),
- false,
- etl::delegate::create<
- ParameterObserver,
- &ParameterObserver::timer_callback>(*this)}
- {
- }
-
- void
- notification(std::pair parameter) override
- {
- auto &[id, value] = parameter;
- ESP_LOGI(
- TAG, "notified about parameter change %s %f", id.data(), value);
- if(!updated_parameters.available())
- {
- ESP_LOGE(TAG, "no space available");
- return;
- }
-
- updated_parameters[id] = value;
-
- if(!timer.is_active())
- {
- if(pdPASS != timer.start(pdMS_TO_TICKS(5)))
- {
- ESP_LOGE(TAG, "Failed to start parameter observer timer");
- }
- }
- }
-
- void loop()
- {
- if(!is_save_throttled.test_and_set())
- {
- persist_parameters();
- ESP_LOGI(TAG, "Parameters saved to NVS");
- }
- }
-
-private:
- void timer_callback()
- {
- is_save_throttled.clear();
- is_save_throttled.notify_all();
- }
-
- void persist_parameters()
- {
- for(const auto ¶m : updated_parameters)
- {
- persistence->save(param.first.data(), param.second);
- }
-
- updated_parameters.clear();
- }
-
- std::atomic_flag is_save_throttled;
- std::shared_ptr persistence;
- os::Timer timer;
- etl::map updated_parameters;
-};
-
template
class PresetLoader
{
@@ -189,10 +124,7 @@ class PresetLoader
presets::deserialise_live_parameters(*parameters, preset.parameters);
- send_message({selected_preset::Notify{
- .selectedPresetId = id,
- },
- std::nullopt});
+ send_message({selected_preset::Notify{.selectedPresetId = id}});
return 0;
}
@@ -204,32 +136,34 @@ class PresetLoader
SendMessageCallback send_message;
};
-template
+template
+ requires GettableParameter &&
+ UpdatableParameter
class MainThread
{
public:
MainThread(SendMessageCallback a_send_message,
+ SendMessageCallback2 a_send_message2,
Queue &a_in_queue,
- midi::MidiUartBase *a_midi_uart,
- std::shared_ptr a_audio_params,
+ std::shared_ptr a_audio_params,
std::shared_ptr a_persistence,
std::unique_ptr>>
a_midi_mapping_storage,
std::unique_ptr>>
- a_presets_storage)
+ a_presets_storage,
+ etl::delegate()> a_get_midi_byte)
: send_message{a_send_message},
+ send_message2{a_send_message2},
in_queue{a_in_queue},
- parameter_observer{a_persistence},
clipping_throttle_timer{
- "clipping throttle", pdMS_TO_TICKS(1000), false},
+ "clipping throttle", os::ms_to_ticks(1000), false},
midi_message_notify_timer{
"midi notify",
- pdMS_TO_TICKS(100),
+ os::ms_to_ticks(100),
true,
os::Timer::Callback::
create(
*this)},
- midi_uart{a_midi_uart},
last_midi_message{},
last_notified_midi_message{},
midi_decoder{std::make_unique(
@@ -238,97 +172,35 @@ class MainThread
*this))},
midi_mutex{},
audio_params{a_audio_params},
- cmd_handling{
- std::make_unique>(
- a_audio_params,
- parameters::CommandHandling::
- SendMessageCallback::create<
- MainThread,
- &MainThread::cmd_handling_send_message>(*this))},
presets_manager{std::make_shared(
std::move(a_presets_storage))},
selected_preset_manager{
std::make_shared(
- a_persistence)}
+ a_persistence)},
+ get_midi_byte{a_get_midi_byte}
{
- auto create_and_load_parameter = [&](const parameters::id_t &name,
- float minimum,
- float maximum,
- float default_value)
- {
- std::optional loaded_value;
- float value;
- int rc = a_persistence->load(name.data(), value);
- if(rc != 0)
- {
- ESP_LOGW(TAG, "Parameter %s failed to load", name.data());
- goto out;
- }
-
- loaded_value = value;
- out:
- auto range = maximum - minimum;
- rc = a_audio_params->create_and_add_parameter(
- name,
- minimum,
- maximum,
- loaded_value.has_value() ? *loaded_value * range + minimum
- : default_value);
- if(rc != 0)
- {
- ESP_LOGE(TAG, "Failed to create parameter %s", name.c_str());
- }
- };
-
- // XXX: These are duplicated in the JUCE plugin, be sure to update both at
- // the same time
- create_and_load_parameter("ampGain", 0, 1, 0.5);
- create_and_load_parameter("ampChannel", 0, 1, 0);
- create_and_load_parameter("bass", 0, 1, 0.5);
- create_and_load_parameter("middle", 0, 1, 0.5);
- create_and_load_parameter("treble", 0, 1, 0.5);
- //contour gets unstable when set to 0
- create_and_load_parameter("contour", 0.01, 1, 0.5);
- create_and_load_parameter("volume", -30, 0, -15);
-
- create_and_load_parameter("noiseGateThreshold", -80, 0, -60);
- create_and_load_parameter("noiseGateHysteresis", 0, 5, 0);
- create_and_load_parameter("noiseGateAttack", 1, 50, 10);
- create_and_load_parameter("noiseGateHold", 1, 250, 50);
- create_and_load_parameter("noiseGateRelease", 1, 250, 50);
- create_and_load_parameter("noiseGateBypass", 0, 1, 0);
-
- create_and_load_parameter("chorusRate", 0.1, 4, 0.95);
- create_and_load_parameter("chorusDepth", 0, 1, 0.3);
- create_and_load_parameter("chorusMix", 0, 1, 0.8);
- create_and_load_parameter("chorusBypass", 0, 1, 1);
-
- create_and_load_parameter("wahPosition", 0, 1, 0.5);
- create_and_load_parameter("wahVocal", 0, 1, 0);
- create_and_load_parameter("wahBypass", 0, 1, 1);
-
- a_audio_params->add_observer(parameter_observer);
-
- parameter_notifier = std::make_shared(
- a_audio_params, a_send_message);
+ parameter_notifier =
+ std::make_shared>(
+ a_audio_params, a_send_message);
midi_mapping_manager = std::make_shared(
std::move(a_midi_mapping_storage));
- preset_loader = std::make_shared>(
+ preset_loader = std::make_shared<
+ PresetLoader>>(
parameter_notifier,
presets_manager,
selected_preset_manager,
send_message);
- midi_message_handler = std::make_shared<
- MidiMessageHandler>>(
+ midi_message_handler = std::make_shared,
+ MidiMappingType,
+ PresetLoader>>>(
parameter_notifier, midi_mapping_manager, preset_loader);
- BaseType_t rc = midi_message_notify_timer.start(portMAX_DELAY);
- if(rc != pdPASS)
+ auto rc = midi_message_notify_timer.start(os::max_delay());
+ if(rc != os::timer_error::TIMER_START_SUCCESS)
{
ESP_LOGE(TAG, "Failed to start midi message timer");
}
@@ -340,21 +212,20 @@ class MainThread
void loop()
{
{
- auto byte = midi_uart->get_byte(0);
+ auto byte = get_midi_byte();
while(byte.has_value())
{
ESP_LOGI(TAG, "midi got byte 0x%02x", *byte);
midi_decoder->decode(*byte);
- byte = midi_uart->get_byte(0);
+ byte = get_midi_byte();
}
}
- if(AppMessage message; in_queue.receive(&message, 0))
+ if(AppMessage message;
+ queue_error::SUCCESS == in_queue.receive(&message, 0))
{
- auto fd = message.second;
-
- std::visit([this, fd](const auto &m) { handle_message(m, fd); },
- message.first);
+ std::visit([this](const auto &m) { this->handle_message(m); },
+ message);
}
if(!clipping_throttle_timer.is_active())
@@ -362,10 +233,9 @@ class MainThread
if(!events::input_clipped.test_and_set())
{
ESP_LOGI(TAG, "input was clipped");
- send_message({events::InputClipped{}, std::nullopt});
- BaseType_t rc =
- clipping_throttle_timer.start(pdMS_TO_TICKS(10));
- if(rc != pdPASS)
+ send_message({events::InputClipped{}});
+ auto rc = clipping_throttle_timer.start(os::ms_to_ticks(10));
+ if(rc != os::timer_error::TIMER_START_SUCCESS)
{
ESP_LOGE(TAG, "Failed to start clipping throttle timer");
}
@@ -374,48 +244,42 @@ class MainThread
if(!events::output_clipped.test_and_set())
{
ESP_LOGI(TAG, "output was clipped");
- send_message({events::OutputClipped{}, std::nullopt});
- BaseType_t rc =
- clipping_throttle_timer.start(pdMS_TO_TICKS(10));
- if(rc != pdPASS)
+ send_message({events::OutputClipped{}});
+ auto rc = clipping_throttle_timer.start(os::ms_to_ticks(10));
+ if(rc != os::timer_error::TIMER_START_SUCCESS)
{
ESP_LOGE(TAG, "Failed to start clipping throttle timer");
}
}
}
- parameter_observer.loop();
-
if(!is_midi_notify_waiting.test_and_set() &&
last_midi_message.has_value() &&
last_notified_midi_message != *last_midi_message)
{
last_notified_midi_message = *last_midi_message;
- send_message(
- {midi::MessageReceived{*last_midi_message}, std::nullopt});
+ send_message({midi::MessageReceived{*last_midi_message}});
}
}
private:
void
- handle_message(const selected_preset::SelectedPresetApiMessage &app_message,
- std::optional)
+ handle_message(const selected_preset::SelectedPresetApiMessage &app_message)
{
auto response = std::visit(
[this](const auto &message)
-> std::optional
- { return handle_selected_preset_message(message); },
+ { return this->handle_selected_preset_message(message); },
app_message);
if(response.has_value())
{
- send_message({*response, std::nullopt});
+ send_message({*response});
}
}
- void handle_message(const presets::PresetsApiMessage &app_message,
- std::optional)
+ void handle_message(const presets::PresetsApiMessage &app_message)
{
auto response = std::visit(
[&](const auto &presets_message)
@@ -432,10 +296,9 @@ class MainThread
const presets::PresetData &preset)
{
send_message({presets::Notify{
- .id = id,
- .preset = preset,
- },
- std::nullopt});
+ .id = id,
+ .preset = preset,
+ }});
});
}
else if constexpr(std::is_same_v fd)
+ void handle_message(const parameters::ApiMessage &app_message)
{
- if(!fd.has_value())
- {
- ESP_LOGE(TAG, "Must always have fd");
- }
+ std::visit(
+ overloaded{
+ [&](const parameters::Initialise &)
+ {
+ ESP_LOGD(TAG, "initialise");
- cmd_handling->dispatch(app_message, *fd);
- }
+ for(const auto &[key, value] : *audio_params)
+ {
+ parameters::Update message = {
+ .id{parameters::id_t{key}},
+ .value{value->get()},
+ };
- void handle_message(const midi::MappingApiMessage &app_message,
- std::optional)
+ send_message(message);
+ }
+ },
+ [](const parameters::Update &message) { assert(false); },
+ },
+ app_message);
+ } // namespace shrapnel
+
+ void handle_message(const midi::MappingApiMessage &app_message)
{
std::scoped_lock lock{midi_mutex};
@@ -524,15 +398,7 @@ class MainThread
auto mappings = midi_mapping_manager->get();
for(const auto &[id, mapping] : *mappings)
{
- send_message({
- midi::Update{
- {
- id,
- mapping,
- },
- },
- std::nullopt,
- });
+ return {midi::Update{{id, mapping}}};
}
}
else if constexpr(std::is_same_v)
+ void handle_message(const midi::Message &message)
{
on_midi_message(message);
}
- void handle_message(const events::ApiMessage &app_message,
- std::optional)
+ void handle_message(const events::ApiMessage &app_message)
{
// nothing to do
}
+ void handle_message(const ParameterUpdateMessage &app_message)
+ {
+ ESP_LOGI(TAG, "TODO handle parameter update");
+
+ etl::string<100> buffer;
+ etl::string_stream stream{buffer};
+ stream << app_message;
+ ESP_LOGI(TAG, "%s", buffer.data());
+ }
+
std::optional
handle_selected_preset_message(selected_preset::Read)
{
@@ -640,37 +515,31 @@ class MainThread
void clear_midi_notify_waiting() { is_midi_notify_waiting.clear(); };
- void cmd_handling_send_message(const parameters::ApiMessage &m,
- std::optional fd)
- {
- send_message({m, fd});
- }
-
SendMessageCallback send_message;
+ SendMessageCallback2 send_message2;
Queue &in_queue;
- ParameterObserver parameter_observer;
os::Timer clipping_throttle_timer;
os::Timer midi_message_notify_timer;
std::atomic_flag is_midi_notify_waiting;
- midi::MidiUartBase *midi_uart;
std::optional last_midi_message;
std::optional last_notified_midi_message;
std::unique_ptr midi_decoder;
std::mutex midi_mutex;
std::shared_ptr midi_mapping_manager;
- std::shared_ptr>>
+ std::shared_ptr,
+ MidiMappingType,
+ PresetLoader>>>
midi_message_handler;
- std::shared_ptr audio_params;
- std::unique_ptr>>
- cmd_handling;
+ std::shared_ptr audio_params;
std::shared_ptr presets_manager;
std::shared_ptr
selected_preset_manager;
- std::shared_ptr parameter_notifier;
- std::shared_ptr> preset_loader;
+ std::shared_ptr>
+ parameter_notifier;
+ std::shared_ptr>>
+ preset_loader;
+ etl::delegate()> get_midi_byte;
};
} // namespace shrapnel
\ No newline at end of file
diff --git a/firmware/components/messages/CMakeLists.txt b/shrapnel/messages/CMakeLists.txt
similarity index 67%
rename from firmware/components/messages/CMakeLists.txt
rename to shrapnel/messages/CMakeLists.txt
index 404df7af..3a7bddb0 100644
--- a/firmware/components/messages/CMakeLists.txt
+++ b/shrapnel/messages/CMakeLists.txt
@@ -1,15 +1,15 @@
-include(../../../thirdparty/esp-idf-components/nanopb/nanopb-generate-cpp.cmake)
+include(../../thirdparty/esp-idf-components/nanopb/nanopb-generate-cpp.cmake)
add_library(messages STATIC)
add_library(shrapnel::messages ALIAS messages)
set(NANOPB_GENERATE_CPP_APPEND_PATH ON)
nanopb_generate_cpp(PROTO_SRCS PROTO_HDRS
- ../../../proto/audio_events.proto
- ../../../proto/cmd_handling.proto
- ../../../proto/shrapnel.proto
- ../../../proto/presets.proto
- ../../../proto/midi_mapping.proto)
+ ../../proto/audio_events.proto
+ ../../proto/cmd_handling.proto
+ ../../proto/shrapnel.proto
+ ../../proto/presets.proto
+ ../../proto/midi_mapping.proto)
target_include_directories(messages PUBLIC
include
diff --git a/shrapnel/messages/include/messages.h b/shrapnel/messages/include/messages.h
new file mode 100644
index 00000000..607a967b
--- /dev/null
+++ b/shrapnel/messages/include/messages.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 Barabas Raffai
+ *
+ * This file is part of ShrapnelDSP.
+ *
+ * ShrapnelDSP is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * ShrapnelDSP is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ShrapnelDSP. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+template