diff options
| -rw-r--r-- | src/wx/film_viewer.cc | 8 | ||||
| -rw-r--r-- | src/wx/gl_meters.cc | 401 | ||||
| -rw-r--r-- | src/wx/gl_meters.h | 88 | ||||
| -rw-r--r-- | src/wx/gl_util.cc | 58 | ||||
| -rw-r--r-- | src/wx/gl_util.h | 16 | ||||
| -rw-r--r-- | src/wx/gl_video_view.h | 7 | ||||
| -rw-r--r-- | src/wx/simple_video_view.h | 2 | ||||
| -rw-r--r-- | src/wx/video_view.h | 2 | ||||
| -rw-r--r-- | src/wx/wscript | 1 |
9 files changed, 576 insertions, 7 deletions
diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc index a52f4286e..0c144086b 100644 --- a/src/wx/film_viewer.cc +++ b/src/wx/film_viewer.cc @@ -98,20 +98,16 @@ FilmViewer::FilmViewer(wxWindow* p) _video_view = std::make_shared<GLVideoView>(this, p); break; case Config::VIDEO_VIEW_SIMPLE: - { - auto simple = std::make_shared<SimpleVideoView>(this, p); - _meters_dialog->set_meters(simple->get_meters()); - _video_view = simple; - } + _video_view = std::make_shared<SimpleVideoView>(this, p); break; } #else 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)); + _meters_dialog->set_meters(_video_view->get_meters()); _video_view->Sized.connect(boost::bind(&FilmViewer::video_view_sized, this)); _video_view->TooManyDropped.connect(boost::bind(boost::ref(TooManyDropped))); diff --git a/src/wx/gl_meters.cc b/src/wx/gl_meters.cc new file mode 100644 index 000000000..74fb49be0 --- /dev/null +++ b/src/wx/gl_meters.cc @@ -0,0 +1,401 @@ +/* + 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 "film_viewer.h" +#include "gl_meters.h" +#include "gl_util.h" +#include "meter_util.h" +#include "wx_util.h" +#include "lib/butler.h" +#include "lib/constants.h" +#include "lib/image_png.h" +LIBDCP_DISABLE_WARNINGS +#include <wx/dcmemory.h> +#include <wx/glcanvas.h> +#include <wx/graphics.h> +#include <wx/rawbmp.h> +#include <wx/wx.h> +LIBDCP_ENABLE_WARNINGS + +#ifdef DCPOMATIC_OSX +#define GL_DO_NOT_WARN_IF_MULTI_GL_VERSION_HEADERS_INCLUDED +#include <OpenGL/OpenGL.h> +#include <OpenGL/gl3.h> +#endif + +#ifdef DCPOMATIC_LINUX +#include <GL/glu.h> +#include <GL/glext.h> +#endif + +#ifdef DCPOMATIC_WINDOWS +#include <GL/glu.h> +#include <GL/wglext.h> +#endif + + +using std::make_shared; +using std::vector; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif +using namespace dcpomatic::gl; + + +static constexpr char fragment_source[] = +"#version 330 core\n" +"\n" +"in vec2 TexCoord;\n" +"\n" +"uniform sampler2D texture_sampler;\n" +"\n" +/* type = 0: draw meter bar + * type = 1: draw label texture + * type = 2: draw scale lines + * See FragmentType enum below. + */ +"uniform int type = 0;\n" +"uniform vec4 meter_bar_colour;\n" +"uniform vec4 meter_line_colour;\n" +"uniform int height;\n" +"\n" +"out vec4 FragColor;\n" +"\n" +"void main()\n" +"{\n" +" switch (type) {\n" +" case 0:\n" +" float scale = gl_FragCoord.y / height;\n" +" FragColor = meter_bar_colour * vec4(scale, scale, scale, 1);\n" +" break;\n" +" case 1:\n" +" FragColor = texture(texture_sampler, TexCoord);\n" +" break;\n" +" case 2:\n" +" FragColor = meter_line_colour;\n" +" break;\n" +" }\n" +"}\n"; + + +enum class FragmentType +{ + METER_BARS = 0, + LABEL = 1, + SCALE_LINE = 2, +}; + + +auto constexpr label_texture_width = 32; +auto constexpr label_texture_height = 16; + + +GLMeters::GLMeters(wxWindow* parent, FilmViewer* viewer) + : _view( + parent, + boost::bind(&GLMeters::setup_shaders, this), + boost::bind(&GLMeters::create_textures, this), + boost::bind(&GLMeters::play_pre_draw, this), + boost::bind(&GLMeters::draw, this), + boost::bind(&GLMeters::play_post_draw, this), + fragment_source + ) + , _viewer(viewer) +{ + +} + + +void +GLMeters::setup_shaders() +{ + vector<unsigned int> indices; + for (auto i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) { + auto rectangle = _coords.add_filled_rectangle(); + _meter_bars.push_back(rectangle); + _meter_bars_group.add(rectangle); + } + for (auto i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) { + _channel_labels.push_back(_coords.add_filled_rectangle()); + } + for (auto i = 0U; i < dcpomatic::meter::scales().size(); ++i) { + _scale_labels.push_back(_coords.add_filled_rectangle()); + _scale_lines.push_back(_coords.add_line()); + } + + _coords.setup(); + + _fragment_type.setup(_view.program(), "type"); + _texture_sampler.setup(_view.program(), "texture_sampler"); + + _meter_bar_colour.setup(_view.program(), "meter_bar_colour"); + _meter_bar_colour.set(0, 1, 0, 1.0f); + + _meter_line_colour.setup(_view.program(), "meter_line_colour"); + _meter_line_colour.set(0.7, 0.7, 0.7, 1.0f); + + _height.setup(_view.program(), "height"); + + glLineWidth(0.5f); + check_gl_error("glLineWidth"); + glEnable(GL_BLEND); + check_gl_error("glEnable"); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + check_gl_error("glBlendFunc"); +} + + +void +GLMeters::get_next_frame() +{ + if (auto levels = _viewer->butler()->get_audio_levels()) { + _last_time = levels->time; + _peaks = levels->peaks; + } else { + _last_time = boost::none; + _peaks.clear(); + } +} + + +void +GLMeters::play_pre_draw() +{ + get_next_frame(); +} + + +void +GLMeters::draw() +{ + auto constexpr meter_gap = 2; + + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + check_gl_error("glClear"); + + if (_peaks.empty()) { + glFlush(); + check_gl_error("glFlush"); + return; + } + + using namespace dcpomatic::meter; + + auto const width = _view.size().GetWidth(); + auto const height = _view.size().GetHeight(); + auto const meter_height = height - border() * 2 - channel_label_height(); + auto const channel_width = (width - scale_label_width() - border() * 2) / _peaks.size(); + + _height.set(height); + + /* Peak rectangles that we have data for */ + auto const have_data = _peaks.size(); + for (auto i = 0U; i < have_data; ++i) { + auto const y = db_to_y(meter_height, linear_level_to_db(_peaks[i])); + _meter_bars[i].set( + _view.size(), + border() + scale_label_width() + i * channel_width + meter_gap / 2, + y + border(), + wxSize{static_cast<int>(channel_width) - meter_gap, meter_height - y} + ); + } + + /* Scale coordinate labels and lines */ + int index = 0; + for (auto const& i: dcpomatic::meter::scales()) { + auto const y = db_to_y(meter_height, i.first); + _scale_labels[index].set( + _view.size(), + border() + scale_label_width() - _scale_label_widths[index] - 4, + y + border(), + wxSize{label_texture_width, label_texture_height} + ); + _scale_lines[index].set(_view.size(), 0, y + border(), width, y + border()); + ++index; + } + + /* Channel labels */ + for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) { + _channel_labels[i].set( + _view.size(), + border() + scale_label_width() + channel_width * i + (channel_width - _channel_label_widths[i]) / 2, + _view.size().GetHeight() - channel_label_height(), + wxSize{label_texture_width, label_texture_height} + ); + } + + _coords.set_array_buffer(); + + /* Draw the scales first so that the meter bars can overlay them */ + _fragment_type.set(static_cast<GLint>(FragmentType::LABEL)); + for (auto i = 0U; i < dcpomatic::meter::scales().size(); ++i) { + _scale_label_textures[i].bind(); + _texture_sampler.set(i); + _scale_labels[i].draw(); + } + + for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) { + _channel_label_textures[i].bind(); + _texture_sampler.set(dcpomatic::meter::scales().size() + i); + _channel_labels[i].draw(); + } + + _fragment_type.set(static_cast<GLint>(FragmentType::SCALE_LINE)); + for (auto i = 0U; i < dcpomatic::meter::scales().size(); ++i) { + _scale_lines[i].draw(); + } + + _fragment_type.set(static_cast<GLint>(FragmentType::METER_BARS)); + _meter_bars_group.draw(); +} + + +int +GLMeters::play_post_draw() +{ + auto time_until_next_frame = [this]() -> boost::optional<int> { + if (!_last_time) { + return {}; + } + + return dcpomatic::DCPTime(*_last_time - _viewer->position() + _viewer->butler()->time_between_audio_level_measurements()).seconds() * 1000; + }; + + while (true) { + auto const n = time_until_next_frame(); + if (!n || *n > 5) { + break; + } + get_next_frame(); + } + + return time_until_next_frame().get_value_or(0); +} + + +wxWindow* +GLMeters::get() const +{ + return _view.canvas(); +} + + +void +GLMeters::start() +{ + _view.start(); +} + + +void +GLMeters::stop() +{ + _view.stop(); +} + + +void +GLMeters::create_textures() +{ + auto label = [](wxString text) { + + wxDouble width; + + wxBitmap bitmap(label_texture_width, label_texture_height, 24); + { + wxMemoryDC dc(bitmap); + auto gc = wxGraphicsContext::Create(dc); + DCPOMATIC_ASSERT(gc); + + gc->SetAntialiasMode(wxANTIALIAS_DEFAULT); + gc->SetFont(gc->CreateFont(*wxSMALL_FONT, *wxWHITE)); + + dc.SetPen(*wxGREY_PEN); + gc->DrawText(text, 0, 0); + + wxDouble height; + wxDouble descent; + wxDouble leading; + gc->GetTextExtent(text, &width, &height, &descent, &leading); + + delete gc; + } + + auto image = make_shared<Image>(AV_PIX_FMT_RGB24, dcp::Size{label_texture_width, label_texture_height}, Image::Alignment::COMPACT); + auto* p = image->data()[0]; + + wxNativePixelData data(bitmap); + wxNativePixelData::Iterator row(data); + + for (auto y = 0; y < label_texture_height; ++y) { + auto q = row; + for (auto x = 0; x < label_texture_width; ++x) { + *p++ = q.Red(); + *p++ = q.Green(); + *p++ = q.Blue(); + ++q; + } + + row.OffsetY(data, 1); + } + + return make_pair(image, width); + }; + + + int unit = 0; + + for (auto scale: dcpomatic::meter::scales()) { + auto image_and_width = label(scale.second); + + _scale_label_textures.emplace_back(1, unit++); + _scale_label_textures.back().set(image_and_width.first); + _scale_label_widths.push_back(image_and_width.second); + _scale_label_textures.back().bind(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + check_gl_error("glTexParameteri"); + + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + check_gl_error("glTexParameterf"); + } + + for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) { + auto image_and_width = label(std_to_wx(short_audio_channel_name(i))); + + _channel_label_textures.emplace_back(1, unit++); + _channel_label_textures.back().set(image_and_width.first); + _channel_label_widths.push_back(image_and_width.second); + _channel_label_textures.back().bind(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + check_gl_error("glTexParameteri"); + + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + check_gl_error("glTexParameterf"); + } +} + diff --git a/src/wx/gl_meters.h b/src/wx/gl_meters.h new file mode 100644 index 000000000..c49981e6c --- /dev/null +++ b/src/wx/gl_meters.h @@ -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 "gl_util.h" +#include "gl_view.h" +#include "lib/dcpomatic_time.h" +#include <dcp/warnings.h> + + +class FilmViewer; + + +/* See note in gl_view.h */ +#if wxCHECK_VERSION(3,1,0) + + +class GLMeters +{ +public: + GLMeters(wxWindow* parent, FilmViewer* viewer); + + GLMeters(GLMeters const&) = delete; + GLMeters& operator=(GLMeters const&) = delete; + + wxWindow* get() const; + + void start(); + void stop(); + +private: + void play_pre_draw(); + void draw(); + int play_post_draw(); + + void get_next_frame(); + void setup_shaders(); + void create_textures(); + + GLView _view; + FilmViewer* _viewer; + + boost::optional<dcpomatic::DCPTime> _last_time; + std::vector<float> _peaks; + + dcpomatic::gl::Uniform1i _fragment_type; + dcpomatic::gl::Uniform1i _texture_sampler; + dcpomatic::gl::UniformVec4f _meter_bar_colour; + dcpomatic::gl::UniformVec4f _meter_line_colour; + dcpomatic::gl::Uniform1i _height; + + /** Number of rectangles we need nodes and coordinates for */ + int _rectangles = 0; + /** Number of lines we need nodes and coordinates for */ + int _lines = 0; + + std::vector<dcpomatic::gl::Texture> _scale_label_textures; + std::vector<int> _scale_label_widths; + std::vector<dcpomatic::gl::Texture> _channel_label_textures; + std::vector<int> _channel_label_widths; + + dcpomatic::gl::Coords _coords; + std::vector<dcpomatic::gl::Coords::FilledRectangle> _meter_bars; + dcpomatic::gl::Coords::FilledRectangleGroup _meter_bars_group; + std::vector<dcpomatic::gl::Coords::FilledRectangle> _channel_labels; + std::vector<dcpomatic::gl::Coords::FilledRectangle> _scale_labels; + std::vector<dcpomatic::gl::Coords::Line> _scale_lines; +}; + + +#endif diff --git a/src/wx/gl_util.cc b/src/wx/gl_util.cc index 8c09acd0e..6a78f4142 100644 --- a/src/wx/gl_util.cc +++ b/src/wx/gl_util.cc @@ -358,6 +358,64 @@ Coords::FilledRectangleGroup::draw() } +Coords::Line::Line(Coords& coords) + : _coords(coords) + , _node(coords._nodes) + , _index(coords._indices.size()) +{ + auto& indices = coords._indices; + auto& nodes = coords._nodes; + + indices.push_back(nodes + 0); + indices.push_back(nodes + 1); + nodes += 2; + + auto& c = _coords._coords; + + auto offset = c.size(); + + c.resize(c.size() + 5 * 2); + + c[offset + 2] = 0.0f; + c[offset + 3] = 1.0f; + c[offset + 4] = 1.0f; + + c[offset + 7] = 0.0f; + c[offset + 8] = 1.0f; + c[offset + 9] = 0.0f; +} + + +void +Coords::Line::set(wxSize canvas_size, int x1, int y1, int x2, int y2) +{ + auto& coords = _coords._coords; + + auto const o = _node * 5; + + coords[o] = x_pixels_to_gl(canvas_size, x1); + coords[o + 1] = y_pixels_to_gl(canvas_size, y1); + + coords[o + 5] = x_pixels_to_gl(canvas_size, x2); + coords[o + 6] = y_pixels_to_gl(canvas_size, y2); +} + + +void +Coords::Line::draw() +{ + glDrawElements(GL_LINES, 2, GL_UNSIGNED_INT, reinterpret_cast<void*>(_index * sizeof(int))); +} + + +Coords::Line +Coords::add_line() +{ + DCPOMATIC_ASSERT(!_setup); + return Line(*this); +} + + void Coords::setup() { diff --git a/src/wx/gl_util.h b/src/wx/gl_util.h index 101a35721..f5baa04c7 100644 --- a/src/wx/gl_util.h +++ b/src/wx/gl_util.h @@ -23,6 +23,7 @@ #define DCPOMATIC_GL_UTIL_H +#include "lib/dcpomatic_assert.h" #include <dcp/types.h> #include <wx/wx.h> @@ -119,6 +120,20 @@ public: void draw() override; }; + class Line + { + public: + Line(Coords& coords); + + void set(wxSize canvas_size, int x1, int y1, int x2, int y2); + void draw(); + + private: + Coords& _coords; + int _node; + int _index; + }; + class FilledRectangleGroup { public: @@ -132,6 +147,7 @@ public: FilledRectangle add_filled_rectangle(); OutlineRectangle add_outline_rectangle(); + Line add_line(); void setup(); void set_array_buffer(); diff --git a/src/wx/gl_video_view.h b/src/wx/gl_video_view.h index dddcbff48..82450293a 100644 --- a/src/wx/gl_video_view.h +++ b/src/wx/gl_video_view.h @@ -19,6 +19,7 @@ */ +#include "gl_meters.h" #include "gl_view.h" #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS @@ -57,6 +58,10 @@ public: void start() override; void stop() override; + wxWindow* get_meters() const override { + return _meters.get(); + } + NextFrameResult display_next_frame(bool) override; bool vsync_enabled() const { @@ -124,6 +129,8 @@ private: std::shared_ptr<wxTimer> _timer; std::map<GLenum, std::string> _information; + + GLMeters _meters; }; #endif diff --git a/src/wx/simple_video_view.h b/src/wx/simple_video_view.h index 2c6ce7965..c701c48e5 100644 --- a/src/wx/simple_video_view.h +++ b/src/wx/simple_video_view.h @@ -44,7 +44,7 @@ public: return _panel; } - wxWindow* get_meters() { + wxWindow* get_meters() const override { return _meters.get(); } diff --git a/src/wx/video_view.h b/src/wx/video_view.h index 3ea03a5fd..a7ab36388 100644 --- a/src/wx/video_view.h +++ b/src/wx/video_view.h @@ -63,6 +63,8 @@ public: /** Called when playback stops */ virtual void stop () {} + virtual wxWindow* get_meters() const = 0; + enum NextFrameResult { FAIL, AGAIN, diff --git a/src/wx/wscript b/src/wx/wscript index f1066dabe..9e805c752 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -96,6 +96,7 @@ sources = """ gain_calculator_dialog.cc gdc_certificate_panel.cc general_preferences_page.cc + gl_meters.cc gl_view.cc gl_video_view.cc gl_util.cc |
