Implement Liquidsoap 2.2.x Rolling Release (#6249)

* Update paths for fallbacks in utility scripts.

* Move LS util files up.

* Update installed LS version.

* Initial LS config changes for 2.2.x.

* Fix Shoutcast on 2.2.x.

* Update changelog.

* Replace deprecated LS operators & fix a warning (#6246)

* Update Liquidsoap 2.2.x rolling release.

* Don't send empty annotations.

* Update for Enums.
This commit is contained in:
Buster Neece 2023-06-05 07:18:50 -05:00 committed by GitHub
parent 437ca77ead
commit 35a6c8c014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 134 additions and 86 deletions

View File

@ -3,6 +3,20 @@
These changes have not yet been incorporated into a stable release, but if you are on the latest version of the rolling These changes have not yet been incorporated into a stable release, but if you are on the latest version of the rolling
release channel, you can take advantage of these new features and fixes. release channel, you can take advantage of these new features and fixes.
## New Features/Changes
- **Update to Liquidsoap 2.2.x**: We're updating to the latest version of Liquidsoap, which includes many bug fixes,
performance improvements and other changes. We have adopted our syntax to match Liquidsoap's new supported syntax, but
if you use custom Liquidsoap code, you will need to update your code accordingly. You can see the most important
changes in this [migration guide](https://www.liquidsoap.info/doc-dev/migrating.html#from-2.1.x-to-2.2.x). The most
common changes you will need to make are to mutable (`ref()`) variables:
- `!var` becomes `var()`
- `var := value` optionally becomes `var.set(value)`
## Code Quality/Technical Changes
## Bug Fixes
--- ---
# AzuraCast 0.18.3 (Jun 5, 2023) # AzuraCast 0.18.3 (Jun 5, 2023)

View File

@ -31,4 +31,6 @@ interface StationMountInterface
public function getAutodjAdapterType(): AdapterTypeInterface; public function getAutodjAdapterType(): AdapterTypeInterface;
public function getIsPublic(): bool; public function getIsPublic(): bool;
public function getIsShoutcast(): bool;
} }

View File

@ -413,6 +413,14 @@ class StationMount implements
return $this->getStation()->getFrontendType(); return $this->getStation()->getFrontendType();
} }
public function getIsShoutcast(): bool
{
return match ($this->getAutodjAdapterType()) {
FrontendAdapters::Shoutcast => true,
default => false
};
}
/** /**
* Retrieve the API version of the object/array. * Retrieve the API version of the object/array.
* *

View File

@ -11,7 +11,6 @@ use App\Radio\Enums\StreamProtocols;
use App\Radio\Remote\AbstractRemote; use App\Radio\Remote\AbstractRemote;
use App\Utilities; use App\Utilities;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
use Stringable; use Stringable;
@ -376,6 +375,14 @@ class StationRemote implements
return (RemoteAdapters::AzuraRelay !== $this->getType()); return (RemoteAdapters::AzuraRelay !== $this->getType());
} }
public function getIsShoutcast(): bool
{
return match ($this->getAutodjAdapterType()) {
RemoteAdapters::Shoutcast1, RemoteAdapters::Shoutcast2 => true,
default => false,
};
}
/** /**
* Retrieve the API version of the object/array. * Retrieve the API version of the object/array.
* *

View File

@ -83,44 +83,56 @@ final class Annotations implements EventSubscriberInterface
return; return;
} }
$backendConfig = $station->getBackendConfig();
$annotations = []; $annotations = [];
$annotationsRaw = [ $annotationsRaw = array_filter([
'title' => $media->getTitle(), 'title' => $media->getTitle(),
'artist' => $media->getArtist(), 'artist' => $media->getArtist(),
'duration' => $media->getLength(), 'duration' => $media->getLength(),
'song_id' => $media->getSongId(), 'song_id' => $media->getSongId(),
'media_id' => $media->getId(), 'media_id' => $media->getId(),
'liq_amplify' => $media->getAmplify() ?? 0.0, 'liq_amplify' => $media->getAmplify(),
'liq_cross_duration' => $media->getFadeOverlap() ?? $backendConfig->getCrossfadeDuration(), 'liq_cross_duration' => $media->getFadeOverlap(),
'liq_fade_in' => $media->getFadeIn() ?? $backendConfig->getCrossfade(), 'liq_fade_in' => $media->getFadeIn(),
'liq_fade_out' => $media->getFadeOut() ?? $backendConfig->getCrossfade(), 'liq_fade_out' => $media->getFadeOut(),
'liq_cue_in' => $media->getCueIn(), 'liq_cue_in' => $media->getCueIn(),
'liq_cue_out' => $media->getCueOut(), 'liq_cue_out' => $media->getCueOut(),
]; ]);
// Safety checks for cue lengths. // Safety checks for cue lengths.
if ($annotationsRaw['liq_cue_out'] < 0) { if (
isset($annotationsRaw['liq_cue_out'])
&& $annotationsRaw['liq_cue_out'] < 0
) {
$cue_out = abs($annotationsRaw['liq_cue_out']); $cue_out = abs($annotationsRaw['liq_cue_out']);
if (0.0 === $cue_out || $cue_out > $annotationsRaw['duration']) {
$annotationsRaw['liq_cue_out'] = null; if (0.0 === $cue_out) {
} else { unset($annotationsRaw['liq_cue_out']);
$annotationsRaw['liq_cue_out'] = max(0, $annotationsRaw['duration'] - $cue_out); }
if (isset($annotationsRaw['duration'])) {
if ($cue_out > $annotationsRaw['duration']) {
unset($annotationsRaw['liq_cue_out']);
} else {
$annotationsRaw['liq_cue_out'] = max(0, $annotationsRaw['duration'] - $cue_out);
}
} }
} }
if ($annotationsRaw['liq_cue_out'] > $annotationsRaw['duration']) {
$annotationsRaw['liq_cue_out'] = null; if (
isset($annotationsRaw['liq_cue_out'], $annotationsRaw['duration'])
&& $annotationsRaw['liq_cue_out'] > $annotationsRaw['duration']
) {
unset($annotationsRaw['liq_cue_out']);
} }
if ($annotationsRaw['liq_cue_in'] > $annotationsRaw['duration']) {
$annotationsRaw['liq_cue_in'] = null; if (
isset($annotationsRaw['liq_cue_in'], $annotationsRaw['duration'])
&& $annotationsRaw['liq_cue_in'] > $annotationsRaw['duration']
) {
unset($annotationsRaw['liq_cue_in']);
} }
foreach ($annotationsRaw as $name => $prop) { foreach ($annotationsRaw as $name => $prop) {
if (null === $prop) {
continue;
}
$prop = ConfigWriter::annotateString((string)$prop); $prop = ConfigWriter::annotateString((string)$prop);
// Convert Liquidsoap-specific annotations to floats. // Convert Liquidsoap-specific annotations to floats.

View File

@ -201,11 +201,9 @@ final class ConfigWriter implements EventSubscriberInterface
settings.server.socket.path.set("{$socketFile}") settings.server.socket.path.set("{$socketFile}")
settings.harbor.bind_addrs.set(["0.0.0.0"]) settings.harbor.bind_addrs.set(["0.0.0.0"])
settings.tag.encodings.set(["UTF-8","ISO-8859-1"])
settings.encoder.metadata.export.set(["artist","title","album","song"]) settings.encoder.metadata.export.set(["artist","title","album","song"])
setenv("TZ", "{$stationTz}") environment.set("TZ", "{$stationTz}")
autodj_is_loading = ref(true) autodj_is_loading = ref(true)
ignore(autodj_is_loading) ignore(autodj_is_loading)
@ -233,8 +231,8 @@ final class ConfigWriter implements EventSubscriberInterface
<<<LIQ <<<LIQ
azuracast_api_url = "{$stationApiUrl}" azuracast_api_url = "{$stationApiUrl}"
azuracast_api_key = "{$stationApiAuth}" azuracast_api_key = "{$stationApiAuth}"
def azuracast_api_call(~timeout_ms=2000, url, payload) = def azuracast_api_call(~timeout=2.0, url, payload) =
full_url = "#{azuracast_api_url}/#{url}" full_url = "#{azuracast_api_url}/#{url}"
log("API #{url} - Sending POST request to '#{full_url}' with body: #{payload}") log("API #{url} - Sending POST request to '#{full_url}' with body: #{payload}")
@ -245,7 +243,7 @@ final class ConfigWriter implements EventSubscriberInterface
("User-Agent", "Liquidsoap AzuraCast"), ("User-Agent", "Liquidsoap AzuraCast"),
("X-Liquidsoap-Api-Key", "#{azuracast_api_key}") ("X-Liquidsoap-Api-Key", "#{azuracast_api_key}")
], ],
timeout_ms=timeout_ms, timeout=timeout,
data=payload data=payload
) )
@ -271,7 +269,7 @@ final class ConfigWriter implements EventSubscriberInterface
["#{station_media_dir}/#{arg}"] ["#{station_media_dir}/#{arg}"]
end end
add_protocol( protocol.add(
"media", "media",
azuracast_media_protocol, azuracast_media_protocol,
doc="Pull files from AzuraCast media directory.", doc="Pull files from AzuraCast media directory.",
@ -283,15 +281,15 @@ final class ConfigWriter implements EventSubscriberInterface
$event->appendBlock( $event->appendBlock(
<<<LIQ <<<LIQ
def azuracast_media_protocol(~rlog=_,~maxtime,arg) = def azuracast_media_protocol(~rlog=_,~maxtime,arg) =
timeout_ms = 1000 * (int_of_float(maxtime) - int_of_float(time())) timeout = 1000.0 * (maxtime - time())
j = json() j = json()
j.add("uri", arg) j.add("uri", arg)
[azuracast_api_call(timeout_ms=timeout_ms, "cp", json.stringify(j))] [azuracast_api_call(timeout=timeout, "cp", json.stringify(j))]
end end
add_protocol( protocol.add(
"media", "media",
azuracast_media_protocol, azuracast_media_protocol,
temporary=true, temporary=true,
@ -605,22 +603,22 @@ final class ConfigWriter implements EventSubscriberInterface
# Delayed ping for AutoDJ Next Song # Delayed ping for AutoDJ Next Song
def wait_for_next_song(autodj) def wait_for_next_song(autodj)
autodj_ping_attempts := !autodj_ping_attempts + 1 autodj_ping_attempts.set(autodj_ping_attempts() + 1)
if source.is_ready(autodj) then if source.is_ready(autodj) then
log("AutoDJ is ready!") log("AutoDJ is ready!")
autodj_is_loading := false autodj_is_loading.set(false)
-1.0 -1.0
elsif !autodj_ping_attempts > 200 then elsif autodj_ping_attempts() > 200 then
log("AutoDJ could not be initialized within the specified timeout.") log("AutoDJ could not be initialized within the specified timeout.")
autodj_is_loading := false autodj_is_loading.set(false)
-1.0 -1.0
else else
0.5 0.5
end end
end end
dynamic = request.dynamic(id="next_song", timeout=20., retry_delay=10., autodj_next_song) dynamic = request.dynamic(id="next_song", timeout=20.0, retry_delay=10., autodj_next_song)
dynamic = cue_cut(id="cue_next_song", dynamic) dynamic = cue_cut(id="cue_next_song", dynamic)
dynamic_startup = fallback( dynamic_startup = fallback(
@ -630,14 +628,14 @@ final class ConfigWriter implements EventSubscriberInterface
dynamic, dynamic,
source.available( source.available(
blank(id = "autodj_startup_blank", duration = 120.), blank(id = "autodj_startup_blank", duration = 120.),
predicate.activates({!autodj_is_loading}) predicate.activates({autodj_is_loading()})
) )
] ]
) )
radio = fallback(id="autodj_fallback", track_sensitive = true, [dynamic_startup, radio]) radio = fallback(id="autodj_fallback", track_sensitive = true, [dynamic_startup, radio])
ref_dynamic = ref(dynamic); ref_dynamic = ref(dynamic);
thread.run.recurrent(delay=0.25, { wait_for_next_song(!ref_dynamic) }) thread.run.recurrent(delay=0.25, { wait_for_next_song(ref_dynamic()) })
LIQ LIQ
); );
} }
@ -833,7 +831,7 @@ final class ConfigWriter implements EventSubscriberInterface
$event->appendBlock( $event->appendBlock(
<<<LS <<<LS
def live_aware_crossfade(old, new) = def live_aware_crossfade(old, new) =
if !to_live then if to_live() then
# If going to the live show, play a simple sequence # If going to the live show, play a simple sequence
sequence([fade.out(old.source),fade.in(new.source)]) sequence([fade.out(old.source),fade.in(new.source)])
else else
@ -884,13 +882,13 @@ final class ConfigWriter implements EventSubscriberInterface
end end
response = azuracast_api_call( response = azuracast_api_call(
timeout_ms=5000, timeout=5.0,
"auth", "auth",
json.stringify(auth_info) json.stringify(auth_info)
) )
if (response == "true") then if (response == "true") then
last_authenticated_dj := auth_info.user last_authenticated_dj.set(auth_info.user)
true true
else else
false false
@ -898,14 +896,14 @@ final class ConfigWriter implements EventSubscriberInterface
end end
def live_connected(header) = def live_connected(header) =
dj = !last_authenticated_dj dj = last_authenticated_dj()
log("DJ Source connected! Last authenticated DJ: #{dj} - #{header}") log("DJ Source connected! Last authenticated DJ: #{dj} - #{header}")
live_enabled := true live_enabled.set(true)
live_dj := dj live_dj.set(dj)
_ = azuracast_api_call( _ = azuracast_api_call(
timeout_ms=5000, timeout=5.0,
"djon", "djon",
json.stringify({user = dj}) json.stringify({user = dj})
) )
@ -913,13 +911,13 @@ final class ConfigWriter implements EventSubscriberInterface
def live_disconnected() = def live_disconnected() =
_ = azuracast_api_call( _ = azuracast_api_call(
timeout_ms=5000, timeout=5.0,
"djoff", "djoff",
json.stringify({user = !live_dj}) json.stringify({user = live_dj()})
) )
live_enabled := false live_enabled.set(false)
live_dj := "" live_dj.set("")
end end
LIQ LIQ
); );
@ -967,12 +965,12 @@ final class ConfigWriter implements EventSubscriberInterface
# Skip non-live track when live DJ goes live. # Skip non-live track when live DJ goes live.
def check_live() = def check_live() =
if live.is_ready() then if live.is_ready() then
if not !to_live then if not to_live() then
to_live := true to_live.set(true)
radio_without_live.skip() radio_without_live.skip()
end end
else else
to_live := false to_live.set(false)
end end
end end
@ -997,10 +995,11 @@ final class ConfigWriter implements EventSubscriberInterface
recording_extension = "{$recordExtension}" recording_extension = "{$recordExtension}"
output.file( output.file(
{$formatString},
{$formatString}, {$formatString},
fun () -> begin fun () -> begin
if (!live_enabled) then if (live_enabled()) then
"#{recording_base_path}/#{!live_dj}/{$recordPathPrefix}_%Y%m%d-%H%M%S.#{recording_extension}.tmp" "#{recording_base_path}/#{live_dj()}/{$recordPathPrefix}_%Y%m%d-%H%M%S.#{recording_extension}.tmp"
else else
"" ""
end end
@ -1058,7 +1057,10 @@ final class ConfigWriter implements EventSubscriberInterface
<<<LIQ <<<LIQ
error_file = single(id="error_jingle", "{$errorFile}") error_file = single(id="error_jingle", "{$errorFile}")
error_file = single(id="error_jingle", "{$errorFile}")
def tag_error_file(m) = def tag_error_file(m) =
ignore(m)
ignore(m) ignore(m)
[("is_error_file", "true")] [("is_error_file", "true")]
end end
@ -1077,10 +1079,10 @@ final class ConfigWriter implements EventSubscriberInterface
def metadata_updated(m) = def metadata_updated(m) =
def f() = def f() =
if (m["is_error_file"] != "true") then if (m["is_error_file"] != "true") then
if (m["title"] != !last_title or m["artist"] != !last_artist) then if (m["title"] != last_title() or m["artist"] != last_artist()) then
last_title := m["title"] last_title.set(m["title"])
last_artist := m["artist"] last_artist.set(m["artist"])
j = json() j = json()
if (m["song_id"] != "") then if (m["song_id"] != "") then
@ -1109,9 +1111,9 @@ final class ConfigWriter implements EventSubscriberInterface
last_metadata = ref([]) last_metadata = ref([])
def handle_jingle_mode(m) = def handle_jingle_mode(m) =
if (m["jingle_mode"] == "true") then if (m["jingle_mode"] == "true") then
!last_metadata last_metadata()
else else
last_metadata := m last_metadata.set(m)
m m
end end
end end
@ -1284,7 +1286,10 @@ final class ConfigWriter implements EventSubscriberInterface
} }
$output_params[] = 'name = "' . self::cleanUpString($station->getName()) . '"'; $output_params[] = 'name = "' . self::cleanUpString($station->getName()) . '"';
$output_params[] = 'description = "' . self::cleanUpString($station->getDescription()) . '"';
if (!$mount->getIsShoutcast()) {
$output_params[] = 'description = "' . self::cleanUpString($station->getDescription()) . '"';
}
$output_params[] = 'genre = "' . self::cleanUpString($station->getGenre()) . '"'; $output_params[] = 'genre = "' . self::cleanUpString($station->getGenre()) . '"';
if (!empty($station->getUrl())) { if (!empty($station->getUrl())) {
@ -1294,17 +1299,21 @@ final class ConfigWriter implements EventSubscriberInterface
$output_params[] = 'public = ' . ($mount->getIsPublic() ? 'true' : 'false'); $output_params[] = 'public = ' . ($mount->getIsPublic() ? 'true' : 'false');
$output_params[] = 'encoding = "' . $charset . '"'; $output_params[] = 'encoding = "' . $charset . '"';
if (null !== $protocol) { if (!$mount->getIsShoutcast() && null !== $protocol) {
$output_params[] = 'protocol="' . $protocol->value . '"'; $output_params[] = 'protocol="' . $protocol->value . '"';
} }
if ($format->sendIcyMetadata()) { if ($format->sendIcyMetadata()) {
$output_params[] = 'icy_metadata="true"'; $output_params[] = 'send_icy_metadata="true"';
} }
$output_params[] = 'radio'; $output_params[] = 'radio';
return 'output.icecast(' . implode(', ', $output_params) . ')'; $outputCommand = ($mount->getIsShoutcast())
? 'output.shoutcast'
: 'output.icecast';
return $outputCommand . '(' . implode(', ', $output_params) . ')';
} }
private function getOutputFormatString(StreamFormats $format, int $bitrate = 128): string private function getOutputFormatString(StreamFormats $format, int $bitrate = 128): string

View File

@ -1,2 +0,0 @@
liquidsoap.chroot.make('/tmp/liquidsoap')
shutdown()

View File

@ -15,14 +15,12 @@ apt-get install -y --no-install-recommends ladspa-sdk
# Per-architecture LS installs # Per-architecture LS installs
ARCHITECTURE=amd64 ARCHITECTURE=amd64
ARM_FULL_BUILD="${ARM_FULL_BUILD:-false}" if [[ "$(uname -m)" = "aarch64" ]]; then
if [[ "$(uname -m)" = "aarch64" && ${ARM_FULL_BUILD} == "false" ]]; then
ARCHITECTURE=arm64 ARCHITECTURE=arm64
fi fi
# wget -O /tmp/liquidsoap.deb "https://github.com/savonet/liquidsoap/releases/download/v2.1.4/liquidsoap_2.1.4-ubuntu-jammy-1_${ARCHITECTURE}.deb" # wget -O /tmp/liquidsoap.deb "https://github.com/savonet/liquidsoap/releases/download/v2.1.4/liquidsoap_2.1.4-ubuntu-jammy-1_${ARCHITECTURE}.deb"
wget -O /tmp/liquidsoap.deb "https://github.com/savonet/liquidsoap-release-assets/releases/download/rolling-release-v2.1.x/liquidsoap-d6313d1_2.1.5-ubuntu-jammy-1_${ARCHITECTURE}.deb" wget -O /tmp/liquidsoap.deb "https://github.com/savonet/liquidsoap-release-assets/releases/download/rolling-release-v2.2.x/liquidsoap-8101608_2.2.0-ubuntu-jammy-1_${ARCHITECTURE}.deb"
dpkg -i /tmp/liquidsoap.deb dpkg -i /tmp/liquidsoap.deb
apt-get install -y -f --no-install-recommends apt-get install -y -f --no-install-recommends

View File

@ -17,14 +17,14 @@ output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=128), "fallback-[1
output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=192), "fallback-[192].ogg", input, fallible=true) output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=192), "fallback-[192].ogg", input, fallible=true)
output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=256), "fallback-[256].ogg", input, fallible=true) output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=256), "fallback-[256].ogg", input, fallible=true)
output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=320), "fallback-[320].ogg", input, fallible=true) output.file(%vorbis.cbr(samplerate=44100, channels=2, bitrate=320), "fallback-[320].ogg", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=32, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[32].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=32, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[32].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=48, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[48].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=48, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[48].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=64, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[64].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=64, afterburner=false, aot="mpeg4_he_aac_v2", sbr_mode=true), "fallback-[64].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=96, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[96].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=96, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[96].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=128, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[128].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=128, afterburner=false, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[128].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=192, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[192].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=192, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[192].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=256, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[256].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=256, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[256].aac", input, fallible=true)
output.file(%fdkaac(channels=2, samplerate=44100, bitrate=320, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[320].mp4", input, fallible=true) output.file(%fdkaac(channels=2, samplerate=44100, bitrate=320, afterburner=true, aot="mpeg4_aac_lc", sbr_mode=true), "fallback-[320].aac", input, fallible=true)
output.file(%opus(samplerate=48000, bitrate=32, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[32].opus", input, fallible=true) output.file(%opus(samplerate=48000, bitrate=32, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[32].opus", input, fallible=true)
output.file(%opus(samplerate=48000, bitrate=48, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[48].opus", input, fallible=true) output.file(%opus(samplerate=48000, bitrate=48, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[48].opus", input, fallible=true)
output.file(%opus(samplerate=48000, bitrate=64, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[64].opus", input, fallible=true) output.file(%opus(samplerate=48000, bitrate=64, vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band"), "fallback-[64].opus", input, fallible=true)