diff options
| author | Carl Hetherington <cth@carlh.net> | 2025-04-17 02:09:09 +0200 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2025-04-28 02:31:15 +0200 |
| commit | 011ebbfad404991fb0619939fd0fff262588cb02 (patch) | |
| tree | 49c47086494ba1fb21a432ab9ab2e70e3f8885ab | |
| parent | bd08f1845d8ed5e171f4725a3c9d0a23a9c19f77 (diff) | |
Add non-OpenGL "simple" audio meters.
| -rw-r--r-- | src/lib/audio_level_calculator.cc | 98 | ||||
| -rw-r--r-- | src/lib/audio_level_calculator.h | 68 | ||||
| -rw-r--r-- | src/lib/audio_levels.h | 45 | ||||
| -rw-r--r-- | src/lib/butler.cc | 29 | ||||
| -rw-r--r-- | src/lib/butler.h | 9 | ||||
| -rw-r--r-- | src/lib/wscript | 1 | ||||
| -rw-r--r-- | src/tools/dcpomatic_player.cc | 8 | ||||
| -rw-r--r-- | src/wx/film_viewer.cc | 28 | ||||
| -rw-r--r-- | src/wx/film_viewer.h | 4 | ||||
| -rw-r--r-- | src/wx/meter_util.cc | 88 | ||||
| -rw-r--r-- | src/wx/meter_util.h | 41 | ||||
| -rw-r--r-- | src/wx/meters_dialog.cc | 44 | ||||
| -rw-r--r-- | src/wx/meters_dialog.h | 40 | ||||
| -rw-r--r-- | src/wx/simple_meters.cc | 136 | ||||
| -rw-r--r-- | src/wx/simple_meters.h | 65 | ||||
| -rw-r--r-- | src/wx/simple_video_view.cc | 3 | ||||
| -rw-r--r-- | src/wx/simple_video_view.h | 7 | ||||
| -rw-r--r-- | src/wx/text_size_cache.cc | 47 | ||||
| -rw-r--r-- | src/wx/text_size_cache.h | 40 | ||||
| -rw-r--r-- | src/wx/wscript | 4 | ||||
| -rw-r--r-- | test/audio_level_calculator_test.cc | 46 | ||||
| -rw-r--r-- | test/wscript | 1 |
22 files changed, 850 insertions, 2 deletions
diff --git a/src/lib/audio_level_calculator.cc b/src/lib/audio_level_calculator.cc new file mode 100644 index 000000000..f347ffba6 --- /dev/null +++ b/src/lib/audio_level_calculator.cc @@ -0,0 +1,98 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "audio_buffers.h" +#include "audio_level_calculator.h" + + +using std::make_pair; +using std::shared_ptr; +using boost::optional; + + +AudioLevelCalculator::AudioLevelCalculator(int calculation_frame_rate, int falloff) + : _calculation_frame_rate(calculation_frame_rate) + , _falloff_linear(pow(10, -falloff / (calculation_frame_rate * 20.0f))) +{ + +} + + +void +AudioLevelCalculator::put(shared_ptr<const AudioBuffers> audio, dcpomatic::DCPTime time, int frame_rate) +{ + int const frames_per_measurement = frame_rate / _calculation_frame_rate; + + boost::mutex::scoped_lock lm(_current_mutex); + + auto const channels = audio->channels(); + + if (static_cast<int>(_current_peaks.size()) != channels) { + _current_peaks.resize(channels); + } + + auto const data = audio->data(); + auto const frames = audio->frames(); + for (auto frame = 0; frame < frames; ++frame) { + for (auto channel = 0; channel < channels; ++channel) { + _current_peaks[channel] = std::max(std::abs(data[channel][frame]), _current_peaks[channel]); + } + + ++_current_frames; + if (_current_frames == frames_per_measurement) { + { + boost::mutex::scoped_lock lm(_store_mutex); + _levels.emplace_back(time + dcpomatic::DCPTime::from_frames(frame + 1, frame_rate), _current_peaks); + } + for (auto channel = 0; channel < channels; ++channel) { + _current_peaks[channel] *= _falloff_linear; + } + _current_frames = 0; + } + } + +} + + +void +AudioLevelCalculator::clear() +{ + boost::mutex::scoped_lock slm(_store_mutex); + _levels.clear(); + + boost::mutex::scoped_lock clm(_current_mutex); + std::fill(_current_peaks.begin(), _current_peaks.end(), 0.0f); + _current_frames = 0; +} + + +boost::optional<AudioLevels> +AudioLevelCalculator::get() +{ + boost::mutex::scoped_lock lm(_store_mutex); + if (_levels.empty()) { + return {}; + } + auto const m = _levels.front(); + _levels.pop_front(); + return m; +} + diff --git a/src/lib/audio_level_calculator.h b/src/lib/audio_level_calculator.h new file mode 100644 index 000000000..c7488e13f --- /dev/null +++ b/src/lib/audio_level_calculator.h @@ -0,0 +1,68 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_LEVEL_CALCULATOR_H +#define DCPOMATIC_LEVEL_CALCULATOR_H + + +#include "audio_levels.h" +#include "dcpomatic_time.h" +#include <boost/thread/mutex.hpp> +#include <memory> + + +class AudioBuffers; + + +class AudioLevelCalculator +{ +public: + /** @param calculation_frame_rate number of peak calculations to make per second + * @param falloff meter falloff in db/sec + */ + AudioLevelCalculator(int calculation_frame_rate = 30, int falloff = 15); + + AudioLevelCalculator(AudioLevelCalculator const&) = delete; + AudioLevelCalculator& operator=(AudioLevelCalculator const&) = delete; + + void put(std::shared_ptr<const AudioBuffers> audio, dcpomatic::DCPTime time, int frame_rate); + void clear(); + boost::optional<AudioLevels> get(); + + dcpomatic::DCPTime time_between_measurements() const { + return dcpomatic::DCPTime::from_seconds(1) / _calculation_frame_rate; + }; + +private: + int const _calculation_frame_rate; + float const _falloff_linear; + + boost::mutex _current_mutex; + std::vector<float> _current_peaks; + int _current_frames = 0; + + mutable boost::mutex _store_mutex; + std::list<AudioLevels> _levels; +}; + + +#endif + diff --git a/src/lib/audio_levels.h b/src/lib/audio_levels.h new file mode 100644 index 000000000..4ab02d19d --- /dev/null +++ b/src/lib/audio_levels.h @@ -0,0 +1,45 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_AUDIO_LEVELS_H +#define DCPOMATIC_AUDIO_LEVELS_H + + +#include "dcpomatic_time.h" +#include <vector> + + +struct AudioLevels +{ + AudioLevels(dcpomatic::DCPTime t, std::vector<float> p) + : time(t) + , peaks(std::move(p)) + { + + } + + dcpomatic::DCPTime time; + std::vector<float> peaks; +}; + + +#endif + diff --git a/src/lib/butler.cc b/src/lib/butler.cc index 14f342045..61772b115 100644 --- a/src/lib/butler.cc +++ b/src/lib/butler.cc @@ -19,6 +19,7 @@ */ +#include "audio_level_calculator.h" #include "butler.h" #include "compose.hpp" #include "cross.h" @@ -33,10 +34,12 @@ using std::cout; using std::function; using std::make_pair; +using std::make_shared; using std::pair; using std::shared_ptr; using std::string; using std::weak_ptr; +using std::vector; using boost::bind; using boost::optional; using namespace dcpomatic; @@ -88,6 +91,7 @@ Butler::Butler( , _alignment(alignment) , _fast(fast) , _prepare_only_proxy(prepare_only_proxy) + , _enable_audio_level_calculation(false) { _player_video_connection = _player.Video.connect(bind(&Butler::video, this, _1, _2)); _player_audio_connection = _player.Audio.connect(bind(&Butler::audio, this, _1, _2, _3)); @@ -312,6 +316,7 @@ Butler::seek_unlocked(DCPTime position, bool accurate) _video.clear(); _audio.clear(); + _audio_level_calculator.clear(); _closed_caption.clear(); _summon.notify_all(); @@ -371,6 +376,9 @@ Butler::audio(shared_ptr<AudioBuffers> audio, DCPTime time, int frame_rate) } _audio.put(remap(audio, _audio_channels, _audio_mapping), time, frame_rate); + if (_enable_audio_level_calculation) { + _audio_level_calculator.put(audio, time, frame_rate); + } } @@ -481,3 +489,24 @@ Butler::Error::summary() const return ""; } + +void +Butler::enable_audio_level_calculation(bool enable) +{ + _enable_audio_level_calculation = enable; +} + + +dcpomatic::DCPTime +Butler::time_between_audio_level_measurements() const +{ + return _audio_level_calculator.time_between_measurements(); +} + + +boost::optional<AudioLevels> +Butler::get_audio_levels() +{ + return _audio_level_calculator.get(); +} + diff --git a/src/lib/butler.h b/src/lib/butler.h index ad2552769..eb07a8a3e 100644 --- a/src/lib/butler.h +++ b/src/lib/butler.h @@ -23,6 +23,8 @@ #define DCPOMATIC_BUTLER_H +#include "audio_level_calculator.h" +#include "audio_levels.h" #include "audio_mapping.h" #include "audio_ring_buffers.h" #include "change_signaller.h" @@ -92,8 +94,12 @@ public: std::pair<std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime> get_video(Behaviour behaviour, Error* e = nullptr); boost::optional<dcpomatic::DCPTime> get_audio(Behaviour behaviour, float* out, Frame frames); + boost::optional<AudioLevels> get_audio_levels(); boost::optional<TextRingBuffers::Data> get_closed_caption(); + void enable_audio_level_calculation(bool enable); + dcpomatic::DCPTime time_between_audio_level_measurements() const; + std::pair<size_t, std::string> memory_used() const; private: @@ -112,6 +118,7 @@ private: VideoRingBuffers _video; AudioRingBuffers _audio; + AudioLevelCalculator _audio_level_calculator; TextRingBuffers _closed_caption; boost::thread_group _prepare_pool; @@ -154,6 +161,8 @@ private: */ boost::optional<dcpomatic::DCPTime> _awaiting; + boost::atomic<bool> _enable_audio_level_calculation; + boost::signals2::scoped_connection _player_video_connection; boost::signals2::scoped_connection _player_audio_connection; boost::signals2::scoped_connection _player_text_connection; diff --git a/src/lib/wscript b/src/lib/wscript index dafd655fe..a20a2de10 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -38,6 +38,7 @@ sources = """ audio_delay.cc audio_filter.cc audio_filter_graph.cc + audio_level_calculator.cc audio_mapping.cc audio_merger.cc audio_point.cc diff --git a/src/tools/dcpomatic_player.cc b/src/tools/dcpomatic_player.cc index c5242c310..3137acaa3 100644 --- a/src/tools/dcpomatic_player.cc +++ b/src/tools/dcpomatic_player.cc @@ -133,6 +133,7 @@ enum { ID_view_full_screen = DCPOMATIC_MAIN_MENU + 200, ID_view_dual_screen, ID_view_closed_captions, + ID_view_meters, ID_view_eye, ID_view_eye_left, ID_view_eye_right, @@ -241,6 +242,7 @@ public: Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_full_screen, this), ID_view_full_screen); Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_dual_screen, this), ID_view_dual_screen); Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_closed_captions, this), ID_view_closed_captions); + Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_meters, this), ID_view_meters); Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_cpl, this, _1), ID_view_cpl, ID_view_cpl + MAX_CPLS); Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_eye_changed, this, _1), ID_view_eye_left); Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_eye_changed, this, _1), ID_view_eye_right); @@ -644,6 +646,7 @@ private: setup_menu (); view->AppendSeparator(); view->Append(ID_view_closed_captions, _("Closed captions...")); + view->Append(ID_view_meters, _("Audio meters...\tCtrl-L")); _view_eye_menu = new wxMenu; _view_eye_left = _view_eye_menu->AppendRadioItem(ID_view_eye_left, _("Left")); _view_eye_menu->AppendRadioItem(ID_view_eye_right, _("Right")); @@ -989,6 +992,11 @@ private: _viewer.show_closed_captions(); } + void view_meters () + { + _viewer.show_meters(); + } + void tools_verify () { DCPOMATIC_ASSERT(!_film->content().empty()); diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc index b77ff987c..a52f4286e 100644 --- a/src/wx/film_viewer.cc +++ b/src/wx/film_viewer.cc @@ -28,6 +28,7 @@ #include "closed_captions_dialog.h" #include "film_viewer.h" #include "gl_video_view.h" +#include "meters_dialog.h" #include "nag_dialog.h" #include "playhead_to_frame_dialog.h" #include "playhead_to_timecode_dialog.h" @@ -89,6 +90,7 @@ rtaudio_callback(void* out, void *, unsigned int frames, double, RtAudioStreamSt FilmViewer::FilmViewer(wxWindow* p) : _closed_captions_dialog(new ClosedCaptionsDialog(p, this)) + , _meters_dialog(new MetersDialog(p)) { #if wxCHECK_VERSION(3, 1, 0) switch (Config::instance()->video_view_type()) { @@ -96,13 +98,21 @@ FilmViewer::FilmViewer(wxWindow* p) _video_view = std::make_shared<GLVideoView>(this, p); break; case Config::VIDEO_VIEW_SIMPLE: - _video_view = std::make_shared<SimpleVideoView>(this, p); + { + auto simple = std::make_shared<SimpleVideoView>(this, p); + _meters_dialog->set_meters(simple->get_meters()); + _video_view = simple; + } break; } #else - _video_view = std::make_shared<SimpleVideoView>(this, p); + auto simple = std::make_shared<SimpleVideoView>(this, p); + _meters_dialog->set_meters(simple->get_meters()); + _video_view = simple; #endif + _meters_dialog->Bind(wxEVT_SHOW, boost::bind(&FilmViewer::meters_shown, this, _1)); + _video_view->Sized.connect(boost::bind(&FilmViewer::video_view_sized, this)); _video_view->TooManyDropped.connect(boost::bind(boost::ref(TooManyDropped))); @@ -119,6 +129,13 @@ FilmViewer::~FilmViewer() } +void +FilmViewer::meters_shown(wxShowEvent& ev) +{ + _butler->enable_audio_level_calculation(ev.IsShown()); +} + + /** Ask for ::idle_handler() to be called next time we are idle */ void FilmViewer::request_idle_display_next_frame() @@ -808,6 +825,13 @@ FilmViewer::show_closed_captions() void +FilmViewer::show_meters() +{ + _meters_dialog->Show(); +} + + +void FilmViewer::seek_by(DCPTime by, bool accurate) { seek(_video_view->position() + by, accurate); diff --git a/src/wx/film_viewer.h b/src/wx/film_viewer.h index 2a6239d7d..25c99b378 100644 --- a/src/wx/film_viewer.h +++ b/src/wx/film_viewer.h @@ -48,6 +48,7 @@ class FFmpegPlayer; class Image; class Player; class PlayerVideo; +class MetersDialog; class RGBPlusAlphaImage; class wxToggleButton; @@ -71,6 +72,7 @@ public: } void show_closed_captions(); + void show_meters(); void set_film(std::shared_ptr<Film>); std::shared_ptr<Film> film() const { @@ -184,6 +186,7 @@ private: void film_length_change(); void ui_finished(); void start_audio_stream_if_open(); + void meters_shown(wxShowEvent& ev); dcpomatic::DCPTime uncorrected_time() const; @@ -212,6 +215,7 @@ private: Optimisation _optimisation = Optimisation::NONE; ClosedCaptionsDialog* _closed_captions_dialog = nullptr; + MetersDialog* _meters_dialog = nullptr; bool _outline_content = false; boost::optional<dcpomatic::Rect<double>> _outline_subtitles; diff --git a/src/wx/meter_util.cc b/src/wx/meter_util.cc new file mode 100644 index 000000000..4f69219ae --- /dev/null +++ b/src/wx/meter_util.cc @@ -0,0 +1,88 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "meter_util.h" +#include <wx/wx.h> +#include <algorithm> +#include <cmath> + + +using std::pair; +using std::vector; + + +auto constexpr min_db = -70.0f; + +static auto const _scales = std::vector<std::pair<int, wxString>>{ + { 0, wxT("0dB") }, + { -10, wxT("-10dB") }, + { -20, wxT("-20dB") }, + { -30, wxT("-30dB") }, + { -40, wxT("-40dB") }, + { -50, wxT("-50dB") }, + { -60, wxT("-60dB") }, + { -70, wxT("-70dB") }, +}; + + + +int +dcpomatic::meter::db_to_y(int meter_height, float db) +{ + return meter_height - (db - min_db) * meter_height / -min_db; +} + + +float +dcpomatic::meter::linear_level_to_db(float level) +{ + return std::max(min_db, 20 * log10f(level)); + +} + + +vector<pair<int, wxString>> const& +dcpomatic::meter::scales() +{ + return _scales; +} + + +int +dcpomatic::meter::border() +{ + return 16; +} + + +int +dcpomatic::meter::scale_label_width() +{ + return 28; +} + + +int +dcpomatic::meter::channel_label_height() +{ + return 24; +} + diff --git a/src/wx/meter_util.h b/src/wx/meter_util.h new file mode 100644 index 000000000..cdf347e26 --- /dev/null +++ b/src/wx/meter_util.h @@ -0,0 +1,41 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include <wx/wx.h> +#include <utility> +#include <vector> + + +namespace dcpomatic { +namespace meter { + +int border(); +int scale_label_width(); +int channel_label_height(); + +int db_to_y(int meter_height, float db); +float linear_level_to_db(float level); +std::vector<std::pair<int, wxString>> const& scales(); + + +} +} + diff --git a/src/wx/meters_dialog.cc b/src/wx/meters_dialog.cc new file mode 100644 index 000000000..e95fb65f2 --- /dev/null +++ b/src/wx/meters_dialog.cc @@ -0,0 +1,44 @@ +/* + Copyright (C) 2019-2021 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "meters_dialog.h" +#include <wx/wx.h> + + +MetersDialog::MetersDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Meters")) +{ + +} + +void +MetersDialog::set_meters(wxWindow* meters) +{ + meters->Reparent(this); + + auto sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(meters, 1, wxEXPAND); + SetSizer(sizer); + + SetSize({640, 480}); + sizer->Layout(); +} + diff --git a/src/wx/meters_dialog.h b/src/wx/meters_dialog.h new file mode 100644 index 000000000..84e2f0504 --- /dev/null +++ b/src/wx/meters_dialog.h @@ -0,0 +1,40 @@ +/* + Copyright (C) 2019-2021 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "simple_meters.h" +#include "lib/dcpomatic_time.h" +#include <wx/wx.h> +#include <memory> + + +class Butler; +class LevelCalculator; +class wxWindow; + + +class MetersDialog : public wxDialog +{ +public: + MetersDialog(wxWindow* parent); + + void set_meters(wxWindow* meters); +}; + diff --git a/src/wx/simple_meters.cc b/src/wx/simple_meters.cc new file mode 100644 index 000000000..3c0b99969 --- /dev/null +++ b/src/wx/simple_meters.cc @@ -0,0 +1,136 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "meter_util.h" +#include "simple_meters.h" +#include "wx_util.h" +#include "lib/butler.h" +#include "lib/constants.h" +#include "lib/util.h" +#include <dcp/types.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/graphics.h> +LIBDCP_ENABLE_WARNINGS +#include <wx/wx.h> +#include <algorithm> + + +using std::shared_ptr; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif + + +SimpleMeters::SimpleMeters(wxWindow* parent) + : _panel(new wxPanel(parent, wxID_ANY)) +{ + _panel->Bind(wxEVT_PAINT, boost::bind(&SimpleMeters::paint, this)); +} + + +wxWindow* +SimpleMeters::get() const +{ + return _panel; +} + + +void +SimpleMeters::paint() +{ + using namespace dcpomatic::meter; + + wxPaintDC dc(_panel); + + dc.SetBackground(*wxBLACK_BRUSH); + dc.Clear(); + + auto const scale = 1 / dpi_scale_factor(_panel); + auto const panel_size = dcp::Size(_panel->GetSize().GetWidth() / scale, _panel->GetSize().GetHeight() / scale); + auto const meter_height = panel_size.height - channel_label_height() - 2 * border(); + dc.SetLogicalScale(scale, scale); + + auto gc = wxGraphicsContext::Create(dc); + if (!gc) { + return; + } + + gc->SetAntialiasMode(wxANTIALIAS_DEFAULT); + gc->SetFont(gc->CreateFont(*wxSMALL_FONT, *wxWHITE)); + + /* Scale lines and labels (0dB, -10dB etc.) */ + dc.SetPen(*wxGREY_PEN); + for (auto const& scale: dcpomatic::meter::scales()) { + auto const y = static_cast<int>(std::round(border() + db_to_y(meter_height, scale.first))); + dc.DrawLine({ border() + scale_label_width(), y }, { panel_size.width - border(), y }); + auto const size = _sizes.get(gc, scale.second); + gc->DrawText(scale.second, border() + scale_label_width() - size.width - 4, y - size.height / 2); + } + + if (!_levels) { + delete gc; + return; + } + + auto const channel_width = (panel_size.width - 2 * border() - scale_label_width()) / _levels->peaks.size(); + + /* Meter bars */ + int x = border() + scale_label_width(); + dc.SetBrush(*wxGREEN_BRUSH); + dc.SetPen(*wxBLACK_PEN); + for (auto peak: _levels->peaks) { + auto const y = border() + db_to_y(meter_height, linear_level_to_db(peak)); + dc.DrawRectangle(x, y, channel_width, meter_height - y + border()); + x += channel_width; + } + + /* Channel name labels */ + for (auto i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) { + auto name = std_to_wx(short_audio_channel_name(i)); + gc->DrawText(name, border() + scale_label_width() + i * channel_width + (channel_width - _sizes.get(gc, name).width) / 2, panel_size.height - channel_label_height()); + } + + delete gc; +} + + +void +SimpleMeters::update(shared_ptr<Butler> butler, dcpomatic::DCPTime time) +{ + while (!_levels || (_levels->time + butler->time_between_audio_level_measurements()) < time) { + _levels = butler->get_audio_levels(); + if (!_levels) { + return; + } + } + + _panel->Refresh(); + _panel->Update(); +} + + +void +SimpleMeters::clear() +{ + _levels = {}; + _panel->Refresh(); + _panel->Update(); +} diff --git a/src/wx/simple_meters.h b/src/wx/simple_meters.h new file mode 100644 index 000000000..d05129e6c --- /dev/null +++ b/src/wx/simple_meters.h @@ -0,0 +1,65 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_SIMPLE_METERS_H +#define DCPOMATIC_SIMPLE_METERS_H + + +#include "lib/audio_levels.h" +#include "lib/dcpomatic_time.h" +#include "text_size_cache.h" +#include <memory> +#include <vector> + + +class Butler; +class LevelCalculator; +class wxPanel; +class wxShowEvent; +class wxWindow; + + +class SimpleMeters +{ +public: + SimpleMeters(wxWindow* parent); + + SimpleMeters(SimpleMeters const&) = delete; + SimpleMeters& operator=(SimpleMeters const&) = delete; + + wxWindow* get() const; + + void update(std::shared_ptr<Butler> butler, dcpomatic::DCPTime time); + void clear(); + +private: + void paint(); + void shown(wxShowEvent& ev); + + wxPanel* _panel; + boost::optional<AudioLevels> _levels; + + TextSizeCache _sizes; +}; + + +#endif + diff --git a/src/wx/simple_video_view.cc b/src/wx/simple_video_view.cc index 665cee117..dcc15c80a 100644 --- a/src/wx/simple_video_view.cc +++ b/src/wx/simple_video_view.cc @@ -50,6 +50,7 @@ SimpleVideoView::SimpleVideoView (FilmViewer* viewer, wxWindow* parent) : VideoView (viewer) , _rec2020_filter("convert", "convert", "", "colorspace=all=bt709:iall=bt2020") , _rec2020_filter_graph({ _rec2020_filter }, dcp::Fraction(24, 1)) + , _meters(parent) { _panel = new wxPanel (parent); @@ -265,4 +266,6 @@ SimpleVideoView::update () _inter_size = player_video().first->inter_size (); refresh_panel (); + + _meters.update(_viewer->butler(), _viewer->time()); } diff --git a/src/wx/simple_video_view.h b/src/wx/simple_video_view.h index e19068979..2c6ce7965 100644 --- a/src/wx/simple_video_view.h +++ b/src/wx/simple_video_view.h @@ -19,6 +19,7 @@ */ +#include "simple_meters.h" #include "video_view.h" #include "lib/filter.h" #include "lib/position.h" @@ -43,6 +44,10 @@ public: return _panel; } + wxWindow* get_meters() { + return _meters.get(); + } + void update () override; void start () override; NextFrameResult display_next_frame (bool non_blocking) override; @@ -60,4 +65,6 @@ private: Filter _rec2020_filter; VideoFilterGraphSet _rec2020_filter_graph; + + SimpleMeters _meters; }; diff --git a/src/wx/text_size_cache.cc b/src/wx/text_size_cache.cc new file mode 100644 index 000000000..03b7e62ff --- /dev/null +++ b/src/wx/text_size_cache.cc @@ -0,0 +1,47 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "text_size_cache.h" +LIBDCP_DISABLE_WARNINGS +#include <wx/graphics.h> +LIBDCP_ENABLE_WARNINGS +#include <wx/wx.h> + + +dcp::Size +TextSizeCache::get(wxGraphicsContext* gc, wxString const& text) +{ + auto iter = _known.find(text); + if (iter != _known.end()) { + return iter->second; + } + + wxDouble width; + wxDouble height; + wxDouble descent; + wxDouble leading; + gc->GetTextExtent(text, &width, &height, &descent, &leading); + auto size = dcp::Size{static_cast<int>(std::round(width)), static_cast<int>(std::round(height))}; + _known[text] = size; + + return size; +} + diff --git a/src/wx/text_size_cache.h b/src/wx/text_size_cache.h new file mode 100644 index 000000000..58fb9c82a --- /dev/null +++ b/src/wx/text_size_cache.h @@ -0,0 +1,40 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include <dcp/types.h> +#include <wx/wx.h> +#include <unordered_map> + + +class wxGraphicsContext; +class wxString; + + +class TextSizeCache +{ +public: + TextSizeCache() = default; + + dcp::Size get(wxGraphicsContext* gc, wxString const& text); + +private: + std::unordered_map<wxString, dcp::Size> _known; +}; diff --git a/src/wx/wscript b/src/wx/wscript index 6bb10511b..e1a2bf084 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -125,6 +125,8 @@ sources = """ markers_panel.cc message_dialog.cc metadata_dialog.cc + meter_util.cc + meters_dialog.cc move_to_dialog.cc nag_dialog.cc name_format_editor.cc @@ -160,6 +162,7 @@ sources = """ server_dialog.cc servers_list_dialog.cc short_kdm_output_panel.cc + simple_meters.cc simple_video_view.cc smpte_metadata_dialog.cc sound_preferences_page.cc @@ -174,6 +177,7 @@ sources = """ tall_kdm_output_panel.cc templates_dialog.cc text_panel.cc + text_size_cache.cc text_view.cc time_picker.cc timer_display.cc diff --git a/test/audio_level_calculator_test.cc b/test/audio_level_calculator_test.cc new file mode 100644 index 000000000..c9b6d8245 --- /dev/null +++ b/test/audio_level_calculator_test.cc @@ -0,0 +1,46 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "lib/audio_buffers.h" +#include "lib/audio_level_calculator.h" +#include <boost/test/unit_test.hpp> + + +using std::make_shared; + + +BOOST_AUTO_TEST_CASE(level_calculator_falloff_correct) +{ + auto pulse = make_shared<AudioBuffers>(6, 96000); + pulse->make_silent(); + + pulse->data()[0][0] = 1; + + AudioLevelCalculator calc(30, 15); + calc.put(pulse, {}, 48000); + + for (auto i = 0; i < 30; ++i) { + calc.get(); + } + + BOOST_CHECK_CLOSE(calc.get()->peaks[0], pow(10, -15.0 / 20), 0.0001); +} + diff --git a/test/wscript b/test/wscript index b568872a3..afcc47ed7 100644 --- a/test/wscript +++ b/test/wscript @@ -54,6 +54,7 @@ def build(bld): audio_content_test.cc audio_delay_test.cc audio_filter_test.cc + audio_level_calculator_test.cc audio_mapping_test.cc audio_merger_test.cc audio_processor_test.cc |
