Master me processing ()

This commit is contained in:
Thomas Göttgens 2024-02-13 14:22:04 +01:00 committed by GitHub
parent c3d85a4e22
commit 62b1d813a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 527 additions and 1 deletions
Dockerfile
api/libretime_api/core
install
legacy
application
public/js/airtime/preferences
playout
libretime_playout/liquidsoap
tests

View File

@ -98,17 +98,28 @@ ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
# Playout #
#======================================================================================#
FROM python-base-ffmpeg as libretime-playout
ARG MASTER_ME=1.2.0
COPY tools/packages.py /tmp/packages.py
COPY playout/packages.ini /tmp/packages.ini
RUN set -eux \
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xz-utils \
$(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /tmp/packages.py /tmp/packages.ini
# get master_me soundprocessor plugin from github
RUN set -eux \
&& mkdir /tmp/master_me \
&& mkdir /usr/lib/ladspa \
&& curl -LJO https://github.com/trummerschlunk/master_me/releases/download/$MASTER_ME/master_me-$MASTER_ME-linux-x86_64.tar.xz \
&& tar -xf master_me-$MASTER_ME-linux-x86_64.tar.xz -C /tmp/master_me/ \
&& cp /tmp/master_me/master_me-$MASTER_ME/master_me-ladspa.so /usr/lib/ladspa/ \
&& rm -rf /tmp/master_me \
&& rm -f master_me-$MASTER_ME-linux-x86_64.tar.xz
WORKDIR /src
COPY playout/requirements.txt .

View File

@ -20,6 +20,8 @@ class StreamPreferences(BaseModel):
message_offline: str
replay_gain_enabled: bool
replay_gain_offset: float
master_me_preset: int
master_me_lufs: int
# input_auto_switch_off: bool
# input_auto_switch_on: bool
@ -85,6 +87,8 @@ class Preference(models.Model):
message_offline=entries.get("off_air_meta") or "Offline",
replay_gain_enabled=entries.get("enable_replay_gain") == "1",
replay_gain_offset=float(entries.get("replay_gain_modifier") or 0.0),
master_me_preset=int(entries.get("master_me_preset") or 0),
master_me_lufs=int(entries.get("master_me_lufs") or -16),
)
@classmethod

View File

@ -18,6 +18,8 @@ def test_preference_get_stream_preferences(db):
"message_offline": "LibreTime - offline",
"replay_gain_enabled": True,
"replay_gain_offset": 0.0,
"master_me_lufs": -16,
"master_me_preset": 0,
}

View File

@ -11,6 +11,10 @@ def test_stream_preferences_get(db, api_client: APIClient):
"message_offline": "LibreTime - offline",
"replay_gain_enabled": True,
"replay_gain_offset": 0.0,
"master_me_lufs": -16,
"master_me_preset": 0,
"replay_gain_enabled": True,
"replay_gain_offset": 0.0,
}

View File

@ -21,6 +21,8 @@ class StreamPreferencesView(views.APIView):
"message_offline",
"replay_gain_enabled",
"replay_gain_offset",
"master_me_preset",
"master_me_lufs",
}
)
)

23
install
View File

