diff options
| author | Carl Hetherington <cth@carlh.net> | 2025-04-17 02:09:09 +0200 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2025-04-20 21:18:05 +0200 |
| commit | 2136a540da3f2fe55966db8f30a05193bcb83484 (patch) | |
| tree | 30f7371c4e2e1d59a875f2f0276d9a017bbfe4e7 | |
| parent | 9323d3ec1e168a7003b5c0515247129c2fb3d1e7 (diff) | |
wip: audio meters.
| -rw-r--r-- | src/lib/butler.cc | 13 | ||||
| -rw-r--r-- | src/lib/butler.h | 5 | ||||
| -rw-r--r-- | src/lib/level_calculator.cc | 127 | ||||
| -rw-r--r-- | src/lib/level_calculator.h | 74 | ||||
| -rw-r--r-- | src/lib/wscript | 1 | ||||
| -rw-r--r-- | src/tools/dcpomatic_player.cc | 8 | ||||
| -rw-r--r-- | src/wx/film_viewer.cc | 19 | ||||
| -rw-r--r-- | src/wx/film_viewer.h | 3 | ||||
| -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 | 103 | ||||
| -rw-r--r-- | src/wx/simple_meters.h | 63 | ||||
| -rw-r--r-- | src/wx/simple_video_view.cc | 5 | ||||
| -rw-r--r-- | src/wx/simple_video_view.h | 7 | ||||
| -rw-r--r-- | src/wx/wscript | 2 |
15 files changed, 512 insertions, 2 deletions
diff --git a/src/lib/butler.cc b/src/lib/butler.cc index 14f342045..57c891404 100644 --- a/src/lib/butler.cc +++ b/src/lib/butler.cc @@ -24,6 +24,7 @@ #include "cross.h" #include "dcpomatic_log.h" #include "exceptions.h" +#include "level_calculator.h" #include "log.h" #include "player.h" #include "util.h" @@ -33,6 +34,7 @@ using std::cout; using std::function; using std::make_pair; +using std::make_shared; using std::pair; using std::shared_ptr; using std::string; @@ -88,6 +90,7 @@ Butler::Butler( , _alignment(alignment) , _fast(fast) , _prepare_only_proxy(prepare_only_proxy) + , _level_calculator(make_shared<LevelCalculator>()) { _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 +315,7 @@ Butler::seek_unlocked(DCPTime position, bool accurate) _video.clear(); _audio.clear(); + _level_calculator->clear(); _closed_caption.clear(); _summon.notify_all(); @@ -370,6 +374,8 @@ Butler::audio(shared_ptr<AudioBuffers> audio, DCPTime time, int frame_rate) return; } + _level_calculator->put(audio, time, frame_rate); + _audio.put(remap(audio, _audio_channels, _audio_mapping), time, frame_rate); } @@ -481,3 +487,10 @@ Butler::Error::summary() const return ""; } + +shared_ptr<LevelCalculator> +Butler::level_calculator() const +{ + return _level_calculator; +} + diff --git a/src/lib/butler.h b/src/lib/butler.h index ad2552769..dd04e7b86 100644 --- a/src/lib/butler.h +++ b/src/lib/butler.h @@ -37,6 +37,7 @@ #include <boost/thread/condition.hpp> +class LevelCalculator; class Player; class PlayerVideo; @@ -94,6 +95,8 @@ public: boost::optional<dcpomatic::DCPTime> get_audio(Behaviour behaviour, float* out, Frame frames); boost::optional<TextRingBuffers::Data> get_closed_caption(); + std::shared_ptr<LevelCalculator> level_calculator() const; + std::pair<size_t, std::string> memory_used() const; private: @@ -154,6 +157,8 @@ private: */ boost::optional<dcpomatic::DCPTime> _awaiting; + std::shared_ptr<LevelCalculator> _level_calculator; + 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/level_calculator.cc b/src/lib/level_calculator.cc new file mode 100644 index 000000000..0563d4ed8 --- /dev/null +++ b/src/lib/level_calculator.cc @@ -0,0 +1,127 @@ +/* + 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 "level_calculator.h" + + +using std::make_pair; +using std::shared_ptr; +using boost::optional; + + +constexpr int frames_per_measurement = 48000 / 30; + + +LevelCalculator::LevelCalculator() + : _enabled(false) +{ + +} + + +void +LevelCalculator::put(shared_ptr<const AudioBuffers> audio, dcpomatic::DCPTime time, int frame_rate) +{ + if (!_enabled) { + std::cout << "no calcs for me.\n"; + return; + } + + 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); + _peaks.emplace_back(time + dcpomatic::DCPTime::from_frames(frame, frame_rate), _current_peaks); + } + std::fill(_current_peaks.begin(), _current_peaks.end(), 0.0f); + _current_frames = 0; + } + } + +} + + +void +LevelCalculator::clear() +{ + boost::mutex::scoped_lock slm(_store_mutex); + _peaks.clear(); + + boost::mutex::scoped_lock clm(_current_mutex); + std::fill(_current_peaks.begin(), _current_peaks.end(), 0.0f); + _current_frames = 0; +} + + +std::vector<float> +LevelCalculator::get(dcpomatic::DCPTime time) +{ + boost::mutex::scoped_lock slm(_store_mutex); + + auto iter = _peaks.begin(); + optional<dcpomatic::DCPTime> last_delta; + std::list<Measurement>::iterator last_iter; + while (iter != _peaks.end()) { + auto const delta = dcpomatic::DCPTime(time - iter->time).abs(); + if (last_delta) { + if (delta > *last_delta) { + /* This is worse than the last - use the last one */ + return last_iter->value; + } else { + /* This is better - keep looking */ + _peaks.erase(last_iter); + } + } + last_delta = delta; + last_iter = iter; + ++iter; + } + + if (iter == _peaks.end()) { + return {}; + } + + return iter->value; +} + + +void +LevelCalculator::enable(bool e) +{ + _enabled = e; +} + diff --git a/src/lib/level_calculator.h b/src/lib/level_calculator.h new file mode 100644 index 000000000..9eeb5b9b8 --- /dev/null +++ b/src/lib/level_calculator.h @@ -0,0 +1,74 @@ +/* + 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 "dcpomatic_time.h" +#include <boost/thread.hpp> +#include <memory> + + +class AudioBuffers; + + +class LevelCalculator +{ +public: + LevelCalculator(); + + LevelCalculator(LevelCalculator const&) = delete; + LevelCalculator& operator=(LevelCalculator const&) = delete; + + void put(std::shared_ptr<const AudioBuffers> audio, dcpomatic::DCPTime time, int frame_rate); + void clear(); + std::vector<float> get(dcpomatic::DCPTime time); + + void enable(bool e); + +private: + boost::mutex _current_mutex; + std::vector<float> _current_peaks; + int _current_frames = 0; + + struct Measurement + { + Measurement(dcpomatic::DCPTime t, std::vector<float> v) + : time(t) + , value(std::move(v)) + { + + } + + dcpomatic::DCPTime time; + std::vector<float> value; + }; + + mutable boost::mutex _store_mutex; + std::list<Measurement> _peaks; + + boost::atomic<bool> _enabled; +}; + + +#endif + diff --git a/src/lib/wscript b/src/lib/wscript index dafd655fe..4b0002f04 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -156,6 +156,7 @@ sources = """ kdm_recipient.cc kdm_with_metadata.cc kdm_util.cc + level_calculator.cc log.cc log_entry.cc make_dcp.cc diff --git a/src/tools/dcpomatic_player.cc b/src/tools/dcpomatic_player.cc index c5242c310..98fa6517a 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...")); _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..5e1caa1bf 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,11 +98,17 @@ 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 _video_view->Sized.connect(boost::bind(&FilmViewer::video_view_sized, this)); @@ -808,6 +816,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..31fdbe0c7 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 { @@ -212,6 +214,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/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..b719f93f9 --- /dev/null +++ b/src/wx/simple_meters.cc @@ -0,0 +1,103 @@ +/* + 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 "simple_meters.h" +#include "wx_util.h" +#include "lib/level_calculator.h" +#include <dcp/types.h> +#include <wx/wx.h> +#include <algorithm> + + +static auto constexpr min_db = -70.0f; + + +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() +{ + wxPaintDC dc(_panel); + + dc.SetBackground(*wxBLACK_BRUSH); + dc.Clear(); + + if (_peaks.empty()) { + return; + } + + auto const scale = 1 / dpi_scale_factor(_panel); + dc.SetLogicalScale(scale, scale); + + auto const panel_size = dcp::Size(_panel->GetSize().GetWidth() / scale, _panel->GetSize().GetHeight() / scale); + + auto const channel_width = panel_size.width / _peaks.size(); + + int x = 0; + dc.SetBrush(*wxGREEN_BRUSH); + for (auto peak: _peaks) { + auto const peak_db = std::max(min_db, 20 * log10(peak)); + dc.DrawRectangle(x, panel_size.height, channel_width, (1 - (peak_db - min_db)) * panel_size.height / (-min_db)); + x += channel_width; + } +} + + +bool +SimpleMeters::update(shared_ptr<LevelCalculator> calculator, dcpomatic::DCPTime time) +{ + if (!_panel->GetParent()->IsShown()) { + return false; + } + + _peaks = calculator->get(time); + _panel->Refresh(); + _panel->Update(); + + return true; +} + + +void +SimpleMeters::clear() +{ + _peaks.clear(); + _panel->Refresh(); + _panel->Update(); +} diff --git a/src/wx/simple_meters.h b/src/wx/simple_meters.h new file mode 100644 index 000000000..6b3843f3c --- /dev/null +++ b/src/wx/simple_meters.h @@ -0,0 +1,63 @@ +/* + 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/dcpomatic_time.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; + + bool update(std::shared_ptr<LevelCalculator> calculator, dcpomatic::DCPTime time); + void clear(); + +private: + void paint(); + void shown(wxShowEvent& ev); + + wxPanel* _panel; + std::vector<float> _peaks; + + std::weak_ptr<LevelCalculator> _level_calculator; +}; + + +#endif + diff --git a/src/wx/simple_video_view.cc b/src/wx/simple_video_view.cc index 665cee117..fe0ea42e0 100644 --- a/src/wx/simple_video_view.cc +++ b/src/wx/simple_video_view.cc @@ -26,6 +26,7 @@ #include "lib/butler.h" #include "lib/dcpomatic_log.h" #include "lib/image.h" +#include "lib/level_calculator.h" #include "lib/video_filter_graph.h" #include "lib/video_filter_graph_set.h" #include <dcp/util.h> @@ -50,6 +51,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 +267,7 @@ SimpleVideoView::update () _inter_size = player_video().first->inter_size (); refresh_panel (); + + auto calculator = _viewer->butler()->level_calculator(); + calculator->enable(_meters.update(calculator, _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/wscript b/src/wx/wscript index 6bb10511b..9a9a79d9c 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -125,6 +125,7 @@ sources = """ markers_panel.cc message_dialog.cc metadata_dialog.cc + meters_dialog.cc move_to_dialog.cc nag_dialog.cc name_format_editor.cc @@ -160,6 +161,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 |
