From af517d2d2a0a02ea167ffac4c617845727984720 Mon Sep 17 00:00:00 2001 From: Carl Hetherington Date: Wed, 22 Oct 2025 15:32:30 +0200 Subject: Add DCP-o-matic Processor tool. --- run/dcpomatic_processor | 24 +++ src/lib/fix_audio_levels_job.cc | 99 ++++++++++ src/lib/fix_audio_levels_job.h | 46 +++++ src/lib/variant.cc | 7 + src/lib/variant.h | 1 + src/lib/wscript | 1 + src/tools/dcpomatic_processor.cc | 401 +++++++++++++++++++++++++++++++++++++++ src/tools/wscript | 5 +- src/wx/wx_variant.cc | 12 ++ src/wx/wx_variant.h | 2 + 10 files changed, 597 insertions(+), 1 deletion(-) create mode 100755 run/dcpomatic_processor create mode 100644 src/lib/fix_audio_levels_job.cc create mode 100644 src/lib/fix_audio_levels_job.h create mode 100644 src/tools/dcpomatic_processor.cc diff --git a/run/dcpomatic_processor b/run/dcpomatic_processor new file mode 100755 index 000000000..0cd37c74a --- /dev/null +++ b/run/dcpomatic_processor @@ -0,0 +1,24 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source $DIR/environment +binary=$build/src/tools/dcpomatic2_processor + +if [[ "$(uname -m)" == arm64 ]]; then + env=arm64/11.0 +else + env=x86_64/10.10 +fi + +export DYLD_LIBRARY_PATH=/Users/cah/osx-environment/$env/lib:/usr/local/lib + +if [ "$1" == "--debug" ]; then + shift + if [[ "$(uname)" == Darwin ]]; then + /Applications/Xcode.app/Contents/Developer/usr/bin/lldb $binary $* + else + gdb --args $binary $* + fi +else + $binary $* 2> >(grep -v Gtk-CRITICAL | grep -v Gtk-WARNING) +fi diff --git a/src/lib/fix_audio_levels_job.cc b/src/lib/fix_audio_levels_job.cc new file mode 100644 index 000000000..b5d9e39d9 --- /dev/null +++ b/src/lib/fix_audio_levels_job.cc @@ -0,0 +1,99 @@ +/* + Copyright (C) 2025 Carl Hetherington + + 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 . + +*/ + + +#include "analyse_audio_job.h" +#include "audio_content.h" +#include "copy_dcp_details_to_film.h" +#include "dcp_content.h" +#include "dcp_film_encoder.h" +#include "dcp_transcode_job.h" +#include "film.h" +#include "fix_audio_levels_job.h" +#include "playlist.h" +#include +#include +#include + +#include "i18n.h" + + +using std::make_shared; +using std::shared_ptr; +using std::string; +using boost::optional; + + +FixAudioLevelsJob::FixAudioLevelsJob(boost::filesystem::path input_dcp_path, boost::filesystem::path output_dcp_path, float leqm_target, bool make_quieter_dcps_louder) + : Job(shared_ptr()) + , _input_dcp_path(input_dcp_path) + , _output_dcp_path(output_dcp_path) + , _leqm_target(leqm_target) + , _make_quieter_dcps_louder(make_quieter_dcps_louder) +{ + +} + + +string +FixAudioLevelsJob::name() const +{ + return fmt::format(_("Correcting audio levels of {} to {}"), _input_dcp_path.string(), _leqm_target); +} + + +string +FixAudioLevelsJob::json_name() const +{ + return N_("fix_audio_levels"); +} + + +void +FixAudioLevelsJob::run() +{ + auto input_dcp = make_shared(_input_dcp_path); + auto film = std::make_shared(_output_dcp_path); + input_dcp->examine(film, shared_from_this(), true); + film->add_content({input_dcp}); + copy_dcp_settings_to_film(input_dcp, film); + + auto playlist = make_shared(); + playlist->add(film, input_dcp); + + AnalyseAudioJob analyse(film, playlist, false, boost::none); + analyse.run(); + + auto const level = analyse.analysis().leqm(); + DCPOMATIC_ASSERT(level); + + if ((!_make_quieter_dcps_louder && *level < _leqm_target) || !input_dcp->audio) { + set_progress(1); + set_state(FINISHED_OK); + } + + input_dcp->audio->set_gain(_leqm_target - *level); + auto transcode = make_shared(film, TranscodeJob::ChangedBehaviour::IGNORE); + transcode->set_encoder(make_shared(film, transcode, true)); + transcode->run(); + + set_progress(1); + set_state(FINISHED_OK); +} diff --git a/src/lib/fix_audio_levels_job.h b/src/lib/fix_audio_levels_job.h new file mode 100644 index 000000000..eb504bb98 --- /dev/null +++ b/src/lib/fix_audio_levels_job.h @@ -0,0 +1,46 @@ +/* + Copyright (C) 2025 Carl Hetherington + + 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 . + +*/ + + +#include "job.h" + + +class FixAudioLevelsJob : public Job +{ +public: + /** @param input_dcp_path Directory containing the DCP to process. + * @param output_dcp_path Directory in which to write the fixed DCP (if required). The DCP will be created as a directory + * within output_dcp_path. + * @param leqm_target LEQ(m) target. + * @param make_quieter_dcps_louder true to increase the gain of DCPs that are less than leqm_target, false to only correct those which are louder. + */ + FixAudioLevelsJob(boost::filesystem::path input_dcp_path, boost::filesystem::path output_dcp_path, float leqm_target, bool make_quieter_dcps_louder); + + std::string name() const override; + std::string json_name() const override; + void run() override; + +private: + boost::filesystem::path _input_dcp_path; + boost::filesystem::path _output_dcp_path; + float _leqm_target; + bool _make_quieter_dcps_louder; +}; + diff --git a/src/lib/variant.cc b/src/lib/variant.cc index 81931dcf7..5ca7a9982 100644 --- a/src/lib/variant.cc +++ b/src/lib/variant.cc @@ -36,6 +36,7 @@ static char const* _dcpomatic_batch_converter_app = "DCP-o-matic 2 Batch Convert static char const* _dcpomatic_playlist_editor = "DCP-o-matic Playlist Editor"; static char const* _dcpomatic_combiner = "DCP-o-matic Combiner"; static char const* _dcpomatic_batch_converter = "DCP-o-matic Batch Converter"; +static char const* _dcpomatic_processor = "DCP-o-matic Processor"; static char const* _report_problem_email = "carl@dcpomatic.com"; @@ -106,6 +107,12 @@ variant::dcpomatic_verifier() return _dcpomatic_verifier; } +std::string +variant::dcpomatic_processor() +{ + return _dcpomatic_processor; +} + std::string variant::insert_dcpomatic(std::string const& s) { diff --git a/src/lib/variant.h b/src/lib/variant.h index c7e26c1d6..d1fc7ad7e 100644 --- a/src/lib/variant.h +++ b/src/lib/variant.h @@ -35,6 +35,7 @@ std::string dcpomatic_kdm_creator(); std::string dcpomatic_player(); std::string dcpomatic_playlist_editor(); std::string dcpomatic_verifier(); +std::string dcpomatic_processor(); std::string insert_dcpomatic(std::string const& s); std::string insert_dcpomatic_encode_server(std::string const& s); diff --git a/src/lib/wscript b/src/lib/wscript index 2e7b0339c..abded0152 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -104,6 +104,7 @@ sources = """ fcpxml.cc fcpxml_content.cc fcpxml_decoder.cc + fix_audio_levels_job.cc frame_info.cc file_group.cc file_log.cc diff --git a/src/tools/dcpomatic_processor.cc b/src/tools/dcpomatic_processor.cc new file mode 100644 index 000000000..0624d3b45 --- /dev/null +++ b/src/tools/dcpomatic_processor.cc @@ -0,0 +1,401 @@ +/* + Copyright (C) 2025 Carl Hetherington + + 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 . + +*/ + + +#include "wx/about_dialog.h" +#include "wx/dcpomatic_button.h" +#include "wx/check_box.h" +#include "wx/editable_list.h" +#include "wx/i18n_setup.h" +#include "wx/wx_util.h" +#include "wx/wx_variant.h" +#include "lib/cross.h" +#include "lib/fix_audio_levels_job.h" +#include "lib/job_manager.h" +#include "lib/util.h" +#include "lib/variant.h" +#include +#include +LIBDCP_DISABLE_WARNINGS +#include +#include +LIBDCP_ENABLE_WARNINGS +#ifdef __WXGTK__ +#include +#endif + + +using std::exception; +using std::make_shared; +using std::shared_ptr; +using std::vector; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif + + +/** @file src/tools/dcpomatic_processor.cc + * @brief A tool to batch-process DCPs in some way (e.g. fix their sound levels) + */ + + +class DirDialogWrapper : public wxDirDialog +{ +public: + DirDialogWrapper(wxWindow* parent) + : wxDirDialog(parent, _("Choose a folder"), {}, wxDD_DIR_MUST_EXIST) + { + + } + + vector get() const + { + return dcp::find_potential_dcps(boost::filesystem::path(wx_to_std(GetPath()))); + } + + void set(boost::filesystem::path) + { + /* Not used */ + } +}; + + + +class DOMFrame : public wxFrame +{ +public: + explicit DOMFrame(wxString const& title) + : wxFrame(nullptr, -1, title) + /* Use a panel as the only child of the Frame so that we avoid + the dark-grey background on Windows. + */ + , _overall_panel(new wxPanel(this, wxID_ANY)) + { + auto bar = new wxMenuBar; + setup_menu(bar); + SetMenuBar(bar); + + Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_exit, this), wxID_EXIT); + Bind(wxEVT_MENU, boost::bind(&DOMFrame::help_about, this), wxID_ABOUT); + +#ifdef DCPOMATIC_WINDOWS + SetIcon(wxIcon(std_to_wx("id"))); +#endif + auto overall_sizer = new wxBoxSizer(wxVERTICAL); + + auto input_dcp_sizer = new wxBoxSizer(wxHORIZONTAL); + add_label_to_sizer(input_dcp_sizer, _overall_panel, _("Input DCPs"), true, 0, wxALIGN_CENTER_VERTICAL); + + auto input_dcps = new EditableList( + _overall_panel, + { EditableListColumn(_("DCP"), 300, true) }, + boost::bind(&DOMFrame::dcp_paths, this), + boost::bind(&DOMFrame::set_dcp_paths, this, _1), + [](boost::filesystem::path p, int) { return p.filename().string(); }, + EditableListTitle::INVISIBLE, + EditableListButton::NEW | EditableListButton::REMOVE + ); + + input_dcp_sizer->Add(input_dcps, 1, wxLEFT | wxEXPAND, DCPOMATIC_SIZER_GAP); + overall_sizer->Add(input_dcp_sizer, 0, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + + auto options_sizer = new wxBoxSizer(wxVERTICAL); + _fix_audio_levels = new CheckBox(_overall_panel, _("Fix audio levels")); + _fix_audio_levels->set(true); + _fix_audio_levels->SetToolTip( + _("Tick to change the audio levels to match the given LEQ(m) value") + ); + options_sizer->Add(_fix_audio_levels, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP); + overall_sizer->Add(options_sizer, 0, wxLEFT, DCPOMATIC_DIALOG_BORDER); + + auto actions_sizer = new wxBoxSizer(wxHORIZONTAL); + _cancel = new Button(_overall_panel, _("Cancel")); + actions_sizer->Add(_cancel, 0, wxRIGHT, DCPOMATIC_SIZER_GAP); + _process = new Button(_overall_panel, _("Process")); + actions_sizer->Add(_process, 0, wxRIGHT, DCPOMATIC_SIZER_GAP); + overall_sizer->Add(actions_sizer, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER, DCPOMATIC_DIALOG_BORDER); + + _overall_panel->SetSizerAndFit(overall_sizer); + + _cancel->bind(&DOMFrame::cancel_clicked, this); + _process->bind(&DOMFrame::process_clicked, this); + + setup_sensitivity(); + } + +private: + void file_exit() + { + Close(); + } + + void help_about() + { + AboutDialog dialog(_overall_panel); + dialog.ShowModal(); + } + + void setup_menu(wxMenuBar* m) + { + auto help = new wxMenu; +#ifdef DCPOMATIC_OSX + /* This just needs to be appended somewhere, it seems - it magically + * gets moved to the right place. + */ + help->Append(wxID_EXIT, _("&Exit")); + help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic(_("About %s"))); +#else + auto file = new wxMenu; + file->Append(wxID_EXIT, _("&Quit")); + m->Append(file, _("&File")); + + help->Append(wxID_ABOUT, _("About")); +#endif + m->Append(help, _("&Help")); + } + + void setup_sensitivity() + { + auto const work = JobManager::instance()->work_to_do(); + _cancel->Enable(work); + _process->Enable(!_dcp_paths.empty() && !work); + } + + void cancel_clicked() + { + _cancel_pending = true; + } + + void process_clicked() + { + auto job_manager = JobManager::instance(); + vector> jobs; + for (auto const& dcp: _dcp_paths) { + auto job = make_shared(dcp, boost::filesystem::path("/home/carl/tmp/wankz"), 60, true); + job_manager->add(job); + jobs.push_back(job); + } + + setup_sensitivity(); + + while (job_manager->work_to_do() && !_cancel_pending) { + wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT); + dcpomatic_sleep_milliseconds(250); + } + + if (_cancel_pending) { + _cancel_pending = false; + JobManager::instance()->cancel_all_jobs(); + setup_sensitivity(); + return; + } + +#if 0 + dcp::VerificationOptions options; + options.check_picture_details = _check_picture_details->get(); + auto job_manager = JobManager::instance(); + vector> jobs; + for (auto const& dcp: _dcp_paths) { + auto job = make_shared( + std::vector{dcp}, + std::vector(), + options + ); + job_manager->add(job); + jobs.push_back(job); + } + + setup_sensitivity(); + + while (job_manager->work_to_do() && !_cancel_pending) { + wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT); + dcpomatic_sleep_milliseconds(250); + auto last = job_manager->last_active_job(); + if (auto locked = last.lock()) { + if (auto dcp = dynamic_pointer_cast(locked)) { + _progress_panel->update(dcp); + } + } + } + + if (_cancel_pending) { + _cancel_pending = false; + JobManager::instance()->cancel_all_jobs(); + _progress_panel->clear(); + setup_sensitivity(); + return; + } + + DCPOMATIC_ASSERT(_dcp_paths.size() == jobs.size()); + _result_panel->add(jobs); + if (_write_log->get()) { + for (size_t i = 0; i < _dcp_paths.size(); ++i) { + dcp::TextFormatter formatter(_dcp_paths[i] / "REPORT.txt"); + dcp::verify_report({ jobs[i]->result() }, formatter); + } + } + + _progress_panel->clear(); + setup_sensitivity(); +#endif + } + +private: + void set_dcp_paths (vector dcps) + { + _dcp_paths = dcps; + setup_sensitivity(); + } + + vector dcp_paths() const + { + return _dcp_paths; + } + + wxPanel* _overall_panel = nullptr; + std::vector _dcp_paths; + CheckBox* _fix_audio_levels; + Button* _cancel; + Button* _process; + bool _cancel_pending = false; +}; + + +/** @class App + * @brief The magic App class for wxWidgets. + */ +class App : public wxApp +{ +public: + App() + : wxApp() + { + dcpomatic_setup_path_encoding(); +#ifdef DCPOMATIC_LINUX + XInitThreads(); +#endif + } + +private: + bool OnInit() override + { + try { + SetAppName(variant::wx::dcpomatic_processor()); + + if (!wxApp::OnInit()) { + return false; + } + +#ifdef DCPOMATIC_LINUX + unsetenv("UBUNTU_MENUPROXY"); +#endif + +#ifdef DCPOMATIC_OSX + dcpomatic_sleep_seconds(1); + make_foreground_application(); +#endif + + /* Enable i18n; this will create a Config object + to look for a force-configured language. This Config + object will be wrong, however, because dcpomatic_setup + hasn't yet been called and there aren't any filters etc. + set up yet. + */ + dcpomatic::wx::setup_i18n(); + + /* Set things up, including filters etc. + which will now be internationalised correctly. + */ + dcpomatic_setup(); + + /* Force the configuration to be re-loaded correctly next + time it is needed. + */ + Config::drop(); + + _frame = new DOMFrame(variant::wx::dcpomatic_processor()); + SetTopWindow(_frame); + _frame->Maximize(); + _frame->Show(); + } + catch (exception& e) + { + error_dialog(nullptr, variant::wx::insert_dcpomatic_processor(char_to_wx("%s could not start.")), std_to_wx(e.what())); + } + + return true; + } + + void report_exception() + { + try { + throw; + } catch (FileError& e) { + error_dialog( + nullptr, + wxString::Format( + _("An exception occurred: %s (%s)\n\n%s"), + std_to_wx(e.what()), + std_to_wx(e.file().string().c_str()), + dcpomatic::wx::report_problem() + ) + ); + } catch (boost::filesystem::filesystem_error& e) { + error_dialog( + nullptr, + wxString::Format( + _("An exception occurred: %s (%s) (%s)\n\n%s"), + std_to_wx(e.what()), + std_to_wx(e.path1().string()), + std_to_wx(e.path2().string()), + dcpomatic::wx::report_problem() + ) + ); + } catch (exception& e) { + error_dialog( + nullptr, + wxString::Format( + _("An exception occurred: %s.\n\n%s"), + std_to_wx(e.what()), + dcpomatic::wx::report_problem() + ) + ); + } catch (...) { + error_dialog(nullptr, wxString::Format(_("An unknown exception occurred. %s"), dcpomatic::wx::report_problem())); + } + } + + /* An unhandled exception has occurred inside the main event loop */ + bool OnExceptionInMainLoop() override + { + report_exception(); + return false; + } + + void OnUnhandledException() override + { + report_exception(); + } + + DOMFrame* _frame = nullptr; +}; + + +IMPLEMENT_APP(App) diff --git a/src/tools/wscript b/src/tools/wscript index 3128486f5..a577214f9 100644 --- a/src/tools/wscript +++ b/src/tools/wscript @@ -34,6 +34,7 @@ def description(tool, variant): 'dcpomatic_playlist': 'DCP-o-matic Playlist Editor', 'dcpomatic_combiner': 'DCP-o-matic Combiner', 'dcpomatic_verifier': 'DCP-o-matic Verifier', + 'dcpomatic_processor': 'DCP-o-matic Processor', } return descriptions[tool] if tool in descriptions else tool @@ -143,7 +144,8 @@ def build(bld): 'dcpomatic_playlist', 'dcpomatic_combiner', 'dcpomatic_editor', - 'dcpomatic_verifier'] + 'dcpomatic_verifier', + 'dcpomatic_processor'] if bld.env.ENABLE_DISK: gui_tools.append('dcpomatic_disk') @@ -181,6 +183,7 @@ def pot(bld): dcpomatic_playlist.cc dcpomatic_server.cc dcpomatic_verifier.cc + dcpomatic_processor.cc """ i18n.pot(os.path.join('src', 'tools'), cc, 'dcpomatic') diff --git a/src/wx/wx_variant.cc b/src/wx/wx_variant.cc index 96b00bba4..dd2021209 100644 --- a/src/wx/wx_variant.cc +++ b/src/wx/wx_variant.cc @@ -85,6 +85,12 @@ variant::wx::dcpomatic_verifier() return std_to_wx(variant::dcpomatic_verifier()); } +wxString +variant::wx::dcpomatic_processor() +{ + return std_to_wx(variant::dcpomatic_processor()); +} + wxString variant::wx::insert_dcpomatic(wxString const& s) { @@ -139,6 +145,12 @@ variant::wx::insert_dcpomatic_verifier(wxString const& s) return wxString::Format(s, dcpomatic_verifier()); } +wxString +variant::wx::insert_dcpomatic_processor(wxString const& s) +{ + return wxString::Format(s, dcpomatic_processor()); +} + wxString variant::wx::report_problem_email() { diff --git a/src/wx/wx_variant.h b/src/wx/wx_variant.h index 69565f9e9..8d71c0b58 100644 --- a/src/wx/wx_variant.h +++ b/src/wx/wx_variant.h @@ -39,6 +39,7 @@ wxString dcpomatic_kdm_creator(); wxString dcpomatic_player(); wxString dcpomatic_playlist_editor(); wxString dcpomatic_verifier(); +wxString dcpomatic_processor(); wxString insert_dcpomatic(wxString const& s); wxString insert_dcpomatic_batch_converter(wxString const& s); @@ -49,6 +50,7 @@ wxString insert_dcpomatic_kdm_creator(wxString const& s); wxString insert_dcpomatic_player(wxString const& s); wxString insert_dcpomatic_playlist_editor(wxString const& s); wxString insert_dcpomatic_verifier(wxString const& s); +wxString insert_dcpomatic_processor(wxString const& s); wxString report_problem_email(); -- cgit v1.2.3