Skip to content

Commit

Permalink
feat: Implement the mixer transform
Browse files Browse the repository at this point in the history
  • Loading branch information
feelfreelinux committed Jan 31, 2025
1 parent 3dc1bb0 commit 1d1a1bd
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 46 deletions.
93 changes: 50 additions & 43 deletions include/bell/dsp/MixerTransform.h
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
// #pragma once

// // Standard includes
// #include <utility>
// #include <vector>

// // 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<std::pair<int, int>>& mixerMapping);

// // Transform implementation, see Transform.h for details
// void process(DataSlots& sampleSlots) override;
// float calculateHeadroom() override;

// private:
// std::vector<std::pair<int, int>> 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 <utility>
#include <vector>

// 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<std::pair<int, int>>& mixerMapping);

// Transform implementation, see Transform.h for details
void process(DataSlots& sampleSlots) override;
float calculateHeadroom() override;

private:
// Mixer config
std::vector<std::pair<int, int>> mixerMapping;

using ChannelData = std::array<float, DataSlots::maxSamples>;

// A map to keep track of how many source channels are contributing to each target channel
std::unordered_map<int, std::pair<ChannelData, int>> outputDataAcc{};

// Calculates the input and output size
// Currently unused
int sourceChannels = 0;
int targetChannels = 0;
};
} // namespace bell::dsp

namespace bell {
using MixerTransform = dsp::MixerTransform;
}
2 changes: 1 addition & 1 deletion include/bell/utils/Task.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
69 changes: 69 additions & 0 deletions main/dsp/MixerTransform.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#include "bell/dsp/MixerTransform.h"

using namespace bell;

void dsp::MixerTransform::configure(
const std::vector<std::pair<int, int>>& 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<float>(count), 1.0F);
}
}

// Zero out any input channels that are being downmixed (not remapped back)
std::unordered_map<int, bool> 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);
}
}
}
18 changes: 17 additions & 1 deletion main/dsp/TransformPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,7 +58,7 @@ std::shared_ptr<Transform> parseTransform(const tao::json::value& json) {
json.at("channel").is_number()) {
// Parse the channel
channels.push_back(json.at("channel").as<uint8_t>());
} else {
} else if (transformType != "mixer") {
throw std::invalid_argument("Channels not specified for transform");
}

Expand All @@ -76,6 +77,21 @@ std::shared_ptr<Transform> parseTransform(const tao::json::value& json) {
return gain;
}

// Parse the gain transform
if (transformType == "mixer") {
auto mixer = std::make_shared<MixerTransform>();

if (json.find("mapping") != nullptr && json.at("mapping").is_array()) {
mixer->configure(
json.at("mapping").as<std::vector<std::pair<int, int>>>());
} 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");
Expand Down
2 changes: 1 addition & 1 deletion main/net/MQTTClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>(qos));

if (err != MQTT_OK) {
BELL_LOG(error, "mqtt", "MQTT publish failed: %d", err);
Expand Down

0 comments on commit 1d1a1bd

Please sign in to comment.