Add a VUMeter node.

time
Ben Niemann 2020-02-22 06:57:47 +01:00
parent d34f92e08e
commit cb040f3686
11 changed files with 448 additions and 0 deletions

View File

@ -43,6 +43,7 @@ from .metronome.node_description import MetronomeDescription
from .midi_velocity_mapper.node_description import MidiVelocityMapperDescription
from .cv_mapper.node_description import CVMapperDescription
from .oscilloscope.node_description import OscilloscopeDescription
from .vumeter.node_description import VUMeterDescription
def node_descriptions() -> Iterator[node_db.NodeDescription]:
@ -66,3 +67,4 @@ def node_descriptions() -> Iterator[node_db.NodeDescription]:
yield MidiVelocityMapperDescription
yield CVMapperDescription
yield OscilloscopeDescription
yield VUMeterDescription

View File

@ -39,6 +39,7 @@
#include "noisicaa/builtin_nodes/midi_velocity_mapper/processor.h"
#include "noisicaa/builtin_nodes/cv_mapper/processor.h"
#include "noisicaa/builtin_nodes/oscilloscope/processor.h"
#include "noisicaa/builtin_nodes/vumeter/processor.h"
namespace noisicaa {
@ -101,6 +102,9 @@ StatusOr<Processor*> create_processor(
} else if (desc.processor().type() == "builtin://oscilloscope") {
assert(desc.type() == pb::NodeDescription::PROCESSOR);
return new ProcessorOscilloscope(realm_name, node_id, host_system, desc);
} else if (desc.processor().type() == "builtin://vumeter") {
assert(desc.type() == pb::NodeDescription::PROCESSOR);
return new ProcessorVUMeter(realm_name, node_id, host_system, desc);
}
return ERROR_STATUS("Invalid processor type %s", desc.processor().type().c_str());

View File

@ -45,6 +45,7 @@ from .metronome.node_ui import MetronomeNode
from .midi_velocity_mapper.node_ui import MidiVelocityMapperNode
from .cv_mapper.node_ui import CVMapperNode
from .oscilloscope.node_ui import OscilloscopeNode
from .vumeter.node_ui import VUMeterNode
if typing.TYPE_CHECKING:
from noisicaa.ui.graph import base_node
@ -69,6 +70,7 @@ node_ui_cls_map = {
'builtin://midi-velocity-mapper': MidiVelocityMapperNode,
'builtin://cv-mapper': CVMapperNode,
'builtin://oscilloscope': OscilloscopeNode,
'builtin://vumeter': VUMeterNode,
} # type: Dict[str, Type[base_node.Node]]
track_editor_cls_map = {

View File

@ -0,0 +1,21 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license

View File

@ -0,0 +1,49 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
from noisicaa import node_db
VUMeterDescription = node_db.NodeDescription(
uri='builtin://vumeter',
display_name='VU Meter',
type=node_db.NodeDescription.PROCESSOR,
node_ui=node_db.NodeUIDescription(
type='builtin://vumeter',
),
builtin_icon='node-type-builtin',
processor=node_db.ProcessorDescription(
type='builtin://vumeter',
),
ports=[
node_db.PortDescription(
name='in:left',
direction=node_db.PortDescription.INPUT,
types=[node_db.PortDescription.AUDIO],
),
node_db.PortDescription(
name='in:right',
direction=node_db.PortDescription.INPUT,
types=[node_db.PortDescription.AUDIO],
),
]
)

View File

@ -0,0 +1,93 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import logging
from typing import Any, Dict
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import core
from noisicaa import music
from noisicaa.ui import ui_base
from noisicaa.ui import vumeter
from noisicaa.ui.graph import base_node
logger = logging.getLogger(__name__)
class VUMeterNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QWidget):
def __init__(self, node: music.BaseNode, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__node = node
self.__vu_meter = vumeter.VUMeter(self)
self.__meter_urid = self.app.urid_mapper.map(
'http://noisicaa.odahoda.de/lv2/processor_vumeter#meter')
listener = self.audioproc_client.node_messages.add(
'%016x' % self.__node.id, self.__nodeMessage)
self.add_cleanup_function(listener.remove)
self.setMinimumSize(QtCore.QSize(10, 10))
self.__current_orientation = None # type: Qt.Orientation
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.__vu_meter)
self.setLayout(layout)
def __nodeMessage(self, msg: Dict[str, Any]) -> None:
meter = 'http://noisicaa.odahoda.de/lv2/processor_vumeter#meter'
if meter in msg:
current_left, peak_left, current_right, peak_right = msg[meter]
self.__vu_meter.setLeftValue(current_left)
self.__vu_meter.setLeftPeak(peak_left)
self.__vu_meter.setRightValue(current_right)
self.__vu_meter.setRightPeak(peak_right)
def resizeEvent(self, evt: QtGui.QResizeEvent) -> None:
super().resizeEvent(evt)
w, h = self.width(), self.height()
if w > h:
orientation = Qt.Horizontal
else:
orientation = Qt.Vertical
if orientation == self.__current_orientation:
return
self.__vu_meter.setOrientation(orientation)
self.__current_orientation = orientation
class VUMeterNode(base_node.Node):
def createBodyWidget(self) -> QtWidgets.QWidget:
widget = VUMeterNodeWidget(node=self.node(), context=self.context)
self.add_cleanup_function(widget.cleanup)
return widget

View File

@ -0,0 +1,137 @@
/*
* @begin:license
*
* Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @end:license
*/
#include <math.h>
#include "lv2/lv2plug.in/ns/ext/atom/forge.h"
#include "noisicaa/host_system/host_system.h"
#include "noisicaa/audioproc/public/processor_message.pb.h"
#include "noisicaa/audioproc/engine/misc.h"
#include "noisicaa/audioproc/engine/message_queue.h"
#include "noisicaa/builtin_nodes/vumeter/processor.h"
namespace noisicaa {
static const float min_db = -70.0f;
static const float max_db = 20.0f;
ProcessorVUMeter::ProcessorVUMeter(
const string& realm_name, const string& node_id, HostSystem *host_system,
const pb::NodeDescription& desc)
: Processor(
realm_name, node_id, "noisicaa.audioproc.engine.processor.vumeter", host_system, desc) {}
Status ProcessorVUMeter::setup_internal() {
RETURN_IF_ERROR(Processor::setup_internal());
_meter_urid = _host_system->lv2->map(
"http://noisicaa.odahoda.de/lv2/processor_vumeter#meter");
_window_size = min(
(uint32_t)(0.05 * _host_system->sample_rate()), // 50ms
_host_system->sample_rate());
_history_pos = 0;
_peak_decay = 20 / (0.4 * _host_system->sample_rate());
for (int ch = 0 ; ch < 2 ; ++ch) {
_history[ch].reset(new float[_window_size]);
for (uint32_t i = 0 ; i < _window_size ; ++i) {
_history[ch].get()[i] = min_db;
}
_peak_hold[ch] = 0;
_peak[ch] = min_db;
}
return Status::Ok();
}
void ProcessorVUMeter::cleanup_internal() {
for (int ch = 0 ; ch < 2 ; ++ch) {
_history[ch].reset();
}
Processor::cleanup_internal();
}
Status ProcessorVUMeter::process_block_internal(BlockContext* ctxt, TimeMapper* time_mapper) {
static const int LEFT = 0;
static const int RIGHT = 1;
float* buf[2] = {
(float*)_buffers[LEFT]->data(),
(float*)_buffers[RIGHT]->data()
};
for (uint32_t i = 0 ; i < _host_system->block_size() ; ++i) {
for (int ch = 0 ; ch < 2 ; ++ch) {
float value = logf(fabsf(*(buf[ch]))) / 0.11512925f;
value = max(min_db, min(value, max_db));
_history[ch].get()[_history_pos] = value;
if (value > _peak[ch]) {
_peak_hold[ch] = int(0.5 * _host_system->sample_rate());
_peak[ch] = value;
} else if (_peak_hold[ch] == 0) {
_peak[ch] = max(min_db, _peak[ch] - _peak_decay);
} else {
--_peak_hold[ch];
}
++buf[ch];
}
_history_pos = (_history_pos + 1) % _window_size;
}
float current[2] = { min_db, min_db };
for (uint32_t i = 0 ; i < _window_size ; ++i) {
for (int ch = 0 ; ch < 2 ; ++ch) {
current[ch] = max(current[ch], _history[ch].get()[i]);
}
}
uint8_t atom[200];
LV2_Atom_Forge forge;
lv2_atom_forge_init(&forge, &_host_system->lv2->urid_map);
lv2_atom_forge_set_buffer(&forge, atom, sizeof(atom));
LV2_Atom_Forge_Frame oframe;
lv2_atom_forge_object(&forge, &oframe, _host_system->lv2->urid.core_nodemsg, 0);
lv2_atom_forge_key(&forge, _meter_urid);
LV2_Atom_Forge_Frame tframe;
lv2_atom_forge_tuple(&forge, &tframe);
for (int ch = 0 ; ch < 2 ; ++ch) {
lv2_atom_forge_float(&forge, current[ch]);
lv2_atom_forge_float(&forge, _peak[ch]);
}
lv2_atom_forge_pop(&forge, &tframe);
lv2_atom_forge_pop(&forge, &oframe);
NodeMessage::push(ctxt->out_messages, _node_id, (LV2_Atom*)atom);
return Status::Ok();
}
}

View File

@ -0,0 +1,66 @@
// -*- mode: c++ -*-
/*
* @begin:license
*
* Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @end:license
*/
#ifndef _NOISICAA_BUILTIN_NODES_VUMETER_PROCESSOR_H
#define _NOISICAA_BUILTIN_NODES_VUMETER_PROCESSOR_H
#include <memory>
#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
#include "noisicaa/core/status.h"
#include "noisicaa/audioproc/engine/processor.h"
namespace noisicaa {
using namespace std;
class HostSystem;
class ProcessorVUMeter : public Processor {
public:
ProcessorVUMeter(
const string& realm_name, const string& node_id, HostSystem* host_system,
const pb::NodeDescription& desc);
protected:
Status setup_internal() override;
void cleanup_internal() override;
Status process_block_internal(BlockContext* ctxt, TimeMapper* time_mapper) override;
private:
LV2_URID _meter_urid;
size_t _window_size;
uint32_t _history_pos;
unique_ptr<float> _history[2];
unique_ptr<float> _history_right;
float _peak_decay;
uint32_t _peak_hold[2];
float _peak[2];
};
} // namespace noisicaa
#endif

View File

@ -0,0 +1,33 @@
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
from noisidev import unittest
from noisidev import unittest_processor_mixins
class ProcessorVUMeterTest(
unittest_processor_mixins.ProcessorTestMixin,
unittest.TestCase):
def test_process_block(self):
self.node_description = self.node_db['builtin://vumeter']
self.create_processor()
self.fill_buffer('in:left', 1.0)
self.fill_buffer('in:right', -1.0)
self.process_block()

View File

@ -0,0 +1,39 @@
# -*- mode: python -*-
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
def build(ctx):
ctx.py_module('__init__.py')
ctx.py_module('node_description.py')
ctx.py_module('node_ui.py')
ctx.py_test('processor_test.py')
ctx.shlib(
target='noisicaa-builtin_nodes-vumeter-processor',
features=['cxxshlib'],
source=[
ctx.cpp_module('processor.cpp'),
],
use=[
'noisicaa-audioproc-public',
'noisicaa-host_system',
]
)

View File

@ -69,6 +69,7 @@ def build(ctx):
'noisicaa-builtin_nodes-midi_velocity_mapper-processor',
'noisicaa-builtin_nodes-cv_mapper-processor',
'noisicaa-builtin_nodes-oscilloscope-processor',
'noisicaa-builtin_nodes-vumeter-processor',
],
)
@ -93,3 +94,4 @@ def build(ctx):
ctx.recurse('midi_velocity_mapper')
ctx.recurse('cv_mapper')
ctx.recurse('oscilloscope')
ctx.recurse('vumeter')