@ -48,6 +48,9 @@ EOF
SCRIPT_DIR="$( cd "$( dirname "$0")" && pwd)"
# version of sound compressor to download
MASTER_ME="1.2.0"
version() {
if [ ! -f "$SCRIPT_DIR/VERSION" ]; then
make VERSION > /dev/null
@ -73,6 +76,7 @@ Options:
--no-setup-icecast Do not setup Icecast.
--no-setup-postgresql Do not setup Postgresql.
--no-setup-rabbitmq Do not setup RabbitMQ.
--setup-master-me Install master_me liquidsoap plugin.
Environment variables:
@ -117,6 +121,8 @@ LIBRETIME_SETUP_ICECAST=${LIBRETIME_SETUP_ICECAST:-true}
LIBRETIME_SETUP_POSTGRESQL=${LIBRETIME_SETUP_POSTGRESQL:-true}
# > Create a default rabbitmq user with a random password.
LIBRETIME_SETUP_RABBITMQ=${LIBRETIME_SETUP_RABBITMQ:-true}
# > Optional: install Processor Plugin
LIBRETIME_SETUP_MASTER_ME=${LIBRETIME_SETUP_MASTER_ME:-false}
# > Comma separated list of sections to exclude from packages list.
LIBRETIME_PACKAGES_EXCLUDES=${LIBRETIME_PACKAGES_EXCLUDES:-}
@ -154,6 +160,10 @@ while [[ $# -gt 0 ]]; do
LIBRETIME_SETUP_RABBITMQ=false
shift 1
;;
--setup-master-me)
LIBRETIME_SETUP_MASTER_ME=true
shift 1
;;
--packages-excludes)
LIBRETIME_PACKAGES_EXCLUDES=$2
shift 2
@ -642,6 +652,19 @@ section "Playout"
# shellcheck disable=SC2046
install_packages $(list_packages "$SCRIPT_DIR/playout")
# install master_me liquidsoap plugin
if $LIBRETIME_SETUP_MASTER_ME; then
install_packages xz-utils
info "installing master_me liquidsoap plugin"
mkdir /tmp/master_me
mkdir /usr/lib/ladspa
curl -LJO https://github.com/trummerschlunk/master_me/releases/download/$MASTER_ME/master_me-$MASTER_ME-linux-x86_64.tar.xz
tar -xf master_me-$MASTER_ME-linux-x86_64.tar.xz -C /tmp/master_me/
cp /tmp/master_me/master_me-$MASTER_ME/master_me-ladspa.so /usr/lib/ladspa/
rm -rf /tmp/master_me
rm -f master_me-$MASTER_ME-linux-x86_64.tar.xz
fi
install_python_app "$SCRIPT_DIR/playout"
link_python_app libretime-liquidsoap
link_python_app libretime-playout

View File

@ -202,6 +202,14 @@ class PreferenceController extends Zend_Controller_Action
// Application_Model_RabbitMq::PushSchedule();
}
// handle master_me plugin
if (Application_Model_Preference::GetMasterMePreset() != $values['masterMe']) {
Application_Model_Preference::SetMasterMePreset($values['masterMe']);
}
if (Application_Model_Preference::GetMasterMeLufs() != $values['masterMeLufs']) {
Application_Model_Preference::SetMasterMeLufs($values['masterMeLufs']);
}
// store stream update timestamp
Application_Model_Preference::SetStreamUpdateTimestamp();

View File

@ -53,6 +53,29 @@ class Application_Form_StreamSetting extends Zend_Form
->setDecorators(['ViewHelper']);
$this->addElement($replay_gain);
$master_me = new Zend_Form_Element_Radio('masterMe');
$master_me
->setLabel(_('Master Me Processor Preset:'))
->setMultiOptions([
_('Disabled'),
_('Speech General (-16 LUFS)'),
_('Music General (-16 LUFS)'),
_('EBU R128 (-23 LUFS)'),
_('Apple Podcasts (-16 LUFS)'),
_('YouTube (-14 LUFS)'),
])
->setValue(Application_Model_Preference::GetMasterMePreset())
->setDecorators(['ViewHelper']);
$this->addElement($master_me);
$master_me_lufs = new Zend_Form_Element_Hidden('masterMeLufs');
$master_me_lufs
->setLabel(_('Master-Me LUFS'))
->setValue(Application_Model_Preference::getMasterMeLufs())
->setAttribs(['style' => 'border: 0; color: #f6931f; font-weight: bold;'])
->setDecorators(['ViewHelper']);
$this->addElement($master_me_lufs);
$output_sound_device = new Zend_Form_Element_Checkbox('output_sound_device');
$output_sound_device
->setLabel(_('Hardware Audio Output:'))

View File

