diff --git a/include/bell/dsp/MixerTransform.h b/include/bell/dsp/MixerTransform.h index a4222f4..87e108d 100644 --- a/include/bell/dsp/MixerTransform.h +++ b/include/bell/dsp/MixerTransform.h @@ -1,43 +1,50 @@ -// #pragma once - -// // Standard includes -// #include -// #include - -// // Bell includes -// #include "TransformPipeline.h" - -// namespace bell::dsp { -// /** -// * @brief MixerTransform allows for either downmixing or upmixing of audio channels. -// */ -// class MixerTransform : public Transform { -// public: -// MixerTransform() = default; - -// /** -// * @brief Configure the mixer with the given map of input channel to output channels -// * -// * For example, to downmix stereo (0, 1) to mono (0), use [ {0, 0}, {1, 0} ] -// * To upmix mono (0) to stereo (0, 1), use [ {0, 0}, {0, 1} ] -// * -// * @param mixerMap Vector of pairs of input channel to output channel -// */ -// void configure(const std::vector>& mixerMapping); - -// // Transform implementation, see Transform.h for details -// void process(DataSlots& sampleSlots) override; -// float calculateHeadroom() override; - -// private: -// std::vector> mixerMapping; - -// // Calculates the input and output size -// int sourceChannels = 0; -// int targetChannels = 0; -// }; -// } // namespace bell::dsp - -// namespace bell { -// using MixerTransform = dsp::MixerTransform; -// } +#pragma once + +// Standard includes +#include +#include + +// Bell includes +#include "TransformPipeline.h" + +namespace bell::dsp { +/** + * @brief MixerTransform allows for either downmixing or upmixing of audio channels. + */ +class MixerTransform : public Transform { + public: + MixerTransform() = default; + + /** + * @brief Configure the mixer with the given map of input channel to output channels + * + * For example, to downmix stereo (0, 1) to mono (0), use [ {0, 0}, {1, 0} ] + * To upmix mono (0) to stereo (0, 1), use [ {0, 0}, {0, 1} ] + * + * @param mixerMap Vector of pairs of input channel to output channel + */ + void configure(const std::vector>& mixerMapping); + + // Transform implementation, see Transform.h for details + void process(DataSlots& sampleSlots) override; + float calculateHeadroom() override; + + private: + // Mixer config + std::vector> mixerMapping; + + using ChannelData = std::array; + + // A map to keep track of how many source channels are contributing to each target channel + std::unordered_map> outputDataAcc{}; + + // Calculates the input and output size + // Currently unused + int sourceChannels = 0; + int targetChannels = 0; +}; +} // namespace bell::dsp + +namespace bell { +using MixerTransform = dsp::MixerTransform; +} diff --git a/include/bell/utils/Task.h b/include/bell/utils/Task.h index f931e0c..b683829 100644 --- a/include/bell/utils/Task.h +++ b/include/bell/utils/Task.h @@ -54,7 +54,7 @@ class Task { * @brief The task loop function. This function is called repeatedly by the runTask method. * @remark This method should be implemented by the derived class to perform the task's work, unless a custom runTask method is provided. */ - virtual void taskLoop() {}; + virtual void taskLoop(){}; // @brief Starts the task's execution. This method is implemented per-platform. bool startTask(); diff --git a/main/dsp/MixerTransform.cpp b/main/dsp/MixerTransform.cpp new file mode 100644 index 0000000..191fcb1 --- /dev/null +++ b/main/dsp/MixerTransform.cpp @@ -0,0 +1,69 @@ +#include "bell/dsp/MixerTransform.h" + +using namespace bell; + +void dsp::MixerTransform::configure( + const std::vector>& mixerMapping) { + std::scoped_lock lock(accessMutex); + this->mixerMapping = mixerMapping; +} + +float dsp::MixerTransform::calculateHeadroom() { + return 0.0F; // No headroom required for mixer +} + +void dsp::MixerTransform::process(dsp::DataSlots& sampleSlots) { + std::scoped_lock lock(accessMutex); + + // Initialize each target channel in sampleSlots to make sure we do not run into missing keys + for (const auto& [inputChannel, outputChannel] : mixerMapping) { + // Clear the output data acc + outputDataAcc[outputChannel].first.fill(0.0F); + outputDataAcc[outputChannel].second = 0; + + if (sampleSlots.sampleSlots.find(outputChannel) == + sampleSlots.sampleSlots.end()) { + sampleSlots.sampleSlots[outputChannel] = ChannelData{}; + } + } + + // Fill each outputChannel with contributions from inputChannels + for (const auto& [inputChannel, outputChannel] : mixerMapping) { + const auto itInput = sampleSlots.sampleSlots.find(inputChannel); + if (itInput != sampleSlots.sampleSlots.end()) { + auto& [dataSum, count] = outputDataAcc[outputChannel]; + auto& inputData = itInput->second; + for (size_t i = 0; i < sampleSlots.numSamples; ++i) { + dataSum[i] += inputData[i]; + } + count += 1; + } + } + + // Write back the accumulated data to sampleSlots, averaging if necessary + for (auto& [outputChannel, acc] : outputDataAcc) { + auto& [dataSum, count] = acc; + auto& outputData = sampleSlots.sampleSlots[outputChannel]; + for (size_t i = 0; i < sampleSlots.numSamples; ++i) { + outputData[i] = dataSum[i] / std::max(static_cast(count), 1.0F); + } + } + + // Zero out any input channels that are being downmixed (not remapped back) + std::unordered_map inputMappedBack; + for (const auto& [inputChannel, _] : mixerMapping) { + inputMappedBack[inputChannel] = false; + } + for (const auto& [_, outputChannel] : mixerMapping) { + inputMappedBack[outputChannel] = true; // mark this channel as final target + } + + for (const auto& [inputChannel, _] : mixerMapping) { + if (!inputMappedBack[inputChannel]) { + std::fill(sampleSlots.sampleSlots[inputChannel].begin(), + sampleSlots.sampleSlots[inputChannel].begin() + + sampleSlots.numSamples, + 0.0F); + } + } +} \ No newline at end of file diff --git a/main/dsp/TransformPipeline.cpp b/main/dsp/TransformPipeline.cpp index 7c6a226..3e14e03 100644 --- a/main/dsp/TransformPipeline.cpp +++ b/main/dsp/TransformPipeline.cpp @@ -4,6 +4,7 @@ #include "bell/dsp/BiquadComboTransform.h" #include "bell/dsp/BiquadTransform.h" #include "bell/dsp/GainTransform.h" +#include "bell/dsp/MixerTransform.h" #ifndef BELL_DISABLE_TAOJSON // Used for JSON deserialization of the transforms @@ -57,7 +58,7 @@ std::shared_ptr parseTransform(const tao::json::value& json) { json.at("channel").is_number()) { // Parse the channel channels.push_back(json.at("channel").as()); - } else { + } else if (transformType != "mixer") { throw std::invalid_argument("Channels not specified for transform"); } @@ -76,6 +77,21 @@ std::shared_ptr parseTransform(const tao::json::value& json) { return gain; } + // Parse the gain transform + if (transformType == "mixer") { + auto mixer = std::make_shared(); + + if (json.find("mapping") != nullptr && json.at("mapping").is_array()) { + mixer->configure( + json.at("mapping").as>>()); + } else { + throw std::invalid_argument("Invalid mixer mapping"); + } + + return mixer; + } + + // Parse the biquad transform if (transformType == "biquad") { if (channels.size() > 1) { throw std::invalid_argument("Biquad transform only supports one channel"); diff --git a/main/net/MQTTClient.cpp b/main/net/MQTTClient.cpp index f5c6d8b..802ead5 100644 --- a/main/net/MQTTClient.cpp +++ b/main/net/MQTTClient.cpp @@ -78,7 +78,7 @@ void MQTTClient::publish(const std::string& topic, const std::string& message, } int err = mqtt_publish(&client, topic.c_str(), message.c_str(), - message.size(), (uint8_t)qos); + message.size(), static_cast(qos)); if (err != MQTT_OK) { BELL_LOG(error, "mqtt", "MQTT publish failed: %d", err);