Initial: OPLabsBandChannel.lv2 + OPLabsBandBus.lv2
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# ── OPLabsBandChannel shared library ─────────────────────────────────
|
||||
add_library(OPLabsBandChannel SHARED
|
||||
OPLabsBandChannel.cpp
|
||||
OPLabsBandChannel.hpp
|
||||
)
|
||||
|
||||
target_include_directories(OPLabsBandChannel
|
||||
PRIVATE
|
||||
${LV2_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_compile_options(OPLabsBandChannel PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
# Output .so directly into the LV2 bundle directory
|
||||
set_target_properties(OPLabsBandChannel PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/lv2/OPLabsBandChannel.lv2"
|
||||
OUTPUT_NAME "OPLabsBandChannel"
|
||||
PREFIX ""
|
||||
)
|
||||
|
||||
# Version script — export only lv2_descriptor
|
||||
target_link_options(OPLabsBandChannel PRIVATE
|
||||
-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/OPLabsBandChannel.version
|
||||
-Wl,-z,nodelete
|
||||
)
|
||||
|
||||
# ── OPLabsBandBus shared library ────────────────────────────────────
|
||||
add_library(OPLabsBandBus SHARED
|
||||
OPLabsBandBus.cpp
|
||||
OPLabsBandBus.hpp
|
||||
)
|
||||
|
||||
target_include_directories(OPLabsBandBus
|
||||
PRIVATE
|
||||
${LV2_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_compile_options(OPLabsBandBus PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
set_target_properties(OPLabsBandBus PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/lv2/OPLabsBandBus.lv2"
|
||||
OUTPUT_NAME "OPLabsBandBus"
|
||||
PREFIX ""
|
||||
)
|
||||
|
||||
target_link_options(OPLabsBandBus PRIVATE
|
||||
-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/OPLabsBandBus.version
|
||||
-Wl,-z,nodelete
|
||||
)
|
||||
@@ -0,0 +1,319 @@
|
||||
#include "OPLabsBandBus.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
|
||||
// ====================================================================
|
||||
// LV2 Descriptor
|
||||
// ====================================================================
|
||||
|
||||
static LV2_Descriptor sDescriptor = {
|
||||
OPLabsBandBus::URI,
|
||||
OPLabsBandBus::instantiate,
|
||||
OPLabsBandBus::connectPort,
|
||||
OPLabsBandBus::activate,
|
||||
OPLabsBandBus::run,
|
||||
OPLabsBandBus::deactivate,
|
||||
OPLabsBandBus::cleanup,
|
||||
OPLabsBandBus::extensionData
|
||||
};
|
||||
|
||||
LV2_SYMBOL_EXPORT
|
||||
const LV2_Descriptor* lv2_descriptor(uint32_t index)
|
||||
{
|
||||
return (index == 0) ? &sDescriptor : nullptr;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// State interface (extension data)
|
||||
// ====================================================================
|
||||
|
||||
static const LV2_State_Interface sStateInterface = {
|
||||
OPLabsBandBus::saveState,
|
||||
OPLabsBandBus::restoreState
|
||||
};
|
||||
|
||||
// ====================================================================
|
||||
// Constructor
|
||||
// ====================================================================
|
||||
|
||||
OPLabsBandBus::OPLabsBandBus(double sampleRate,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)sampleRate;
|
||||
initUrids(features);
|
||||
}
|
||||
|
||||
bool OPLabsBandBus::initUrids(const LV2_Feature* const* features)
|
||||
{
|
||||
for (int i = 0; features && features[i]; ++i) {
|
||||
if (strcmp(features[i]->URI, LV2_URID__map) == 0) {
|
||||
uridMap = (LV2_URID_Map*)features[i]->data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!uridMap) {
|
||||
fprintf(stderr, "OPLabsBandBus: Host does not support urid:map\n");
|
||||
return false;
|
||||
}
|
||||
urid__stateDummy = uridMap->map(
|
||||
uridMap->handle,
|
||||
"http://ourpad.casa/plugins/oplabs-band-bus#dummy"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Static callbacks
|
||||
// ====================================================================
|
||||
|
||||
LV2_Handle OPLabsBandBus::instantiate(const LV2_Descriptor* descriptor,
|
||||
double sampleRate,
|
||||
const char* bundlePath,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)descriptor;
|
||||
(void)bundlePath;
|
||||
OPLabsBandBus* self = new OPLabsBandBus(sampleRate, features);
|
||||
if (!self->uridMap) {
|
||||
delete self;
|
||||
return nullptr;
|
||||
}
|
||||
return (LV2_Handle)self;
|
||||
}
|
||||
|
||||
void OPLabsBandBus::connectPort(LV2_Handle instance, uint32_t port, void* data)
|
||||
{
|
||||
OPLabsBandBus* self = (OPLabsBandBus*)instance;
|
||||
|
||||
switch ((Ports)port) {
|
||||
// Audio inputs
|
||||
case kCh1L: self->pAudioIn[0][0] = (const float*)data; break;
|
||||
case kCh1R: self->pAudioIn[0][1] = (const float*)data; break;
|
||||
case kCh2L: self->pAudioIn[1][0] = (const float*)data; break;
|
||||
case kCh2R: self->pAudioIn[1][1] = (const float*)data; break;
|
||||
case kCh3L: self->pAudioIn[2][0] = (const float*)data; break;
|
||||
case kCh3R: self->pAudioIn[2][1] = (const float*)data; break;
|
||||
case kCh4L: self->pAudioIn[3][0] = (const float*)data; break;
|
||||
case kCh4R: self->pAudioIn[3][1] = (const float*)data; break;
|
||||
case kCh5L: self->pAudioIn[4][0] = (const float*)data; break;
|
||||
case kCh5R: self->pAudioIn[4][1] = (const float*)data; break;
|
||||
case kCh6L: self->pAudioIn[5][0] = (const float*)data; break;
|
||||
case kCh6R: self->pAudioIn[5][1] = (const float*)data; break;
|
||||
case kCh7L: self->pAudioIn[6][0] = (const float*)data; break;
|
||||
case kCh7R: self->pAudioIn[6][1] = (const float*)data; break;
|
||||
case kCh8L: self->pAudioIn[7][0] = (const float*)data; break;
|
||||
case kCh8R: self->pAudioIn[7][1] = (const float*)data; break;
|
||||
// Audio outputs
|
||||
case kOutL: self->pAudioOut[0] = (float*)data; break;
|
||||
case kOutR: self->pAudioOut[1] = (float*)data; break;
|
||||
// Per-channel volume
|
||||
case kCh1Vol: self->pVol[0] = (const float*)data; break;
|
||||
case kCh2Vol: self->pVol[1] = (const float*)data; break;
|
||||
case kCh3Vol: self->pVol[2] = (const float*)data; break;
|
||||
case kCh4Vol: self->pVol[3] = (const float*)data; break;
|
||||
case kCh5Vol: self->pVol[4] = (const float*)data; break;
|
||||
case kCh6Vol: self->pVol[5] = (const float*)data; break;
|
||||
case kCh7Vol: self->pVol[6] = (const float*)data; break;
|
||||
case kCh8Vol: self->pVol[7] = (const float*)data; break;
|
||||
// Per-channel pan
|
||||
case kCh1Pan: self->pPan[0] = (const float*)data; break;
|
||||
case kCh2Pan: self->pPan[1] = (const float*)data; break;
|
||||
case kCh3Pan: self->pPan[2] = (const float*)data; break;
|
||||
case kCh4Pan: self->pPan[3] = (const float*)data; break;
|
||||
case kCh5Pan: self->pPan[4] = (const float*)data; break;
|
||||
case kCh6Pan: self->pPan[5] = (const float*)data; break;
|
||||
case kCh7Pan: self->pPan[6] = (const float*)data; break;
|
||||
case kCh8Pan: self->pPan[7] = (const float*)data; break;
|
||||
// Per-channel mute
|
||||
case kCh1Mute: self->pMute[0] = (const float*)data; break;
|
||||
case kCh2Mute: self->pMute[1] = (const float*)data; break;
|
||||
case kCh3Mute: self->pMute[2] = (const float*)data; break;
|
||||
case kCh4Mute: self->pMute[3] = (const float*)data; break;
|
||||
case kCh5Mute: self->pMute[4] = (const float*)data; break;
|
||||
case kCh6Mute: self->pMute[5] = (const float*)data; break;
|
||||
case kCh7Mute: self->pMute[6] = (const float*)data; break;
|
||||
case kCh8Mute: self->pMute[7] = (const float*)data; break;
|
||||
// Master controls
|
||||
case kMasterVol: self->pMasterVol = (const float*)data; break;
|
||||
case kMasterMute: self->pMasterMute = (const float*)data; break;
|
||||
// Per-channel level L outputs
|
||||
case kCh1LevelL: self->pLevelL[0] = (float*)data; break;
|
||||
case kCh2LevelL: self->pLevelL[1] = (float*)data; break;
|
||||
case kCh3LevelL: self->pLevelL[2] = (float*)data; break;
|
||||
case kCh4LevelL: self->pLevelL[3] = (float*)data; break;
|
||||
case kCh5LevelL: self->pLevelL[4] = (float*)data; break;
|
||||
case kCh6LevelL: self->pLevelL[5] = (float*)data; break;
|
||||
case kCh7LevelL: self->pLevelL[6] = (float*)data; break;
|
||||
case kCh8LevelL: self->pLevelL[7] = (float*)data; break;
|
||||
// Per-channel level R outputs
|
||||
case kCh1LevelR: self->pLevelR[0] = (float*)data; break;
|
||||
case kCh2LevelR: self->pLevelR[1] = (float*)data; break;
|
||||
case kCh3LevelR: self->pLevelR[2] = (float*)data; break;
|
||||
case kCh4LevelR: self->pLevelR[3] = (float*)data; break;
|
||||
case kCh5LevelR: self->pLevelR[4] = (float*)data; break;
|
||||
case kCh6LevelR: self->pLevelR[5] = (float*)data; break;
|
||||
case kCh7LevelR: self->pLevelR[6] = (float*)data; break;
|
||||
case kCh8LevelR: self->pLevelR[7] = (float*)data; break;
|
||||
// Master level outputs
|
||||
case kMasterLevelL: self->pMasterLevelL = (float*)data; break;
|
||||
case kMasterLevelR: self->pMasterLevelR = (float*)data; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void OPLabsBandBus::activate(LV2_Handle instance)
|
||||
{
|
||||
(void)instance;
|
||||
}
|
||||
|
||||
void OPLabsBandBus::run(LV2_Handle instance, uint32_t nSamples)
|
||||
{
|
||||
OPLabsBandBus* self = (OPLabsBandBus*)instance;
|
||||
|
||||
// Read master controls
|
||||
if (self->pMasterVol) self->cachedMasterVol = *self->pMasterVol;
|
||||
if (self->pMasterMute) self->cachedMasterMute = (*self->pMasterMute > 0.5f);
|
||||
|
||||
// Read per-channel controls
|
||||
for (uint32_t ch = 0; ch < kNumChannels; ++ch) {
|
||||
if (self->pVol[ch]) self->cachedVol[ch] = *self->pVol[ch];
|
||||
if (self->pPan[ch]) self->cachedPan[ch] = *self->pPan[ch];
|
||||
if (self->pMute[ch]) self->cachedMute[ch] = (*self->pMute[ch] > 0.5f);
|
||||
}
|
||||
|
||||
// Prepare audio output buffers (zero for accumulation)
|
||||
float* outL = self->pAudioOut[0];
|
||||
float* outR = self->pAudioOut[1];
|
||||
if (outL) std::memset(outL, 0, nSamples * sizeof(float));
|
||||
if (outR) std::memset(outR, 0, nSamples * sizeof(float));
|
||||
|
||||
// Master gain (linear)
|
||||
float masterGain = self->cachedMasterMute ? 0.0f
|
||||
: dBToGain(self->cachedMasterVol);
|
||||
|
||||
// Accumulate each channel into the bus
|
||||
float newMasterPeakL = -70.0f;
|
||||
float newMasterPeakR = -70.0f;
|
||||
|
||||
for (uint32_t ch = 0; ch < kNumChannels; ++ch) {
|
||||
const float* inL = self->pAudioIn[ch][0];
|
||||
const float* inR = self->pAudioIn[ch][1];
|
||||
|
||||
bool muted = self->cachedMute[ch];
|
||||
float chGain = muted ? 0.0f : dBToGain(self->cachedVol[ch]);
|
||||
float pan = self->cachedPan[ch];
|
||||
|
||||
// Equal-power pan coefficients
|
||||
float panL = 1.0f;
|
||||
float panR = 1.0f;
|
||||
if (pan <= 0.0f) {
|
||||
panR = 1.0f + pan;
|
||||
} else {
|
||||
panL = 1.0f - pan;
|
||||
}
|
||||
|
||||
// Per-channel peak tracking
|
||||
float chPeakL = -70.0f;
|
||||
float chPeakR = -70.0f;
|
||||
|
||||
for (uint32_t s = 0; s < nSamples; ++s) {
|
||||
float sampleL = inL ? inL[s] : 0.0f;
|
||||
float sampleR = inR ? inR[s] : 0.0f;
|
||||
|
||||
// Apply channel gain
|
||||
sampleL *= chGain;
|
||||
sampleR *= chGain;
|
||||
|
||||
// Apply pan
|
||||
float pannedL = sampleL * panL;
|
||||
float pannedR = sampleR * panR;
|
||||
|
||||
// Accumulate into bus
|
||||
if (outL) outL[s] += pannedL;
|
||||
if (outR) outR[s] += pannedR;
|
||||
|
||||
// Track channel peaks
|
||||
float absL = std::fabs(pannedL);
|
||||
float absR = std::fabs(pannedR);
|
||||
if (absL > chPeakL) chPeakL = absL;
|
||||
if (absR > chPeakR) chPeakR = absR;
|
||||
}
|
||||
|
||||
// Convert channel peaks to dB and write meters
|
||||
self->peakL[ch] = (chPeakL > 0.0f)
|
||||
? (20.0f * std::log10(chPeakL)) : -70.0f;
|
||||
self->peakR[ch] = (chPeakR > 0.0f)
|
||||
? (20.0f * std::log10(chPeakR)) : -70.0f;
|
||||
if (self->peakL[ch] < -70.0f) self->peakL[ch] = -70.0f;
|
||||
if (self->peakR[ch] < -70.0f) self->peakR[ch] = -70.0f;
|
||||
if (self->pLevelL[ch]) *self->pLevelL[ch] = self->peakL[ch];
|
||||
if (self->pLevelR[ch]) *self->pLevelR[ch] = self->peakR[ch];
|
||||
|
||||
// Track master peaks (pre-master-gain)
|
||||
if (chPeakL > newMasterPeakL) newMasterPeakL = chPeakL;
|
||||
if (chPeakR > newMasterPeakR) newMasterPeakR = chPeakR;
|
||||
}
|
||||
|
||||
// Apply master gain to output buffers
|
||||
if (masterGain != 1.0f) {
|
||||
for (uint32_t s = 0; s < nSamples; ++s) {
|
||||
if (outL) outL[s] *= masterGain;
|
||||
if (outR) outR[s] *= masterGain;
|
||||
}
|
||||
}
|
||||
|
||||
// Write master level meters (pre-master-gain reading)
|
||||
self->peakMasterL = (newMasterPeakL > 0.0f)
|
||||
? (20.0f * std::log10(newMasterPeakL)) : -70.0f;
|
||||
self->peakMasterR = (newMasterPeakR > 0.0f)
|
||||
? (20.0f * std::log10(newMasterPeakR)) : -70.0f;
|
||||
if (self->peakMasterL < -70.0f) self->peakMasterL = -70.0f;
|
||||
if (self->peakMasterR < -70.0f) self->peakMasterR = -70.0f;
|
||||
if (self->pMasterLevelL) *self->pMasterLevelL = self->peakMasterL;
|
||||
if (self->pMasterLevelR) *self->pMasterLevelR = self->peakMasterR;
|
||||
}
|
||||
|
||||
void OPLabsBandBus::deactivate(LV2_Handle instance)
|
||||
{
|
||||
(void)instance;
|
||||
}
|
||||
|
||||
void OPLabsBandBus::cleanup(LV2_Handle instance)
|
||||
{
|
||||
delete (OPLabsBandBus*)instance;
|
||||
}
|
||||
|
||||
const void* OPLabsBandBus::extensionData(const char* uri)
|
||||
{
|
||||
if (strcmp(uri, LV2_STATE__interface) == 0) {
|
||||
return &sStateInterface;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// State save/restore — stub implementations
|
||||
// ====================================================================
|
||||
|
||||
LV2_State_Status OPLabsBandBus::saveState(
|
||||
LV2_Handle instance,
|
||||
LV2_State_Store_Function store,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)instance; (void)store; (void)handle; (void)flags; (void)features;
|
||||
return LV2_STATE_SUCCESS;
|
||||
}
|
||||
|
||||
LV2_State_Status OPLabsBandBus::restoreState(
|
||||
LV2_Handle instance,
|
||||
LV2_State_Retrieve_Function retrieve,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)instance; (void)retrieve; (void)handle; (void)flags; (void)features;
|
||||
return LV2_STATE_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#ifndef OPLABSBANDBUS_HPP
|
||||
#define OPLABSBANDBUS_HPP
|
||||
|
||||
#include <lv2/core/lv2.h>
|
||||
#include <lv2/state/state.h>
|
||||
#include <lv2/urid/urid.h>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
/**
|
||||
* OPLabsBandBus — 8-channel stereo bus mixer LV2 plugin.
|
||||
*
|
||||
* Sums 8 stereo input pairs into a stereo output with per-channel
|
||||
* volume, pan, mute controls and peak level metering. Includes master
|
||||
* volume and mute. Compatible with PiPedal and any LV2 host.
|
||||
*
|
||||
* Port layout (62 ports total):
|
||||
* Audio inputs: ch1L..ch8R (0–15, 16 ports)
|
||||
* Audio outputs: outL, outR (16–17, 2 ports)
|
||||
* Controls in: ch1Vol..ch8Vol (18–25, 8 ports)
|
||||
* ch1Pan..ch8Pan (26–33, 8 ports)
|
||||
* ch1Mute..ch8Mute(34–41, 8 ports)
|
||||
* masterVol (42, 1 port)
|
||||
* masterMute (43, 1 port)
|
||||
* Controls out: ch1LevelL..ch8LevelL (44–51, 8 ports)
|
||||
* ch1LevelR..ch8LevelR (52–59, 8 ports)
|
||||
* masterLevelL, masterLevelR (60–61, 2 ports)
|
||||
*
|
||||
* URI: http://ourpad.casa/plugins/oplabs-band-bus#
|
||||
*/
|
||||
class OPLabsBandBus {
|
||||
public:
|
||||
static constexpr const char* URI = "http://ourpad.casa/plugins/oplabs-band-bus#";
|
||||
static constexpr uint32_t kNumChannels = 8;
|
||||
|
||||
enum Ports : uint32_t {
|
||||
// Audio inputs (16)
|
||||
kCh1L = 0, kCh1R = 1,
|
||||
kCh2L = 2, kCh2R = 3,
|
||||
kCh3L = 4, kCh3R = 5,
|
||||
kCh4L = 6, kCh4R = 7,
|
||||
kCh5L = 8, kCh5R = 9,
|
||||
kCh6L = 10, kCh6R = 11,
|
||||
kCh7L = 12, kCh7R = 13,
|
||||
kCh8L = 14, kCh8R = 15,
|
||||
// Audio outputs (2)
|
||||
kOutL = 16,
|
||||
kOutR = 17,
|
||||
// Per-channel volume (8)
|
||||
kCh1Vol = 18, kCh2Vol = 19,
|
||||
kCh3Vol = 20, kCh4Vol = 21,
|
||||
kCh5Vol = 22, kCh6Vol = 23,
|
||||
kCh7Vol = 24, kCh8Vol = 25,
|
||||
// Per-channel pan (8)
|
||||
kCh1Pan = 26, kCh2Pan = 27,
|
||||
kCh3Pan = 28, kCh4Pan = 29,
|
||||
kCh5Pan = 30, kCh6Pan = 31,
|
||||
kCh7Pan = 32, kCh8Pan = 33,
|
||||
// Per-channel mute (8)
|
||||
kCh1Mute = 34, kCh2Mute = 35,
|
||||
kCh3Mute = 36, kCh4Mute = 37,
|
||||
kCh5Mute = 38, kCh6Mute = 39,
|
||||
kCh7Mute = 40, kCh8Mute = 41,
|
||||
// Master controls (2)
|
||||
kMasterVol = 42,
|
||||
kMasterMute = 43,
|
||||
// Per-channel level L (8)
|
||||
kCh1LevelL = 44, kCh2LevelL = 45,
|
||||
kCh3LevelL = 46, kCh4LevelL = 47,
|
||||
kCh5LevelL = 48, kCh6LevelL = 49,
|
||||
kCh7LevelL = 50, kCh8LevelL = 51,
|
||||
// Per-channel level R (8)
|
||||
kCh1LevelR = 52, kCh2LevelR = 53,
|
||||
kCh3LevelR = 54, kCh4LevelR = 55,
|
||||
kCh5LevelR = 56, kCh6LevelR = 57,
|
||||
kCh7LevelR = 58, kCh8LevelR = 59,
|
||||
// Master level (2)
|
||||
kMasterLevelL = 60,
|
||||
kMasterLevelR = 61,
|
||||
kNumPorts = 62
|
||||
};
|
||||
|
||||
// ---- Static LV2 callbacks ----
|
||||
static LV2_Handle instantiate(const LV2_Descriptor* descriptor,
|
||||
double sampleRate,
|
||||
const char* bundlePath,
|
||||
const LV2_Feature* const* features);
|
||||
static void connectPort(LV2_Handle instance, uint32_t port, void* data);
|
||||
static void activate(LV2_Handle instance);
|
||||
static void run(LV2_Handle instance, uint32_t nSamples);
|
||||
static void deactivate(LV2_Handle instance);
|
||||
static void cleanup(LV2_Handle instance);
|
||||
static const void* extensionData(const char* uri);
|
||||
|
||||
// ---- State callbacks ----
|
||||
static LV2_State_Status saveState(LV2_Handle instance,
|
||||
LV2_State_Store_Function store,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features);
|
||||
static LV2_State_Status restoreState(LV2_Handle instance,
|
||||
LV2_State_Retrieve_Function retrieve,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features);
|
||||
|
||||
private:
|
||||
OPLabsBandBus(double sampleRate, const LV2_Feature* const* features);
|
||||
~OPLabsBandBus() = default;
|
||||
OPLabsBandBus(const OPLabsBandBus&) = delete;
|
||||
OPLabsBandBus& operator=(const OPLabsBandBus&) = delete;
|
||||
|
||||
bool initUrids(const LV2_Feature* const* features);
|
||||
static inline float dBToGain(float dB) {
|
||||
return std::pow(10.0f, dB / 20.0f);
|
||||
}
|
||||
|
||||
// URID map
|
||||
LV2_URID_Map* uridMap = nullptr;
|
||||
LV2_URID urid__stateDummy = 0;
|
||||
|
||||
// Audio port pointers [ch][0=L,1=R]
|
||||
const float* pAudioIn[8][2] = {};
|
||||
float* pAudioOut[2] = {};
|
||||
|
||||
// Control input port pointers
|
||||
const float* pVol[8] = {};
|
||||
const float* pPan[8] = {};
|
||||
const float* pMute[8] = {};
|
||||
const float* pMasterVol = nullptr;
|
||||
const float* pMasterMute = nullptr;
|
||||
|
||||
// Control output port pointers (meters)
|
||||
float* pLevelL[8] = {};
|
||||
float* pLevelR[8] = {};
|
||||
float* pMasterLevelL = nullptr;
|
||||
float* pMasterLevelR = nullptr;
|
||||
|
||||
// Cached parameter values
|
||||
float cachedVol[8] = {};
|
||||
float cachedPan[8] = {};
|
||||
bool cachedMute[8] = {};
|
||||
float cachedMasterVol = 0.0f;
|
||||
bool cachedMasterMute = false;
|
||||
|
||||
// Peak meter state
|
||||
float peakL[8] = {};
|
||||
float peakR[8] = {};
|
||||
float peakMasterL = -70.0f;
|
||||
float peakMasterR = -70.0f;
|
||||
};
|
||||
|
||||
#endif // OPLABSBANDBUS_HPP
|
||||
@@ -0,0 +1,6 @@
|
||||
VERS_1.0 {
|
||||
global:
|
||||
lv2_descriptor;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
@@ -0,0 +1,312 @@
|
||||
#include "OPLabsBandChannel.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
|
||||
// ====================================================================
|
||||
// LV2 Descriptor
|
||||
// ====================================================================
|
||||
|
||||
static LV2_Descriptor sDescriptor = {
|
||||
OPLabsBandChannel::URI,
|
||||
OPLabsBandChannel::instantiate,
|
||||
OPLabsBandChannel::connectPort,
|
||||
OPLabsBandChannel::activate,
|
||||
OPLabsBandChannel::run,
|
||||
OPLabsBandChannel::deactivate,
|
||||
OPLabsBandChannel::cleanup,
|
||||
OPLabsBandChannel::extensionData
|
||||
};
|
||||
|
||||
LV2_SYMBOL_EXPORT
|
||||
const LV2_Descriptor* lv2_descriptor(uint32_t index)
|
||||
{
|
||||
return (index == 0) ? &sDescriptor : nullptr;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// State interface (extension data)
|
||||
// ====================================================================
|
||||
|
||||
static const LV2_State_Interface sStateInterface = {
|
||||
OPLabsBandChannel::saveState,
|
||||
OPLabsBandChannel::restoreState
|
||||
};
|
||||
|
||||
// ====================================================================
|
||||
// Constructor / Destructor helpers
|
||||
// ====================================================================
|
||||
|
||||
OPLabsBandChannel::OPLabsBandChannel(double sampleRate,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)sampleRate;
|
||||
initUrids(features);
|
||||
}
|
||||
|
||||
bool OPLabsBandChannel::initUrids(const LV2_Feature* const* features)
|
||||
{
|
||||
for (int i = 0; features && features[i]; ++i) {
|
||||
if (strcmp(features[i]->URI, LV2_URID__map) == 0) {
|
||||
uridMap = (LV2_URID_Map*)features[i]->data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!uridMap) {
|
||||
fprintf(stderr, "OPLabsBandChannel: Host does not support urid:map\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Map state key URIDs
|
||||
urid__stateInstrument = uridMap->map(
|
||||
uridMap->handle,
|
||||
"http://ourpad.casa/plugins/oplabs-band-channel#instrument"
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Static callbacks
|
||||
// ====================================================================
|
||||
|
||||
LV2_Handle OPLabsBandChannel::instantiate(const LV2_Descriptor* descriptor,
|
||||
double sampleRate,
|
||||
const char* bundlePath,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)descriptor;
|
||||
(void)bundlePath;
|
||||
|
||||
OPLabsBandChannel* self = new OPLabsBandChannel(sampleRate, features);
|
||||
if (!self->uridMap) {
|
||||
delete self;
|
||||
return nullptr;
|
||||
}
|
||||
return (LV2_Handle)self;
|
||||
}
|
||||
|
||||
void OPLabsBandChannel::connectPort(LV2_Handle instance, uint32_t port, void* data)
|
||||
{
|
||||
OPLabsBandChannel* self = (OPLabsBandChannel*)instance;
|
||||
|
||||
switch ((Ports)port) {
|
||||
case kVolume:
|
||||
self->pVolume = (const float*)data;
|
||||
break;
|
||||
case kPan:
|
||||
self->pPan = (const float*)data;
|
||||
break;
|
||||
case kMute:
|
||||
self->pMute = (const float*)data;
|
||||
break;
|
||||
case kSolo:
|
||||
self->pSolo = (const float*)data;
|
||||
break;
|
||||
case kInstrument:
|
||||
self->pInstrument = (const float*)data;
|
||||
break;
|
||||
case kLevelL:
|
||||
self->pLevelL = (float*)data;
|
||||
break;
|
||||
case kLevelR:
|
||||
self->pLevelR = (float*)data;
|
||||
break;
|
||||
case kInL:
|
||||
self->pInL = (const float*)data;
|
||||
break;
|
||||
case kInR:
|
||||
self->pInR = (const float*)data;
|
||||
break;
|
||||
case kOutL:
|
||||
self->pOutL = (float*)data;
|
||||
break;
|
||||
case kOutR:
|
||||
self->pOutR = (float*)data;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OPLabsBandChannel::activate(LV2_Handle instance)
|
||||
{
|
||||
(void)instance;
|
||||
// No-op for now — buffer allocations go here when DSP is added
|
||||
}
|
||||
|
||||
void OPLabsBandChannel::run(LV2_Handle instance, uint32_t nSamples)
|
||||
{
|
||||
OPLabsBandChannel* self = (OPLabsBandChannel*)instance;
|
||||
|
||||
// Read control values (with caching to detect changes)
|
||||
if (self->pVolume) self->cachedVolume = *self->pVolume;
|
||||
if (self->pPan) self->cachedPan = *self->pPan;
|
||||
if (self->pMute) self->cachedMute = (*self->pMute > 0.5f);
|
||||
if (self->pSolo) self->cachedSolo = (*self->pSolo > 0.5f);
|
||||
if (self->pInstrument) self->cachedInstrument = (int)(*self->pInstrument + 0.5f);
|
||||
|
||||
const float* inL = self->pInL ? self->pInL : nullptr;
|
||||
const float* inR = self->pInR ? self->pInR : nullptr;
|
||||
float* outL = self->pOutL ? self->pOutL : nullptr;
|
||||
float* outR = self->pOutR ? self->pOutR : nullptr;
|
||||
|
||||
// If muted, output silence and update meters
|
||||
if (self->cachedMute) {
|
||||
if (outL) std::memset(outL, 0, nSamples * sizeof(float));
|
||||
if (outR) std::memset(outR, 0, nSamples * sizeof(float));
|
||||
if (self->pLevelL) *self->pLevelL = -60.0f;
|
||||
if (self->pLevelR) *self->pLevelR = -60.0f;
|
||||
self->peakL = -60.0f;
|
||||
self->peakR = -60.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute linear gain from dB
|
||||
float linearGain = dBToGain(self->cachedVolume);
|
||||
|
||||
// Compute pan coefficients (equal power)
|
||||
// pan = -1 → full left, 0 → center, +1 → full right
|
||||
float panL = 1.0f;
|
||||
float panR = 1.0f;
|
||||
if (self->cachedPan <= 0.0f) {
|
||||
// Pan left: reduce right
|
||||
panR = 1.0f + self->cachedPan; // cachedPan is [-1..0], so panR is [0..1]
|
||||
} else {
|
||||
// Pan right: reduce left
|
||||
panL = 1.0f - self->cachedPan; // cachedPan is [0..1], so panL is [0..1]
|
||||
}
|
||||
|
||||
// Process audio samples
|
||||
float newPeakL = -60.0f;
|
||||
float newPeakR = -60.0f;
|
||||
|
||||
for (uint32_t s = 0; s < nSamples; ++s) {
|
||||
float sampleL = inL ? inL[s] : 0.0f;
|
||||
float sampleR = inR ? inR[s] : 0.0f;
|
||||
|
||||
// Apply gain
|
||||
sampleL *= linearGain;
|
||||
sampleR *= linearGain;
|
||||
|
||||
// Apply pan — stereo balance pan
|
||||
// (stereo pan: independent per-channel would be different, but
|
||||
// this is the standard "balance" pan for a stereo channel strip)
|
||||
if (outL) outL[s] = sampleL * panL;
|
||||
if (outR) outR[s] = sampleR * panR;
|
||||
|
||||
// Track peaks
|
||||
float absL = std::fabs(outL ? outL[s] : 0.0f);
|
||||
float absR = std::fabs(outR ? outR[s] : 0.0f);
|
||||
if (absL > newPeakL) newPeakL = absL;
|
||||
if (absR > newPeakR) newPeakR = absR;
|
||||
}
|
||||
|
||||
// Convert peak amplitude to dB
|
||||
self->peakL = (newPeakL > 0.0f) ? (20.0f * std::log10(newPeakL)) : -60.0f;
|
||||
self->peakR = (newPeakR > 0.0f) ? (20.0f * std::log10(newPeakR)) : -60.0f;
|
||||
|
||||
// Clamp to -60 dB minimum for meter display
|
||||
if (self->peakL < -60.0f) self->peakL = -60.0f;
|
||||
if (self->peakR < -60.0f) self->peakR = -60.0f;
|
||||
|
||||
// Write meter outputs
|
||||
if (self->pLevelL) *self->pLevelL = self->peakL;
|
||||
if (self->pLevelR) *self->pLevelR = self->peakR;
|
||||
}
|
||||
|
||||
void OPLabsBandChannel::deactivate(LV2_Handle instance)
|
||||
{
|
||||
(void)instance;
|
||||
}
|
||||
|
||||
void OPLabsBandChannel::cleanup(LV2_Handle instance)
|
||||
{
|
||||
delete (OPLabsBandChannel*)instance;
|
||||
}
|
||||
|
||||
const void* OPLabsBandChannel::extensionData(const char* uri)
|
||||
{
|
||||
if (strcmp(uri, LV2_STATE__interface) == 0) {
|
||||
return &sStateInterface;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// State save/restore
|
||||
// ====================================================================
|
||||
|
||||
LV2_State_Status OPLabsBandChannel::saveState(
|
||||
LV2_Handle instance,
|
||||
LV2_State_Store_Function store,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)flags;
|
||||
(void)features;
|
||||
OPLabsBandChannel* self = (OPLabsBandChannel*)instance;
|
||||
|
||||
// Save instrument selection
|
||||
if (self->urid__stateInstrument) {
|
||||
int32_t instr = self->cachedInstrument;
|
||||
store(handle,
|
||||
self->urid__stateInstrument,
|
||||
&instr,
|
||||
sizeof(int32_t),
|
||||
self->uridMap->map(self->uridMap->handle, LV2_ATOM__Int),
|
||||
LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);
|
||||
}
|
||||
|
||||
return LV2_STATE_SUCCESS;
|
||||
}
|
||||
|
||||
LV2_State_Status OPLabsBandChannel::restoreState(
|
||||
LV2_Handle instance,
|
||||
LV2_State_Retrieve_Function retrieve,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features)
|
||||
{
|
||||
(void)flags;
|
||||
(void)features;
|
||||
OPLabsBandChannel* self = (OPLabsBandChannel*)instance;
|
||||
|
||||
// Restore instrument selection
|
||||
if (self->urid__stateInstrument) {
|
||||
size_t size = 0;
|
||||
uint32_t type = 0;
|
||||
uint32_t valFlags = 0;
|
||||
const void* data = retrieve(handle,
|
||||
self->urid__stateInstrument,
|
||||
&size, &type, &valFlags);
|
||||
if (data && size == sizeof(int32_t)) {
|
||||
int32_t instr = *(const int32_t*)data;
|
||||
if (instr >= 0 && instr <= 4) {
|
||||
self->cachedInstrument = instr;
|
||||
// Update the control port if connected
|
||||
if (self->pInstrument) {
|
||||
*(const_cast<float*>(self->pInstrument)) = (float)instr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LV2_STATE_SUCCESS;
|
||||
}
|
||||
|
||||
float OPLabsBandChannel::computePeakDB(const float* buffer,
|
||||
uint32_t nSamples,
|
||||
float currentPeak)
|
||||
{
|
||||
float peak = currentPeak;
|
||||
for (uint32_t s = 0; s < nSamples; ++s) {
|
||||
float absVal = std::fabs(buffer[s]);
|
||||
if (absVal > peak) peak = absVal;
|
||||
}
|
||||
// Peak-to-decibel conversion happens in the caller;
|
||||
// keeping this helper for future use
|
||||
return peak;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
#ifndef OPLABSBANDCHANNEL_HPP
|
||||
#define OPLABSBANDCHANNEL_HPP
|
||||
|
||||
#include <lv2/core/lv2.h>
|
||||
#include <lv2/state/state.h>
|
||||
#include <lv2/urid/urid.h>
|
||||
#include <lv2/atom/atom.h>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
/**
|
||||
* OPLabsBandChannel — Stereo channel strip LV2 plugin.
|
||||
*
|
||||
* Provides volume, pan, mute, solo, and instrument type controls
|
||||
* with peak level metering. Compatible with PiPedal and any LV2 host.
|
||||
*
|
||||
* URI: http://ourpad.casa/plugins/oplabs-band-channel#
|
||||
*/
|
||||
class OPLabsBandChannel {
|
||||
public:
|
||||
/** Plugin URI constant. */
|
||||
static constexpr const char* URI = "http://ourpad.casa/plugins/oplabs-band-channel#";
|
||||
|
||||
/** Port index enum — must match OPLabsBandChannel.ttl lv2:index values exactly. */
|
||||
enum Ports : uint32_t {
|
||||
kVolume = 0, // Control input, -60..+6 dB, default 0
|
||||
kPan = 1, // Control input, -1..+1, default 0
|
||||
kMute = 2, // Control input, toggled 0/1
|
||||
kSolo = 3, // Control input, toggled 0/1
|
||||
kInstrument = 4, // Control input, enumeration 0..4
|
||||
kLevelL = 5, // Control output, -60..0 dB
|
||||
kLevelR = 6, // Control output, -60..0 dB
|
||||
kInL = 7, // Audio input, left
|
||||
kInR = 8, // Audio input, right
|
||||
kOutL = 9, // Audio output, left
|
||||
kOutR = 10, // Audio output, right
|
||||
kNumPorts = 11
|
||||
};
|
||||
|
||||
// ---- LV2 descriptor callbacks (static) ----
|
||||
|
||||
static LV2_Handle instantiate(const LV2_Descriptor* descriptor,
|
||||
double sampleRate,
|
||||
const char* bundlePath,
|
||||
const LV2_Feature* const* features);
|
||||
|
||||
static void connectPort(LV2_Handle instance, uint32_t port, void* data);
|
||||
|
||||
static void activate(LV2_Handle instance);
|
||||
|
||||
static void run(LV2_Handle instance, uint32_t nSamples);
|
||||
|
||||
static void deactivate(LV2_Handle instance);
|
||||
|
||||
static void cleanup(LV2_Handle instance);
|
||||
|
||||
static const void* extensionData(const char* uri);
|
||||
|
||||
// ---- State extension callbacks (static) ----
|
||||
|
||||
static LV2_State_Status saveState(LV2_Handle instance,
|
||||
LV2_State_Store_Function store,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features);
|
||||
|
||||
static LV2_State_Status restoreState(LV2_Handle instance,
|
||||
LV2_State_Retrieve_Function retrieve,
|
||||
LV2_State_Handle handle,
|
||||
uint32_t flags,
|
||||
const LV2_Feature* const* features);
|
||||
|
||||
private:
|
||||
OPLabsBandChannel(double sampleRate, const LV2_Feature* const* features);
|
||||
~OPLabsBandChannel() = default;
|
||||
|
||||
// Non-copyable, non-movable
|
||||
OPLabsBandChannel(const OPLabsBandChannel&) = delete;
|
||||
OPLabsBandChannel& operator=(const OPLabsBandChannel&) = delete;
|
||||
|
||||
/** Initialize URID mapping from features. */
|
||||
bool initUrids(const LV2_Feature* const* features);
|
||||
|
||||
/** Convert dB to linear gain. */
|
||||
static inline float dBToGain(float dB) {
|
||||
return std::pow(10.0f, dB / 20.0f);
|
||||
}
|
||||
|
||||
/** Compute peak level in dB from a buffer. Returns -60 dB for silence. */
|
||||
static float computePeakDB(const float* buffer, uint32_t nSamples, float currentPeak);
|
||||
|
||||
// ---- Instance data ----
|
||||
|
||||
// URID map feature (provided by host)
|
||||
LV2_URID_Map* uridMap = nullptr;
|
||||
|
||||
// State keys (mapped URIDs)
|
||||
LV2_URID urid__stateInstrument = 0;
|
||||
|
||||
// Port data pointers (set by connectPort)
|
||||
const float* pVolume = nullptr;
|
||||
const float* pPan = nullptr;
|
||||
const float* pMute = nullptr;
|
||||
const float* pSolo = nullptr;
|
||||
const float* pInstrument = nullptr;
|
||||
float* pLevelL = nullptr;
|
||||
float* pLevelR = nullptr;
|
||||
const float* pInL = nullptr;
|
||||
const float* pInR = nullptr;
|
||||
float* pOutL = nullptr;
|
||||
float* pOutR = nullptr;
|
||||
|
||||
// Cached parameter values
|
||||
float cachedVolume = 0.0f;
|
||||
float cachedPan = 0.0f;
|
||||
bool cachedMute = false;
|
||||
bool cachedSolo = false;
|
||||
int cachedInstrument = 0;
|
||||
|
||||
// Peak meters (smoothed)
|
||||
float peakL = -60.0f;
|
||||
float peakR = -60.0f;
|
||||
};
|
||||
|
||||
#endif // OPLABSBANDCHANNEL_HPP
|
||||
@@ -0,0 +1,6 @@
|
||||
VERS_1.0 {
|
||||
global:
|
||||
lv2_descriptor;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
Reference in New Issue
Block a user