Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
|
f94e2e99ab | 3 years ago |
|
b57a212144 | 3 years ago |
|
e567b2330e | 3 years ago |
|
4d0c2df377 | 3 years ago |
|
dec49c3db4 | 3 years ago |
|
f8162bbb7e | 3 years ago |
|
cb040f3686 | 3 years ago |
|
d34f92e08e | 3 years ago |
|
c08ca2140c | 3 years ago |
|
9e298f3966 | 3 years ago |
|
e9518d6635 | 3 years ago |
|
f637bddb56 | 3 years ago |
|
d87f862581 | 3 years ago |
|
402191ffec | 3 years ago |
|
0489093fdf | 3 years ago |
|
8eea318e98 | 3 years ago |
@ -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
|
@ -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],
|
||||
),
|
||||
]
|
||||
)
|
@ -0,0 +1,94 @@
|
||||
#!/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.setContentsMargins(0, 0, 0, 0)
|
||||
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
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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()
|
@ -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',
|
||||
]
|
||||
)
|
@ -0,0 +1,137 @@
|
||||
#!/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 enum
|
||||
import logging
|
||||
import math
|
||||
import typing
|
||||
from typing import Any, List
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import audioproc
|
||||
from . import slots
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from noisicaa import core
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EngineState(slots.SlotContainer, QtCore.QObject):
|
||||
class State(enum.IntEnum):
|
||||
Setup = 0
|
||||
Cleanup = 1
|
||||
Running = 2
|
||||
Stopped = 3
|
||||
|
||||
state, setState, stateChanged = slots.slot(
|
||||
State, 'state', default=State.Stopped)
|
||||
currentLoad, setCurrentLoad, currentLoadChanged = slots.slot(
|
||||
float, 'currentLoad', default=0.0)
|
||||
loadHistoryChanged = QtCore.pyqtSignal()
|
||||
|
||||
HISTORY_LENGTH = 1000
|
||||
|
||||
def __init__(self, parent: QtCore.QObject, **kwargs: Any) -> None:
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self.__history = [None] * self.HISTORY_LENGTH # type: List[float]
|
||||
self.__latest_values = [] # type: List[float]
|
||||
self.currentLoadChanged.connect(self.__latest_values.append)
|
||||
|
||||
self.__timer = QtCore.QTimer(self)
|
||||
self.__timer.setInterval(1000 // 25)
|
||||
self.__timer.timeout.connect(self.__updateHistory)
|
||||
self.__timer.start()
|
||||
|
||||
def updateState(self, msg: audioproc.EngineStateChange) -> None:
|
||||
self.setState({
|
||||
audioproc.EngineStateChange.SETUP: self.State.Setup,
|
||||
audioproc.EngineStateChange.CLEANUP: self.State.Cleanup,
|
||||
audioproc.EngineStateChange.RUNNING: self.State.Running,
|
||||
audioproc.EngineStateChange.STOPPED: self.State.Stopped,
|
||||
}[msg.state])
|
||||
|
||||
if msg.state == audioproc.EngineStateChange.RUNNING and msg.HasField('load'):
|
||||
self.setCurrentLoad(msg.load)
|
||||
|
||||
def loadHistory(self, num_ticks: int) -> List[float]:
|
||||
num_ticks = min(num_ticks, self.HISTORY_LENGTH)
|
||||
return self.__history[-num_ticks:]
|
||||
|
||||
def __updateHistory(self) -> None:
|
||||
if self.__latest_values:
|
||||
self.__history.append(max(self.__latest_values))
|
||||
self.__latest_values.clear()
|
||||
elif self.state() == self.State.Running:
|
||||
self.__history.append(self.currentLoad())
|
||||
else:
|
||||
self.__history.append(None)
|
||||
if len(self.__history) > self.HISTORY_LENGTH:
|
||||
del self.__history[:-self.HISTORY_LENGTH]
|
||||
self.loadHistoryChanged.emit()
|
||||
|
||||
|
||||
class LoadHistory(QtWidgets.QWidget):
|
||||
def __init__(self, parent: QtWidgets.QWidget, engine_state: EngineState) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.__font = QtGui.QFont(self.font())
|
||||
self.__font.setPixelSize(12)
|
||||
|
||||
self.__engine_state = engine_state
|
||||
self.__engine_state.loadHistoryChanged.connect(self.update)
|
||||
|
||||
def paintEvent(self, evt: QtGui.QPaintEvent) -> None:
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
painter.fillRect(self.rect(), QtGui.QColor(0, 0, 0))
|
||||
|
||||
history = self.__engine_state.loadHistory(max(25, self.width() // 2))
|
||||
x = self.width() - 2
|
||||
for value in reversed(history):
|
||||
if value is not None:
|
||||
value = max(0.0, min(value, 1.0))
|
||||
vh = int(self.height() * value)
|
||||
painter.fillRect(
|
||||
x, self.height() - vh, 2, vh,
|
||||
QtGui.QColor(int(255 * value), 255 - int(255 * value), 0))
|
||||
x -= 2
|
||||
|
||||
if self.width() > 50 and self.height() > 16:
|
||||
last_second = [v for v in history[-25:] if v is not None]
|
||||
if len(last_second) > 5:
|
||||
avg = sum(last_second) / len(last_second)
|
||||
stddev = math.sqrt(sum((v - avg) ** 2 for v in last_second) / len(last_second))
|
||||
|
||||
painter.setPen(Qt.white)
|
||||
painter.setFont(self.__font)
|
||||
painter.drawText(
|
||||
4, 1, self.width() - 4, self.height() - 1,
|
||||
Qt.AlignTop,
|
||||
"%d\u00b1%d%%" % (100 * avg, 100 * stddev))
|
||||
|
||||
return super().paintEvent(evt)
|
@ -1,79 +0,0 @@
|
||||
#!/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 math
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
|
||||
|
||||
class LoadHistoryWidget(QtWidgets.QWidget):
|
||||
def __init__(self, width, height):
|
||||
super().__init__()
|
||||
|
||||
self.__width = width
|
||||
self.__height = height
|
||||
self.setFixedSize(self.__width, self.__height)
|
||||
|
||||
self.__pixmap = QtGui.QPixmap(self.__width, self.__height)
|
||||
self.__pixmap.fill(Qt.black)
|
||||
|
||||
self.__history = [] # type: List[float]
|
||||
|
||||
self.__font = QtGui.QFont("Helvetica")
|
||||
self.__font.setPixelSize(12)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.drawPixmap(0, 0, self.__pixmap)
|
||||
|
||||
if len(self.__history) > 5:
|
||||
avg = sum(self.__history) / len(self.__history)
|
||||
stddev = math.sqrt(sum((v - avg) ** 2 for v in self.__history) / len(self.__history))
|
||||
|
||||
painter.setPen(Qt.white)
|
||||
painter.setFont(self.__font)
|
||||
painter.drawText(
|
||||
4, 1, self.__width - 4, self.__height - 1,
|
||||
Qt.AlignTop,
|
||||
"%d\u00b1%d%%" % (avg, stddev))
|
||||
|
||||
return super().paintEvent(event)
|
||||
|
||||
def addValue(self, value):
|
||||
value = max(0, min(value, 1))
|
||||
vh = int(self.__height * value)
|
||||
|
||||
self.__pixmap.scroll(-2, 0, 0, 0, self.__width, self.__height)
|
||||
painter = QtGui.QPainter(self.__pixmap)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.setBrush(Qt.black)
|
||||
painter.drawRect(self.__width - 2, 0, 2, self.__height)
|
||||
painter.setBrush(QtGui.QColor(int(255 * value), 255 - int(255 * value), 0))
|
||||
painter.drawRect(self.__width - 2, self.__height - vh, 2, vh)
|
||||
|
||||
self.__history.append(100 * value)
|
||||
if len(self.__history) > 50:
|
||||
del self.__history[:-50]
|
||||
|
||||
self.update()
|