@ -1268,6 +1268,32 @@ class Application_Model_Preference
self::setValue('replay_gain_modifier', $rg_modifier, false);
}
public static function GetMasterMePreset()
{
return self::getValue('master_me_preset');
}
public static function SetMasterMePreset($preset)
{
self::setValue('master_me_preset', $preset);
}
public static function GetMasterMeLufs()
{
$mm_lufs = self::getValue('master_me_lufs');
if ($mm_lufs === '') {
return '0';
}
return $mm_lufs;
}
public static function SetMasterMeLufs($mm_lufs)
{
self::setValue('master_me_lufs', $mm_lufs);
}
public static function SetHistoryItemTemplate($value)
{
self::setValue('history_item_template', $value);

View File

@ -74,6 +74,51 @@
<?php endif; ?>
<div id="slider-range-max" style="width: 99%"></div>
</dd>
<dt id="masterMe-label">
<label class="optional"><?php echo $this->form->getElement('masterMe')->getLabel() ?></label>
</dt>
<dd id="masterMe-element" class="radio-list">
<?php $i = 0;
$value = $this->form->getElement('masterMe')->getValue();
?>
<?php foreach ($this->form->getElement('masterMe')->getMultiOptions() as $radio) : ?>
<label for="masterMe-<?php echo $i ?>">
<input type="radio" value="<?php echo $i ?>" id="masterMe-<?php echo $i ?>" name="masterMe" <?php if ($i == $value) {
echo 'checked="checked"';
} ?>>
<?php echo $radio ?><br><br>
</input>
</label>
<?php $i = $i + 1; ?>
<?php endforeach; ?>
<?php if ($this->form->getElement('masterMe')->hasErrors()) : ?>
<ul class='errors'>
<?php foreach ($this->form->getElement('masterMe')->getMessages() as $error) : ?>
<li><?php echo $error; ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</dd>
<dt id="masterMeLufs-label" class="block-display">
<label><?php echo $this->form->getElement('masterMeLufs')->getLabel() ?>:
</label>
<span id="mm_lufs_value" style="border: 0; color: #f6931f; font-weight: bold;">
<?php echo $this->form->getElement('masterMeLufs')->getValue() ?>
</span>
<?php echo _("LUFS") ?>
</dt>
<dd id="masterMeLufs-element" class="block-display">
<?php echo $this->form->getElement('masterMeLufs') ?>
<?php if ($this->form->getElement('masterMeLufs')->hasErrors()) : ?>
<ul class='errors'>
<?php foreach ($this->form->getElement('masterMeLufs')->getMessages() as $error) : ?>
<li><?php echo $error; ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<div id="lufs-range-max" style="width: 99%"></div>
</dd>
</dl>
</fieldset>
<?php echo $this->form->getSubform('live_stream_subform'); ?>

View File

@ -412,6 +412,20 @@ function setSliderForReplayGain() {
$("#replayGainModifier").val($("#slider-range-max").slider("value"));
}
function setSliderForLUFS() {
$("#lufs-range-max").slider({
range: "max",
min: -25,
max: -10,
value: $("#mm_lufs_value").html(),
slide: function (event, ui) {
$("#masterMeLufs").val(ui.value);
$("#mm_lufs_value").html(ui.value);
},
});
$("#masterMeLufs").val($("#lufs-range-max").slider("value"));
}
function setPseudoAdminPassword(s1, s2, s3, s4) {
if (s1) {
$("#s1_data-admin_pass").val("xxxxxx");
@ -440,9 +454,22 @@ function getAdminPasswordStatus() {
$(document).ready(function () {
setupEventListeners();
setSliderForReplayGain();
setSliderForLUFS();
getAdminPasswordStatus();
var s = $("[name^='customStreamSettings']:checked");
$("#masterMe-element label input").change(function (e) {
var x = $('label[for="' + e.target.id + '"]').html();
try {
x = parseInt(x.match(/([0-9-]+).LUFS/)[1]);
} catch (err) {
x = -16; // default
}
$("#masterMeLufs").val(x);
$("#lufs-range-max").slider({ value: x });
$("#mm_lufs_value").html(x);
});
$("[id^='stream_save'], [name^='customStreamSettings']").live(
"click",
function () {
@ -460,6 +487,7 @@ $(document).ready(function () {
}
setupEventListeners();
setSliderForReplayGain();
setSliderForLUFS();
getAdminPasswordStatus();
});
},

View File

@ -19,6 +19,8 @@ class StreamPreferences(BaseModel):
message_offline: str
replay_gain_enabled: bool
replay_gain_offset: float
master_me_preset: int
master_me_lufs: int
class StreamState(BaseModel):

View File

@ -39,6 +39,8 @@ input_fade_transition = interactive.float("input_fade_transition", {{ preference
%include "{{ paths.lib_filepath }}"
{% include "master_me.liq.j2" %}
{% include "outputs.liq.j2" %}
gateway("started")

View File

@ -0,0 +1,326 @@
{%- if preferences.master_me_preset == 1 -%}
s = ladspa.master_me(
bypass = false,
target = {{ preferences.master_me_lufs }},
brickwall_bypass = false,
brickwall_ceiling = -1.0,
brickwall_release = 75.0,
eq_bypass = false,
eq_highpass_freq = 20.0,
eq_side_bandwidth = 1.0,
eq_side_freq = 600.0,
eq_side_gain = 0.0,
eq_tilt_gain = 0.0,
gate_attack = 1.0,
gate_bypass = true,
gate_hold = 50.0,
gate_release = 500.0,
gate_threshold = -90.0,
kneecomp_attack = 5.0,
kneecomp_bypass = false,
kneecomp_dry_wet = 50,
kneecomp_ff_fb = 50,
kneecomp_knee = 9.0,
kneecomp_link = 60,
kneecomp_makeup = 0.0,
kneecomp_release = 50.0,
kneecomp_strength = 15,
kneecomp_tar_thresh = -6.0,
leveler_brake_threshold = -20.0,
leveler_bypass = false,
leveler_max = 30.0,
leveler_max__ = 30.0,
leveler_speed = 20,
limiter_attack = 1.0,
limiter_bypass = false,
limiter_ff_fb = 50,
limiter_knee = 3.0,
limiter_makeup = 0.0,
limiter_release = 40.0,
limiter_strength = 80,
limiter_tar_thresh = 3.0,
mscomp_bypass = false,
high_attack = 0.0,
high_crossover = 8000.0,
high_knee = 12.0,
high_link = 30,
high_release = 50.0,
high_strength = 40,
high_tar_thresh = -7.0,
low_attack = 10.0,
low_crossover = 60.0,
low_knee = 12.0,
low_link = 70,
low_release = 80.0,
low_strength = 20,
low_tar_thresh = -5.0,
makeup = 1.0,
dc_blocker = true,
input_gain = 0.0,
mono = false,
phase_l = false,
phase_r = false,
stereo_correct = false,
s
)
{%- elif preferences.master_me_preset == 2 -%}
s = ladspa.master_me(
bypass = false,
target = {{ preferences.master_me_lufs }},
brickwall_bypass = false,
brickwall_ceiling = -1.0,
brickwall_release = 75.0,
eq_bypass = false,
eq_highpass_freq = 5.0,
eq_side_bandwidth = 1.0,
eq_side_freq = 600.0,
eq_side_gain = 1.0,
eq_tilt_gain = 0.0,
gate_attack = 0.0,
gate_bypass = true,
gate_hold = 50.0,
gate_release = 430.5,
gate_threshold = -90.0,
kneecomp_attack = 20.0,
kneecomp_bypass = false,
kneecomp_dry_wet = 50,
kneecomp_ff_fb = 50,
kneecomp_knee = 6.0,
kneecomp_link = 60,
kneecomp_makeup = 0.0,
kneecomp_release = 340.0,
kneecomp_strength = 20,
kneecomp_tar_thresh = -4.0,
leveler_brake_threshold = -10.0,
leveler_bypass = false,
leveler_max = 10.0,
leveler_max__ = 10.0,
leveler_speed = 20,
limiter_attack = 3.0,
limiter_bypass = false,
limiter_ff_fb = 50,
limiter_knee = 3.0,
limiter_makeup = 0.0,
limiter_release = 40.0,
limiter_strength = 80,
limiter_tar_thresh = 6.0,
mscomp_bypass = false,
high_attack = 8.0,
high_crossover = 8000.0,
high_knee = 12.0,
high_link = 30,
high_release = 30.0,
high_strength = 30,
high_tar_thresh = -12.0,
low_attack = 15.0,
low_crossover = 60.0,
low_knee = 12.0,
low_link = 70,
low_release = 150.0,
low_strength = 10,
low_tar_thresh = -3.0,
makeup = 1.0,
dc_blocker = false,
input_gain = 0.0,
mono = false,
phase_l = false,
phase_r = false,
stereo_correct = false,
s
)
{%- elif preferences.master_me_preset == 3 -%}
s = ladspa.master_me(
bypass = false,
target = {{ preferences.master_me_lufs }},
brickwall_bypass = false,
brickwall_ceiling = -1.0,
brickwall_release = 75.0,
eq_bypass = false,
eq_highpass_freq = 20.0,
eq_side_bandwidth = 1.0,
eq_side_freq = 600.0,
eq_side_gain = 0.0,
eq_tilt_gain = 0.0,
gate_attack = 1.0,
gate_bypass = true,
gate_hold = 50.0,
gate_release = 500.0,
gate_threshold = -90.0,
kneecomp_attack = 5.0,
kneecomp_bypass = true,
kneecomp_dry_wet = 50,
kneecomp_ff_fb = 50,
kneecomp_knee = 9.0,
kneecomp_link = 60,
kneecomp_makeup = 0.0,
kneecomp_release = 50.0,
kneecomp_strength = 15,
kneecomp_tar_thresh = -6.0,
leveler_brake_threshold = -20.0,
leveler_bypass = false,
leveler_max = 30.0,
leveler_max__ = 30.0,
leveler_speed = 40,
limiter_attack = 1.0,
limiter_bypass = false,
limiter_ff_fb = 50,
limiter_knee = 3.0,
limiter_makeup = 0.0,
limiter_release = 40.0,
limiter_strength = 80,
limiter_tar_thresh = 3.0,
mscomp_bypass = false,
high_attack = 0.0,
high_crossover = 8000.0,
high_knee = 12.0,
high_link = 30,
high_release = 50.0,
high_strength = 40,
high_tar_thresh = -8.0,
low_attack = 10.0,
low_crossover = 60.0,
low_knee = 12.0,
low_link = 70,
low_release = 80.0,
low_strength = 20,
low_tar_thresh = -6.0,
makeup = 1.0,
dc_blocker = true,
input_gain = 0.0,
mono = false,
phase_l = false,
phase_r = false,
stereo_correct = false,
s
)
{%- elif preferences.master_me_preset == 4 -%}
s = ladspa.master_me(
bypass = false,
target = {{ preferences.master_me_lufs }},
brickwall_bypass = false,
brickwall_ceiling = -1.0,
brickwall_release = 75.0,
eq_bypass = false,
eq_highpass_freq = 20.0,
eq_side_bandwidth = 1.0,
eq_side_freq = 600.0,
eq_side_gain = 0.0,
eq_tilt_gain = 0.0,
gate_attack = 1.0,
gate_bypass = true,
gate_hold = 50.0,
gate_release = 500.0,
gate_threshold = -90.0,
kneecomp_attack = 5.0,
kneecomp_bypass = true,
kneecomp_dry_wet = 50,
kneecomp_ff_fb = 50,
kneecomp_knee = 9.0,
kneecomp_link = 60,
kneecomp_makeup = 0.0,
kneecomp_release = 50.0,
kneecomp_strength = 15,
kneecomp_tar_thresh = -6.0,
leveler_brake_threshold = -20.0,
leveler_bypass = false,
leveler_max = 30.0,
leveler_max__ = 30.0,
leveler_speed = 50,
limiter_attack = 1.0,
limiter_bypass = false,
limiter_ff_fb = 50,
limiter_knee = 3.0,
limiter_makeup = 0.0,
limiter_release = 40.0,
limiter_strength = 80,
limiter_tar_thresh = 3.0,
mscomp_bypass = false,
high_attack = 0.0,
high_crossover = 8000.0,
high_knee = 12.0,
high_link = 30,
high_release = 50.0,
high_strength = 40,
high_tar_thresh = -8.0,
low_attack = 10.0,
low_crossover = 60.0,
low_knee = 12.0,
low_link = 70,
low_release = 80.0,
low_strength = 20,
low_tar_thresh = -6.0,
makeup = 1.0,
dc_blocker = true,
input_gain = 0.0,
mono = false,
phase_l = false,
phase_r = false,
stereo_correct = false,
s
)
{%- elif preferences.master_me_preset == 5 -%}
s = ladspa.master_me(
bypass = false,
target = {{ preferences.master_me_lufs }},
brickwall_bypass = false,
brickwall_ceiling = -1.0,
brickwall_release = 75.0,
eq_bypass = false,
eq_highpass_freq = 20.0,
eq_side_bandwidth = 1.0,
eq_side_freq = 600.0,
eq_side_gain = 0.0,
eq_tilt_gain = 0.0,
gate_attack = 1.0,
gate_bypass = true,
gate_hold = 50.0,
gate_release = 500.0,
gate_threshold = -90.0,
kneecomp_attack = 5.0,
kneecomp_bypass = true,
kneecomp_dry_wet = 50,
kneecomp_ff_fb = 50,
kneecomp_knee = 9.0,
kneecomp_link = 60,
kneecomp_makeup = 0.0,
kneecomp_release = 50.0,
kneecomp_strength = 15,
kneecomp_tar_thresh = -6.0,
leveler_brake_threshold = -20.0,
leveler_bypass = false,
leveler_max = 30.0,
leveler_max__ = 30.0,
leveler_speed = 50,
limiter_attack = 1.0,
limiter_bypass = false,
limiter_ff_fb = 50,
limiter_knee = 3.0,
limiter_makeup = 0.0,
limiter_release = 40.0,
limiter_strength = 80,
limiter_tar_thresh = 3.0,
mscomp_bypass = false,
high_attack = 0.0,
high_crossover = 8000.0,
high_knee = 12.0,
high_link = 30,
high_release = 30.0,
high_strength = 40,
high_tar_thresh = -8.0,
low_attack = 10.0,
low_crossover = 60.0,
low_knee = 12.0,
low_link = 70,
low_release = 80.0,
low_strength = 20,
low_tar_thresh = -6.0,
makeup = 1.0,
dc_blocker = true,
input_gain = 0.0,
mono = false,
phase_l = false,
phase_r = false,
stereo_correct = false,
s
)
{%- endif -%}

View File

@ -47,4 +47,6 @@ def stream_preferences():
message_offline="LibreTime - offline",
replay_gain_enabled=True,
replay_gain_offset=-3.5,
master_me_lufs=-16,
master_me_preset=0,
)

View File

@ -32,6 +32,8 @@
gateway("started")
'''
@ -72,6 +74,8 @@
gateway("started")
'''
@ -107,6 +111,8 @@
%include "/fake/1.4/ls_script.liq"
# icecast:1
output_icecast_1_source = s
# Disable ogg metadata
@ -169,6 +175,8 @@
%include "/fake/1.4/ls_script.liq"
# shoutcast:1
output_shoutcast_1_source = s
output.shoutcast(
@ -227,6 +235,8 @@
%include "/fake/1.4/ls_script.liq"
# icecast:1
output_icecast_1_source = s
# Disable ogg metadata
@ -310,6 +320,8 @@
%include "/fake/1.4/ls_script.liq"
# pulseaudio:1
%ifndef output.pulseaudio
log("output.pulseaudio is not defined!")
@ -358,6 +370,8 @@
%include "/fake/1.4/ls_script.liq"
# pulseaudio:1
%ifndef output.pulseaudio
log("output.pulseaudio is not defined!")
@ -409,6 +423,8 @@
gateway("started")
'''

View File

@ -427,6 +427,8 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
"message_offline": "",
"replay_gain_enabled": True,
"replay_gain_offset": -3.5,
"master_me_lufs": -16,
"master_me_preset": 0,
},
)